conduct-cli 0.6.34__tar.gz → 0.6.36__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.6.34 → conduct_cli-0.6.36}/PKG-INFO +1 -1
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/pyproject.toml +1 -1
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/guard.py +7 -49
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/guardmcp.py +11 -36
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/base.py +4 -24
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/main.py +1 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/mcp_server.py +2 -16
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/README.md +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/setup.cfg +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/setup.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_log_util.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.6.34 → conduct_cli-0.6.36}/tests/test_switch.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.36"
|
|
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" }
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
"""conduct guard — team policy + MCP registration subcommand."""
|
|
2
3
|
|
|
3
4
|
import json
|
|
@@ -18,35 +19,11 @@ CYAN = "\033[36m"
|
|
|
18
19
|
YELLOW = "\033[33m"
|
|
19
20
|
|
|
20
21
|
GUARD_DIR = Path.home() / ".conductguard"
|
|
21
|
-
CONFIG_PATH = GUARD_DIR / "config.json"
|
|
22
|
-
POLICY_PATH = GUARD_DIR / "policy.json"
|
|
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"
|
|
22
|
+
CONFIG_PATH = GUARD_DIR / "config.json"
|
|
23
|
+
POLICY_PATH = GUARD_DIR / "policy.json"
|
|
42
24
|
|
|
43
25
|
|
|
44
26
|
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
27
|
return POLICY_PATH
|
|
51
28
|
|
|
52
29
|
# ── Hook templates — loaded from real .py files (no string embedding) ─────────
|
|
@@ -331,26 +308,16 @@ def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
|
|
|
331
308
|
|
|
332
309
|
|
|
333
310
|
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
|
|
340
311
|
if CONFIG_PATH.exists():
|
|
341
|
-
|
|
312
|
+
try:
|
|
313
|
+
return json.loads(CONFIG_PATH.read_text())
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
342
316
|
return {}
|
|
343
317
|
|
|
344
318
|
|
|
345
319
|
def _save_guard_config(data: dict, workspace_id: str | None = None):
|
|
346
320
|
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
|
|
354
321
|
CONFIG_PATH.write_text(json.dumps(data, indent=2))
|
|
355
322
|
CONFIG_PATH.chmod(0o600)
|
|
356
323
|
|
|
@@ -612,20 +579,11 @@ def _req(method: str, url: str, body=None, token: str = None, api_key: str = Non
|
|
|
612
579
|
|
|
613
580
|
def _save_policy(policy: dict, workspace_id: str | None = None):
|
|
614
581
|
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
|
|
619
582
|
POLICY_PATH.write_text(json.dumps(policy, indent=2))
|
|
620
583
|
|
|
621
584
|
|
|
622
585
|
def _load_policy(workspace_id: str | None = None) -> dict:
|
|
623
586
|
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())
|
|
629
587
|
return json.loads(POLICY_PATH.read_text())
|
|
630
588
|
except Exception:
|
|
631
589
|
return {"rules": []}
|
|
@@ -24,28 +24,9 @@ import urllib.request
|
|
|
24
24
|
import urllib.error
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
-
GUARD_DIR
|
|
28
|
-
POLICY_PATH
|
|
29
|
-
CONFIG_PATH
|
|
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
|
|
27
|
+
GUARD_DIR = Path.home() / ".conductguard"
|
|
28
|
+
POLICY_PATH = GUARD_DIR / "policy.json"
|
|
29
|
+
CONFIG_PATH = GUARD_DIR / "config.json"
|
|
49
30
|
|
|
50
31
|
PROTOCOL_VERSION = "2024-11-05"
|
|
51
32
|
|
|
@@ -141,26 +122,20 @@ _TOOLS = [
|
|
|
141
122
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
142
123
|
|
|
143
124
|
def _load_policy() -> dict:
|
|
144
|
-
|
|
145
|
-
if p.exists():
|
|
125
|
+
if POLICY_PATH.exists():
|
|
146
126
|
try:
|
|
147
|
-
return json.loads(
|
|
127
|
+
return json.loads(POLICY_PATH.read_text())
|
|
148
128
|
except Exception:
|
|
149
129
|
pass
|
|
150
130
|
return {"rules": []}
|
|
151
131
|
|
|
152
132
|
|
|
153
133
|
def _load_config() -> dict:
|
|
154
|
-
|
|
155
|
-
|
|
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():
|
|
134
|
+
if CONFIG_PATH.exists():
|
|
135
|
+
try:
|
|
161
136
|
return json.loads(CONFIG_PATH.read_text())
|
|
162
|
-
|
|
163
|
-
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
164
139
|
return {}
|
|
165
140
|
|
|
166
141
|
|
|
@@ -185,7 +160,7 @@ def _maybe_sync() -> None:
|
|
|
185
160
|
ws_id = cfg.get("workspace_id", "")
|
|
186
161
|
if ws_id:
|
|
187
162
|
with urllib.request.urlopen(f"{_DAEMON_URL}/policy?workspace_id={ws_id}", timeout=1) as r:
|
|
188
|
-
|
|
163
|
+
POLICY_PATH.write_text(r.read().decode())
|
|
189
164
|
return
|
|
190
165
|
except Exception:
|
|
191
166
|
pass # fall through to CLI sync
|
|
@@ -362,7 +337,7 @@ def _handle_guard_sync(workspace_id: str, token: str) -> str:
|
|
|
362
337
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
363
338
|
policy = json.loads(resp.read())
|
|
364
339
|
GUARD_DIR.mkdir(parents=True, exist_ok=True)
|
|
365
|
-
|
|
340
|
+
POLICY_PATH.write_text(json.dumps(policy, indent=2))
|
|
366
341
|
rule_count = len(policy.get("rules", []))
|
|
367
342
|
return f"Policy synced — {rule_count} rule(s) active (version: {policy.get('version', 'unknown')})."
|
|
368
343
|
except urllib.error.HTTPError as e:
|
|
@@ -17,9 +17,8 @@ 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"
|
|
22
|
-
_CONDUCT_CFG = Path.home() / ".conduct" / "config.json"
|
|
20
|
+
CONFIG_PATH = GUARD_DIR / "config.json"
|
|
21
|
+
POLICY_PATH = GUARD_DIR / "policy.json"
|
|
23
22
|
BUDGET_CACHE_PATH = GUARD_DIR / "budget_cache.json"
|
|
24
23
|
BUDGET_CACHE_TTL = 300 # seconds
|
|
25
24
|
VERSION_CACHE_PATH = GUARD_DIR / "version_cache.json"
|
|
@@ -43,23 +42,9 @@ class HookResult:
|
|
|
43
42
|
|
|
44
43
|
# ── Config loading ────────────────────────────────────────────────────────────
|
|
45
44
|
|
|
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
|
-
|
|
55
45
|
def load_config() -> dict:
|
|
56
|
-
"""Load Guard config
|
|
46
|
+
"""Load Guard config from ~/.conductguard/config.json."""
|
|
57
47
|
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())
|
|
63
48
|
if CONFIG_PATH.exists():
|
|
64
49
|
return json.loads(CONFIG_PATH.read_text())
|
|
65
50
|
except Exception:
|
|
@@ -68,12 +53,7 @@ def load_config() -> dict:
|
|
|
68
53
|
|
|
69
54
|
|
|
70
55
|
def active_policy_path() -> Path:
|
|
71
|
-
"""Return
|
|
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
|
|
56
|
+
"""Return policy path: ~/.conductguard/policy.json."""
|
|
77
57
|
return POLICY_PATH
|
|
78
58
|
|
|
79
59
|
|
|
@@ -97,24 +97,10 @@ 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
|
-
|
|
113
100
|
def _load_guard_policy() -> dict:
|
|
114
|
-
|
|
115
|
-
if p.exists():
|
|
101
|
+
if GUARD_POLICY_PATH.exists():
|
|
116
102
|
try:
|
|
117
|
-
return json.loads(
|
|
103
|
+
return json.loads(GUARD_POLICY_PATH.read_text())
|
|
118
104
|
except Exception:
|
|
119
105
|
pass
|
|
120
106
|
return {"rules": []}
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|