conduct-cli 0.6.33__tar.gz → 0.6.34__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 (38) hide show
  1. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/PKG-INFO +1 -1
  2. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/pyproject.toml +1 -1
  3. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/guard.py +192 -18
  4. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/guardmcp.py +36 -11
  5. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/base.py +28 -3
  6. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/pretooluse.py +12 -8
  7. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/main.py +5 -7
  8. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/mcp_server.py +16 -2
  9. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  10. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/README.md +0 -0
  11. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/setup.cfg +0 -0
  12. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/setup.py +0 -0
  13. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/__init__.py +0 -0
  14. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/api.py +0 -0
  15. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/__init__.py +0 -0
  16. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/posttooluse.py +0 -0
  17. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/precompact.py +0 -0
  18. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/session_parser.py +0 -0
  19. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/session_start.py +0 -0
  20. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/hooks/stop.py +0 -0
  21. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/log_util.py +0 -0
  22. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/memory.py +0 -0
  23. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli/paxel.py +0 -0
  24. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  25. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  26. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  27. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/requires.txt +0 -0
  28. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/src/conduct_cli.egg-info/top_level.txt +0 -0
  29. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_bash_operator_signature.py +0 -0
  30. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_command_word_matcher.py +0 -0
  31. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_guard_policy.py +0 -0
  32. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_guard_savings.py +0 -0
  33. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_hook_syntax.py +0 -0
  34. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_journal_drain.py +0 -0
  35. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_log_util.py +0 -0
  36. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_proxy_env.py +0 -0
  37. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_signed_policy.py +0 -0
  38. {conduct_cli-0.6.33 → conduct_cli-0.6.34}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.6.33
3
+ Version: 0.6.34
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.6.33"
7
+ version = "0.6.34"
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" }
@@ -18,8 +18,36 @@ CYAN = "\033[36m"
18
18
  YELLOW = "\033[33m"
19
19
 
20
20
  GUARD_DIR = Path.home() / ".conductguard"
21
- CONFIG_PATH = GUARD_DIR / "config.json"
22
- POLICY_PATH = GUARD_DIR / "policy.json"
21
+ CONFIG_PATH = GUARD_DIR / "config.json" # legacy / fallback
22
+ POLICY_PATH = GUARD_DIR / "policy.json" # legacy / fallback
23
+ _CONDUCT_CFG = Path.home() / ".conduct" / "config.json"
24
+
25
+
26
+ def _active_workspace_id() -> str | None:
27
+ """Active workspace from conduct switch — updates at runtime."""
28
+ try:
29
+ if _CONDUCT_CFG.exists():
30
+ return json.loads(_CONDUCT_CFG.read_text()).get("workspace")
31
+ except Exception:
32
+ pass
33
+ return None
34
+
35
+
36
+ def _ws_config_path(ws_id: str) -> Path:
37
+ return GUARD_DIR / "workspaces" / f"{ws_id}.json"
38
+
39
+
40
+ def _ws_policy_path(ws_id: str) -> Path:
41
+ return GUARD_DIR / f"policy_{ws_id}.json"
42
+
43
+
44
+ def active_policy_path() -> Path:
45
+ ws_id = _active_workspace_id()
46
+ if ws_id:
47
+ p = _ws_policy_path(ws_id)
48
+ if p.exists():
49
+ return p
50
+ return POLICY_PATH
23
51
 
24
52
  # ── Hook templates — loaded from real .py files (no string embedding) ─────────
25
53
 
@@ -102,11 +130,8 @@ def _bash_command_words(cmd: str) -> list[str]:
102
130
 
103
131
  def _check_policy(tool_name, tool_input, tokens_before=0):
104
132
  """Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
105
- if not POLICY_PATH.exists():
106
- return None, "allow", None, None
107
- try:
108
- policy = _json.loads(POLICY_PATH.read_text())
109
- except Exception:
133
+ policy = _load_policy()
134
+ if not policy.get("rules"):
110
135
  return None, "allow", None, None
111
136
 
112
137
  rules = policy.get("rules", [])
@@ -305,14 +330,27 @@ def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
305
330
  return chosen
306
331
 
307
332
 
308
- def _load_guard_config() -> dict:
333
+ def _load_guard_config(workspace_id: str | None = None) -> dict:
334
+ ws_id = workspace_id or _active_workspace_id()
335
+ if ws_id:
336
+ p = _ws_config_path(ws_id)
337
+ if p.exists():
338
+ return json.loads(p.read_text())
339
+ # fallback to legacy single-workspace config
309
340
  if CONFIG_PATH.exists():
310
341
  return json.loads(CONFIG_PATH.read_text())
311
342
  return {}
312
343
 
313
344
 
314
- def _save_guard_config(data: dict):
345
+ def _save_guard_config(data: dict, workspace_id: str | None = None):
315
346
  GUARD_DIR.mkdir(parents=True, exist_ok=True)
347
+ ws_id = workspace_id or data.get("workspace_id") or _active_workspace_id()
348
+ if ws_id:
349
+ p = _ws_config_path(ws_id)
350
+ p.parent.mkdir(parents=True, exist_ok=True)
351
+ p.write_text(json.dumps(data, indent=2))
352
+ p.chmod(0o600)
353
+ # always write legacy path so old hook versions keep working
316
354
  CONFIG_PATH.write_text(json.dumps(data, indent=2))
317
355
  CONFIG_PATH.chmod(0o600)
318
356
 
@@ -572,13 +610,22 @@ def _req(method: str, url: str, body=None, token: str = None, api_key: str = Non
572
610
 
573
611
 
574
612
 
575
- def _save_policy(policy: dict):
613
+ def _save_policy(policy: dict, workspace_id: str | None = None):
576
614
  GUARD_DIR.mkdir(parents=True, exist_ok=True)
615
+ ws_id = workspace_id or _active_workspace_id()
616
+ if ws_id:
617
+ _ws_policy_path(ws_id).write_text(json.dumps(policy, indent=2))
618
+ # always write legacy path for old hook versions
577
619
  POLICY_PATH.write_text(json.dumps(policy, indent=2))
578
620
 
579
621
 
580
- def _load_policy() -> dict:
622
+ def _load_policy(workspace_id: str | None = None) -> dict:
581
623
  try:
624
+ ws_id = workspace_id or _active_workspace_id()
625
+ if ws_id:
626
+ p = _ws_policy_path(ws_id)
627
+ if p.exists():
628
+ return json.loads(p.read_text())
582
629
  return json.loads(POLICY_PATH.read_text())
583
630
  except Exception:
584
631
  return {"rules": []}
@@ -774,6 +821,26 @@ def cmd_guard_install(args):
774
821
  if signing_key_hex:
775
822
  _write_signing_key(signing_key_hex)
776
823
 
824
+ # Start watch daemon — auto-discovers agents every 15 min, no user action needed.
825
+ try:
826
+ import subprocess as _sp, sys as _sys
827
+ running, _ = _is_watch_running()
828
+ if not running:
829
+ log_file = open(_WATCH_LOG, "a")
830
+ proc = _sp.Popen(
831
+ [_sys.executable, "-c",
832
+ "from conduct_cli.guard import _watch_loop; _watch_loop()"],
833
+ start_new_session=True,
834
+ stdout=log_file,
835
+ stderr=log_file,
836
+ )
837
+ _WATCH_PID.write_text(str(proc.pid))
838
+ print(f" {GREEN}Guard watch:{RESET} background discovery daemon started (pid {proc.pid})")
839
+ else:
840
+ print(f" {GREEN}Guard watch:{RESET} already running")
841
+ except Exception:
842
+ pass # non-fatal
843
+
777
844
 
778
845
  def _write_signing_key(hex_key: str) -> None:
779
846
  """Write a hex-encoded signing key to ~/.conductguard/signing.key.
@@ -1634,13 +1701,7 @@ def cmd_guard_status(args):
1634
1701
  pass
1635
1702
 
1636
1703
  # Load local policy for rule count
1637
- rule_count = 0
1638
- if POLICY_PATH.exists():
1639
- try:
1640
- policy = json.loads(POLICY_PATH.read_text())
1641
- rule_count = len(policy.get("rules", []))
1642
- except Exception:
1643
- pass
1704
+ rule_count = len(_load_policy().get("rules", []))
1644
1705
 
1645
1706
  # Fetch today's spend
1646
1707
  spend = {}
@@ -1884,6 +1945,11 @@ def register_guard_parser(sub):
1884
1945
  discover_p.add_argument("--config-only", action="store_true", help="Skip process scan, config files only")
1885
1946
  discover_p.add_argument("--report", default=None, metavar="FILE", help="Write full JSON report to file")
1886
1947
 
1948
+ # conduct guard watch
1949
+ watch_p = guard_sub.add_parser("watch", help="Start background daemon — scans every 15 min, auto-pushes to Guard")
1950
+ watch_p.add_argument("--stop", action="store_true", help="Stop the running watch daemon")
1951
+ watch_p.add_argument("--status", action="store_true", help="Show whether the watch daemon is running")
1952
+
1887
1953
  # conduct guard lint [--file PATH]
1888
1954
  lint_p = guard_sub.add_parser("lint", help="Validate local policy — show errors and warnings")
1889
1955
  lint_p.add_argument("--file", default=None, metavar="FILE",
@@ -2011,6 +2077,112 @@ def _scan_processes() -> list[dict]:
2011
2077
  return found
2012
2078
 
2013
2079
 
2080
+ _WATCH_PID = Path.home() / ".conductguard" / "watch.pid"
2081
+ _WATCH_LOG = Path.home() / ".conductguard" / "watch.log"
2082
+ _WATCH_INTERVAL = 15 * 60 # seconds
2083
+
2084
+
2085
+ def _is_watch_running():
2086
+ """Returns (is_running: bool, pid: int | None)."""
2087
+ if not _WATCH_PID.exists():
2088
+ return False, None
2089
+ try:
2090
+ pid = int(_WATCH_PID.read_text().strip())
2091
+ os.kill(pid, 0)
2092
+ return True, pid
2093
+ except (ValueError, ProcessLookupError):
2094
+ _WATCH_PID.unlink(missing_ok=True)
2095
+ return False, None
2096
+
2097
+
2098
+ def _watch_loop():
2099
+ """Background loop — runs discover+push every 15 min. Invoked as subprocess."""
2100
+ import time, json as _json
2101
+ # resolve config at each iteration so workspace switches are picked up
2102
+ def _cfg():
2103
+ return _load_guard_config()
2104
+
2105
+ while True:
2106
+ try:
2107
+ c = _cfg()
2108
+ api_url = _api_url(c)
2109
+ token = c.get("member_token", "")
2110
+ api_key = c.get("api_key", "")
2111
+ agents = []
2112
+ for name, config_path, mcp_key in _discover_config_agents():
2113
+ agents.append({
2114
+ "name": name, "framework": name, "source": "config",
2115
+ "location": str(config_path),
2116
+ "evidence": {"config_path": str(config_path), "under_guard": mcp_key},
2117
+ "risk_score": 20 if mcp_key else 70,
2118
+ "under_guard": mcp_key,
2119
+ })
2120
+ try:
2121
+ process_agents = _scan_processes()
2122
+ registered = {a["framework"] for a in agents if a["under_guard"]}
2123
+ for a in process_agents:
2124
+ a["under_guard"] = a["framework"] in registered
2125
+ agents += process_agents
2126
+ except Exception:
2127
+ pass
2128
+ _req("POST", f"{api_url}/guard/discover/scan",
2129
+ body={"triggered_by": "watch", "agents": agents},
2130
+ token=token, api_key=api_key)
2131
+ except Exception:
2132
+ pass
2133
+ time.sleep(_WATCH_INTERVAL)
2134
+
2135
+
2136
+ def cmd_guard_watch(args):
2137
+ """Start/stop/status the background discovery daemon."""
2138
+ import signal as _signal
2139
+
2140
+ if getattr(args, "status", False):
2141
+ running, pid = _is_watch_running()
2142
+ if running:
2143
+ print(f" Guard watch daemon running (pid {pid})")
2144
+ log_tail = ""
2145
+ if _WATCH_LOG.exists():
2146
+ lines = _WATCH_LOG.read_text().splitlines()
2147
+ log_tail = "\n".join(lines[-5:])
2148
+ if log_tail:
2149
+ print(f" Last log:\n{log_tail}")
2150
+ else:
2151
+ print(" Guard watch daemon not running. Start with: conduct guard watch")
2152
+ return
2153
+
2154
+ if getattr(args, "stop", False):
2155
+ running, pid = _is_watch_running()
2156
+ if not running:
2157
+ print(" Guard watch daemon is not running.")
2158
+ return
2159
+ os.kill(pid, _signal.SIGTERM)
2160
+ _WATCH_PID.unlink(missing_ok=True)
2161
+ print(f" Guard watch daemon stopped (pid {pid}).")
2162
+ return
2163
+
2164
+ # Start
2165
+ running, pid = _is_watch_running()
2166
+ if running:
2167
+ print(f" Guard watch daemon already running (pid {pid}). Stop with: conduct guard watch --stop")
2168
+ return
2169
+
2170
+ _WATCH_PID.parent.mkdir(parents=True, exist_ok=True)
2171
+ log_file = open(_WATCH_LOG, "a")
2172
+ import subprocess, sys as _sys
2173
+ proc = subprocess.Popen(
2174
+ [_sys.executable, "-c",
2175
+ "from conduct_cli.guard import _watch_loop; _watch_loop()"],
2176
+ start_new_session=True,
2177
+ stdout=log_file,
2178
+ stderr=log_file,
2179
+ )
2180
+ _WATCH_PID.write_text(str(proc.pid))
2181
+ print(f" Guard watch daemon started (pid {proc.pid}).")
2182
+ print(f" Scans every 15 min — results visible at conductai.ai/guard/discovery")
2183
+ print(f" Stop with: conduct guard watch --stop")
2184
+
2185
+
2014
2186
  def cmd_guard_lint(args):
2015
2187
  """Validate the local policy file and report errors / warnings."""
2016
2188
  import json as _json, re as _re
@@ -2329,6 +2501,8 @@ def dispatch_guard(args, guard_p):
2329
2501
  cmd_guard_install(args)
2330
2502
  elif guard_command == "discover":
2331
2503
  cmd_guard_discover(args)
2504
+ elif guard_command == "watch":
2505
+ cmd_guard_watch(args)
2332
2506
  elif guard_command == "lint":
2333
2507
  cmd_guard_lint(args)
2334
2508
  elif guard_command == "booster-status":
@@ -24,9 +24,28 @@ import urllib.request
24
24
  import urllib.error
25
25
  from pathlib import Path
26
26
 
27
- GUARD_DIR = Path.home() / ".conductguard"
28
- POLICY_PATH = GUARD_DIR / "policy.json"
29
- CONFIG_PATH = GUARD_DIR / "config.json"
27
+ GUARD_DIR = Path.home() / ".conductguard"
28
+ POLICY_PATH = GUARD_DIR / "policy.json"
29
+ CONFIG_PATH = GUARD_DIR / "config.json"
30
+ _CONDUCT_CFG = Path.home() / ".conduct" / "config.json"
31
+
32
+
33
+ def _active_workspace_id() -> str | None:
34
+ try:
35
+ if _CONDUCT_CFG.exists():
36
+ return json.loads(_CONDUCT_CFG.read_text()).get("workspace")
37
+ except Exception:
38
+ pass
39
+ return None
40
+
41
+
42
+ def _active_policy_path() -> Path:
43
+ ws_id = _active_workspace_id()
44
+ if ws_id:
45
+ p = GUARD_DIR / f"policy_{ws_id}.json"
46
+ if p.exists():
47
+ return p
48
+ return POLICY_PATH
30
49
 
31
50
  PROTOCOL_VERSION = "2024-11-05"
32
51
 
@@ -122,20 +141,26 @@ _TOOLS = [
122
141
  # ── Helpers ───────────────────────────────────────────────────────────────────
123
142
 
124
143
  def _load_policy() -> dict:
125
- if POLICY_PATH.exists():
144
+ p = _active_policy_path()
145
+ if p.exists():
126
146
  try:
127
- return json.loads(POLICY_PATH.read_text())
147
+ return json.loads(p.read_text())
128
148
  except Exception:
129
149
  pass
130
150
  return {"rules": []}
131
151
 
132
152
 
133
153
  def _load_config() -> dict:
134
- if CONFIG_PATH.exists():
135
- try:
154
+ try:
155
+ ws_id = _active_workspace_id()
156
+ if ws_id:
157
+ p = GUARD_DIR / "workspaces" / f"{ws_id}.json"
158
+ if p.exists():
159
+ return json.loads(p.read_text())
160
+ if CONFIG_PATH.exists():
136
161
  return json.loads(CONFIG_PATH.read_text())
137
- except Exception:
138
- pass
162
+ except Exception:
163
+ pass
139
164
  return {}
140
165
 
141
166
 
@@ -160,7 +185,7 @@ def _maybe_sync() -> None:
160
185
  ws_id = cfg.get("workspace_id", "")
161
186
  if ws_id:
162
187
  with urllib.request.urlopen(f"{_DAEMON_URL}/policy?workspace_id={ws_id}", timeout=1) as r:
163
- POLICY_PATH.write_text(r.read().decode())
188
+ _active_policy_path().write_text(r.read().decode())
164
189
  return
165
190
  except Exception:
166
191
  pass # fall through to CLI sync
@@ -337,7 +362,7 @@ def _handle_guard_sync(workspace_id: str, token: str) -> str:
337
362
  with urllib.request.urlopen(req, timeout=10) as resp:
338
363
  policy = json.loads(resp.read())
339
364
  GUARD_DIR.mkdir(parents=True, exist_ok=True)
340
- POLICY_PATH.write_text(json.dumps(policy, indent=2))
365
+ _active_policy_path().write_text(json.dumps(policy, indent=2))
341
366
  rule_count = len(policy.get("rules", []))
342
367
  return f"Policy synced — {rule_count} rule(s) active (version: {policy.get('version', 'unknown')})."
343
368
  except urllib.error.HTTPError as e:
@@ -17,8 +17,9 @@ from typing import Literal, Optional
17
17
  # ── Canonical paths ───────────────────────────────────────────────────────────
18
18
 
19
19
  GUARD_DIR = Path.home() / ".conductguard"
20
- CONFIG_PATH = GUARD_DIR / "config.json"
21
- POLICY_PATH = GUARD_DIR / "policy.json"
20
+ CONFIG_PATH = GUARD_DIR / "config.json" # legacy / fallback
21
+ POLICY_PATH = GUARD_DIR / "policy.json" # legacy / fallback
22
+ _CONDUCT_CFG = Path.home() / ".conduct" / "config.json"
22
23
  BUDGET_CACHE_PATH = GUARD_DIR / "budget_cache.json"
23
24
  BUDGET_CACHE_TTL = 300 # seconds
24
25
  VERSION_CACHE_PATH = GUARD_DIR / "version_cache.json"
@@ -42,9 +43,23 @@ class HookResult:
42
43
 
43
44
  # ── Config loading ────────────────────────────────────────────────────────────
44
45
 
46
+ def _active_workspace_id() -> str | None:
47
+ try:
48
+ if _CONDUCT_CFG.exists():
49
+ return json.loads(_CONDUCT_CFG.read_text()).get("workspace")
50
+ except Exception:
51
+ pass
52
+ return None
53
+
54
+
45
55
  def load_config() -> dict:
46
- """Return ~/.conductguard/config.json as a dict; empty dict on any error."""
56
+ """Load Guard config for the active workspace, falling back to legacy single config."""
47
57
  try:
58
+ ws_id = _active_workspace_id()
59
+ if ws_id:
60
+ p = GUARD_DIR / "workspaces" / f"{ws_id}.json"
61
+ if p.exists():
62
+ return json.loads(p.read_text())
48
63
  if CONFIG_PATH.exists():
49
64
  return json.loads(CONFIG_PATH.read_text())
50
65
  except Exception:
@@ -52,6 +67,16 @@ def load_config() -> dict:
52
67
  return {}
53
68
 
54
69
 
70
+ def active_policy_path() -> Path:
71
+ """Return per-workspace policy path, or legacy fallback."""
72
+ ws_id = _active_workspace_id()
73
+ if ws_id:
74
+ p = GUARD_DIR / f"policy_{ws_id}.json"
75
+ if p.exists():
76
+ return p
77
+ return POLICY_PATH
78
+
79
+
55
80
  # ── Repo / tool detection ─────────────────────────────────────────────────────
56
81
 
57
82
  def detect_repo() -> Optional[str]:
@@ -18,6 +18,7 @@ from conduct_cli.hooks.base import (
18
18
  JOURNAL_DIR,
19
19
  JOURNAL_PID_PATH,
20
20
  POLICY_PATH,
21
+ active_policy_path,
21
22
  SIGNING_KEY_PATH,
22
23
  VERSION_CACHE_PATH,
23
24
  VERSION_CACHE_TTL,
@@ -152,18 +153,20 @@ def _post_signature_invalid_event(expected_sig, computed_sig, policy_version, ho
152
153
  def _maybe_sync_policy() -> None:
153
154
  """Sync policy from daemon (instant) or remote API (once per minute). Never raises."""
154
155
  try:
155
- cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
156
+ cfg = load_config()
156
157
  workspace_id = cfg.get("workspace_id")
157
158
  api_key = cfg.get("api_key", "")
158
159
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
159
160
  if not workspace_id:
160
161
  return
161
162
 
163
+ pol_path = active_policy_path()
164
+
162
165
  if _daemon_alive():
163
166
  url = f"{_DAEMON_URL}/policy?workspace_id={workspace_id}"
164
167
  with urllib.request.urlopen(url, timeout=1) as resp:
165
168
  remote = json.loads(resp.read())
166
- POLICY_PATH.write_text(json.dumps(remote, indent=2))
169
+ pol_path.write_text(json.dumps(remote, indent=2))
167
170
  return
168
171
 
169
172
  if VERSION_CACHE_PATH.exists():
@@ -188,8 +191,8 @@ def _maybe_sync_policy() -> None:
188
191
  remote_version = remote.get("version", "")
189
192
  local_version = ""
190
193
  local_policy: dict = {}
191
- if POLICY_PATH.exists():
192
- local_policy = json.loads(POLICY_PATH.read_text())
194
+ if pol_path.exists():
195
+ local_policy = json.loads(pol_path.read_text())
193
196
  local_version = local_policy.get("version", "")
194
197
 
195
198
  old_mode = local_policy.get("fail_mode", "fail_open")
@@ -207,7 +210,7 @@ def _maybe_sync_policy() -> None:
207
210
  return
208
211
 
209
212
  if remote_version != local_version:
210
- POLICY_PATH.write_text(json.dumps(remote, indent=2))
213
+ pol_path.write_text(json.dumps(remote, indent=2))
211
214
  VERSION_CACHE_PATH.write_text(json.dumps({"ts": time.time(), "version": remote_version}))
212
215
  except Exception:
213
216
  pass
@@ -292,10 +295,11 @@ def _bash_operator_signature(command: str) -> str:
292
295
 
293
296
  def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
294
297
  """Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
295
- if not POLICY_PATH.exists():
298
+ pol_path = active_policy_path()
299
+ if not pol_path.exists():
296
300
  return None, "allow", None, None
297
301
  try:
298
- policy = json.loads(POLICY_PATH.read_text())
302
+ policy = json.loads(pol_path.read_text())
299
303
  except Exception:
300
304
  return None, "allow", None, None
301
305
 
@@ -409,7 +413,7 @@ def main() -> None:
409
413
  _this_file = Path(__file__).resolve()
410
414
 
411
415
  # Fail-closed gate
412
- if _get_fail_mode() == "fail_closed" and not POLICY_PATH.exists():
416
+ if _get_fail_mode() == "fail_closed" and not active_policy_path().exists():
413
417
  tool_name = (data.get("tool_name") or "").lower()
414
418
  tool_input = data.get("tool_input") or {}
415
419
  session_id = data.get("session_id")
@@ -2736,19 +2736,17 @@ def cmd_test_guard(args):
2736
2736
  """Test each guard policy rule with a matching synthetic tool call."""
2737
2737
  import json as _json
2738
2738
  import re as _re
2739
- from conduct_cli.guard import CONFIG_PATH, POLICY_PATH
2739
+ from conduct_cli.guard import _load_guard_config, _load_policy, active_policy_path
2740
2740
 
2741
- try:
2742
- cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2743
- except Exception:
2744
- cfg = {}
2741
+ cfg = _load_guard_config()
2742
+ pol_path = active_policy_path() # workspace-scoped
2745
2743
 
2746
- if not POLICY_PATH.exists():
2744
+ if not pol_path.exists():
2747
2745
  print(f"{RED}No policy file found. Run: conduct guard sync{RESET}")
2748
2746
  sys.exit(1)
2749
2747
 
2750
2748
  try:
2751
- policy = _json.loads(POLICY_PATH.read_text())
2749
+ policy = _load_policy()
2752
2750
  except Exception as e:
2753
2751
  print(f"{RED}Could not load policy: {e}{RESET}")
2754
2752
  sys.exit(1)
@@ -97,10 +97,24 @@ def _load_conduct_config() -> dict:
97
97
  return {}
98
98
 
99
99
 
100
+ def _active_guard_policy_path() -> Path:
101
+ try:
102
+ cfg = json.loads(CONDUCT_CONFIG_PATH.read_text()) if CONDUCT_CONFIG_PATH.exists() else {}
103
+ ws_id = cfg.get("workspace")
104
+ if ws_id:
105
+ p = GUARD_DIR / f"policy_{ws_id}.json"
106
+ if p.exists():
107
+ return p
108
+ except Exception:
109
+ pass
110
+ return GUARD_POLICY_PATH
111
+
112
+
100
113
  def _load_guard_policy() -> dict:
101
- if GUARD_POLICY_PATH.exists():
114
+ p = _active_guard_policy_path()
115
+ if p.exists():
102
116
  try:
103
- return json.loads(GUARD_POLICY_PATH.read_text())
117
+ return json.loads(p.read_text())
104
118
  except Exception:
105
119
  pass
106
120
  return {"rules": []}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.6.33
3
+ Version: 0.6.34
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
File without changes
File without changes
File without changes