conduct-cli 0.6.28__tar.gz → 0.6.30__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.28 → conduct_cli-0.6.30}/PKG-INFO +3 -2
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/pyproject.toml +3 -3
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/guard.py +294 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/PKG-INFO +3 -2
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/requires.txt +1 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/README.md +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/setup.cfg +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/setup.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_log_util.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.6.28 → conduct_cli-0.6.30}/tests/test_switch.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.6.
|
|
4
|
-
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
3
|
+
Version: 0.6.30
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://conductai.ai
|
|
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
24
|
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: psutil>=5.9
|
|
25
26
|
Provides-Extra: booster
|
|
26
27
|
Requires-Dist: agent-booster[watch]>=0.6.6; extra == "booster"
|
|
27
28
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.6.
|
|
8
|
-
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
7
|
+
version = "0.6.30"
|
|
8
|
+
description = "CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
24
|
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
25
25
|
]
|
|
26
|
-
dependencies = ["pyyaml>=6.0", "rich>=13.0"]
|
|
26
|
+
dependencies = ["pyyaml>=6.0", "rich>=13.0", "psutil>=5.9"]
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
29
|
booster = ["agent-booster[watch]>=0.6.6"]
|
|
@@ -1203,6 +1203,18 @@ def cmd_guard_sync(args):
|
|
|
1203
1203
|
except Exception:
|
|
1204
1204
|
pass
|
|
1205
1205
|
|
|
1206
|
+
# Run shadow agent discovery in background — results pushed to API, visible in /guard/discovery
|
|
1207
|
+
try:
|
|
1208
|
+
import subprocess as _sp, shutil as _shutil
|
|
1209
|
+
_conduct = _shutil.which("conduct") or sys.executable
|
|
1210
|
+
_sp.Popen(
|
|
1211
|
+
[_conduct, "guard", "discover"],
|
|
1212
|
+
stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,
|
|
1213
|
+
)
|
|
1214
|
+
print(f" {CYAN}Discovery daemon started{RESET} — agent inventory updating in background")
|
|
1215
|
+
except Exception:
|
|
1216
|
+
pass
|
|
1217
|
+
|
|
1206
1218
|
print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
|
|
1207
1219
|
|
|
1208
1220
|
# Print Claude.ai remote MCP URL — requires one-time browser paste
|
|
@@ -1867,9 +1879,287 @@ def register_guard_parser(sub):
|
|
|
1867
1879
|
help="Which hook to test",
|
|
1868
1880
|
)
|
|
1869
1881
|
|
|
1882
|
+
# conduct guard discover
|
|
1883
|
+
discover_p = guard_sub.add_parser("discover", help="Scan for AI agents and show Guard coverage")
|
|
1884
|
+
discover_p.add_argument("--config-only", action="store_true", help="Skip process scan, config files only")
|
|
1885
|
+
discover_p.add_argument("--report", default=None, metavar="FILE", help="Write full JSON report to file")
|
|
1886
|
+
|
|
1887
|
+
# conduct guard lint [--file PATH]
|
|
1888
|
+
lint_p = guard_sub.add_parser("lint", help="Validate local policy — show errors and warnings")
|
|
1889
|
+
lint_p.add_argument("--file", default=None, metavar="FILE",
|
|
1890
|
+
help="Path to policy YAML/JSON (default: ~/.conductguard/policy.json)")
|
|
1891
|
+
|
|
1870
1892
|
return guard_p, guard_sub
|
|
1871
1893
|
|
|
1872
1894
|
|
|
1895
|
+
def _discover_config_agents() -> list[tuple]:
|
|
1896
|
+
"""Detect installed AI tools from config dirs + .env files. No cross-module import.
|
|
1897
|
+
Returns list of (name, config_path, under_guard) tuples.
|
|
1898
|
+
"""
|
|
1899
|
+
home = Path.home()
|
|
1900
|
+
results = []
|
|
1901
|
+
|
|
1902
|
+
def _mcp_registered(path: Path) -> bool:
|
|
1903
|
+
try:
|
|
1904
|
+
import json as _j
|
|
1905
|
+
d = _j.loads(path.read_text()) if path.exists() else {}
|
|
1906
|
+
return "conduct" in d.get("mcpServers", {})
|
|
1907
|
+
except Exception:
|
|
1908
|
+
return False
|
|
1909
|
+
|
|
1910
|
+
def _hook_registered(path: Path) -> bool:
|
|
1911
|
+
try:
|
|
1912
|
+
import json as _j
|
|
1913
|
+
d = _j.loads(path.read_text()) if path.exists() else {}
|
|
1914
|
+
hooks = d.get("hooks", {})
|
|
1915
|
+
return any("conductguard" in str(h).lower() or "conduct" in str(h).lower()
|
|
1916
|
+
for h in hooks.get("PreToolUse", []))
|
|
1917
|
+
except Exception:
|
|
1918
|
+
return False
|
|
1919
|
+
|
|
1920
|
+
# Known tool config locations
|
|
1921
|
+
TOOLS = [
|
|
1922
|
+
("claude-code", home / ".claude", home / ".claude" / "settings.json", "mcp"),
|
|
1923
|
+
("codex", home / ".codex", home / ".codex" / "mcp.json", "mcp"),
|
|
1924
|
+
("cursor", home / ".cursor", home / ".cursor" / "mcp.json", "mcp"),
|
|
1925
|
+
("windsurf", home / ".codeium" / "windsurf", home / ".codeium" / "windsurf" / "mcp_config.json", "mcp"),
|
|
1926
|
+
]
|
|
1927
|
+
for name, check_dir, config_path, _ in TOOLS:
|
|
1928
|
+
if check_dir.exists():
|
|
1929
|
+
under = _mcp_registered(config_path) or _hook_registered(config_path)
|
|
1930
|
+
results.append((name, config_path, under))
|
|
1931
|
+
|
|
1932
|
+
# .env file scan for LLM API keys — shadow agents not using known tools
|
|
1933
|
+
LLM_KEYS = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "COHERE_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY"]
|
|
1934
|
+
search_paths = [home, home / "projects", home / "code", home / "dev", Path.cwd()]
|
|
1935
|
+
seen_envs: set[str] = set()
|
|
1936
|
+
for base in search_paths:
|
|
1937
|
+
if not base.exists():
|
|
1938
|
+
continue
|
|
1939
|
+
for env_file in list(base.glob(".env")) + list(base.glob("**/.env")):
|
|
1940
|
+
if str(env_file) in seen_envs or ".git" in str(env_file):
|
|
1941
|
+
continue
|
|
1942
|
+
seen_envs.add(str(env_file))
|
|
1943
|
+
try:
|
|
1944
|
+
content = env_file.read_text(errors="ignore")
|
|
1945
|
+
found_keys = [k for k in LLM_KEYS if k in content]
|
|
1946
|
+
if found_keys:
|
|
1947
|
+
results.append(("env-agent", env_file, False)) # .env with LLM key = unregistered
|
|
1948
|
+
except Exception:
|
|
1949
|
+
pass
|
|
1950
|
+
|
|
1951
|
+
return results
|
|
1952
|
+
|
|
1953
|
+
|
|
1954
|
+
def _scan_processes() -> list[dict]:
|
|
1955
|
+
"""Detect AI agent processes using psutil. Returns list of discovered agent dicts."""
|
|
1956
|
+
try:
|
|
1957
|
+
import psutil
|
|
1958
|
+
except ImportError:
|
|
1959
|
+
return []
|
|
1960
|
+
|
|
1961
|
+
# Known framework signatures: (framework_name, cmdline_patterns)
|
|
1962
|
+
SIGNATURES = [
|
|
1963
|
+
("langchain", ["langchain"]),
|
|
1964
|
+
("crewai", ["crewai", "crew_ai"]),
|
|
1965
|
+
("autogen", ["autogen", "pyautogen"]),
|
|
1966
|
+
("openai-agents", ["openai-agents", "openai_agents"]),
|
|
1967
|
+
("llama-index", ["llama_index", "llamaindex"]),
|
|
1968
|
+
("claude-code", ["claude"]),
|
|
1969
|
+
("codex", ["codex"]),
|
|
1970
|
+
("cursor", ["cursor"]),
|
|
1971
|
+
("windsurf", ["windsurf"]),
|
|
1972
|
+
("copilot", ["copilot-language-server", "github.copilot"]),
|
|
1973
|
+
]
|
|
1974
|
+
|
|
1975
|
+
found = []
|
|
1976
|
+
seen = set()
|
|
1977
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
|
|
1978
|
+
try:
|
|
1979
|
+
cmdline = " ".join(proc.info["cmdline"] or []).lower()
|
|
1980
|
+
name = (proc.info["name"] or "").lower()
|
|
1981
|
+
combined = f"{name} {cmdline}"
|
|
1982
|
+
for framework, patterns in SIGNATURES:
|
|
1983
|
+
if any(p in combined for p in patterns):
|
|
1984
|
+
key = (framework, proc.info["name"])
|
|
1985
|
+
if key not in seen:
|
|
1986
|
+
seen.add(key)
|
|
1987
|
+
found.append({
|
|
1988
|
+
"name": proc.info["name"],
|
|
1989
|
+
"framework": framework,
|
|
1990
|
+
"source": "process",
|
|
1991
|
+
"location": f"pid:{proc.info['pid']} {proc.info['name']}",
|
|
1992
|
+
"evidence": {"pid": proc.info["pid"], "cmdline": cmdline[:200]},
|
|
1993
|
+
"risk_score": 60,
|
|
1994
|
+
})
|
|
1995
|
+
break
|
|
1996
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
1997
|
+
pass
|
|
1998
|
+
return found
|
|
1999
|
+
|
|
2000
|
+
|
|
2001
|
+
def cmd_guard_lint(args):
|
|
2002
|
+
"""Validate the local policy file and report errors / warnings."""
|
|
2003
|
+
import json as _json, re as _re
|
|
2004
|
+
|
|
2005
|
+
policy_path = getattr(args, "file", None)
|
|
2006
|
+
if policy_path:
|
|
2007
|
+
from pathlib import Path as _Path
|
|
2008
|
+
p = _Path(policy_path).expanduser().resolve()
|
|
2009
|
+
if not p.exists():
|
|
2010
|
+
print(f" {RED}File not found: {p}{RESET}")
|
|
2011
|
+
sys.exit(1)
|
|
2012
|
+
raw = p.read_text()
|
|
2013
|
+
else:
|
|
2014
|
+
policy = _load_policy()
|
|
2015
|
+
raw = None
|
|
2016
|
+
|
|
2017
|
+
if raw is not None:
|
|
2018
|
+
try:
|
|
2019
|
+
policy = _json.loads(raw)
|
|
2020
|
+
except Exception:
|
|
2021
|
+
try:
|
|
2022
|
+
import yaml as _yaml
|
|
2023
|
+
policy = _yaml.safe_load(raw)
|
|
2024
|
+
except Exception as e:
|
|
2025
|
+
print(f" {RED}Cannot parse file: {e}{RESET}")
|
|
2026
|
+
sys.exit(1)
|
|
2027
|
+
|
|
2028
|
+
rules = policy.get("rules", [])
|
|
2029
|
+
fail_mode = policy.get("fail_mode")
|
|
2030
|
+
|
|
2031
|
+
# ── local lint (no API call needed) ──────────────────────────────────────
|
|
2032
|
+
VALID_ACTIONS = {"block", "warn", "audit", "approval", "inject"}
|
|
2033
|
+
VALID_FAIL_MODES = {"fail_open", "fail_closed"}
|
|
2034
|
+
MATCH_FIELDS = {"match_tool", "match_pattern", "match_path_pattern",
|
|
2035
|
+
"match_command_word", "match_tokens_before_gt"}
|
|
2036
|
+
|
|
2037
|
+
errors: list[dict] = []
|
|
2038
|
+
warnings: list[dict] = []
|
|
2039
|
+
seen_ids: set[str] = set()
|
|
2040
|
+
|
|
2041
|
+
if fail_mode and fail_mode not in VALID_FAIL_MODES:
|
|
2042
|
+
errors.append({"rule_id": "(policy)", "field": "fail_mode",
|
|
2043
|
+
"message": f"must be one of: {', '.join(sorted(VALID_FAIL_MODES))}"})
|
|
2044
|
+
|
|
2045
|
+
for i, rule in enumerate(rules):
|
|
2046
|
+
rid = rule.get("rule_id") or rule.get("id") or f"(rule[{i}])"
|
|
2047
|
+
if not rule.get("rule_id") and not rule.get("id"):
|
|
2048
|
+
errors.append({"rule_id": rid, "field": "rule_id", "message": "rule_id is required"})
|
|
2049
|
+
if rid in seen_ids:
|
|
2050
|
+
errors.append({"rule_id": rid, "field": "rule_id", "message": f"Duplicate rule_id '{rid}'"})
|
|
2051
|
+
seen_ids.add(rid)
|
|
2052
|
+
|
|
2053
|
+
action = rule.get("action")
|
|
2054
|
+
if not action:
|
|
2055
|
+
errors.append({"rule_id": rid, "field": "action", "message": "action is required"})
|
|
2056
|
+
elif action not in VALID_ACTIONS:
|
|
2057
|
+
errors.append({"rule_id": rid, "field": "action",
|
|
2058
|
+
"message": f"'{action}' must be one of: {', '.join(sorted(VALID_ACTIONS))}"})
|
|
2059
|
+
|
|
2060
|
+
for rf in ("match_pattern", "match_path_pattern"):
|
|
2061
|
+
val = rule.get(rf)
|
|
2062
|
+
if val:
|
|
2063
|
+
try:
|
|
2064
|
+
_re.compile(val)
|
|
2065
|
+
except _re.error as e:
|
|
2066
|
+
errors.append({"rule_id": rid, "field": rf, "message": f"Invalid regex: {e}"})
|
|
2067
|
+
|
|
2068
|
+
if not any(rule.get(f) for f in MATCH_FIELDS):
|
|
2069
|
+
warnings.append({"rule_id": rid, "field": "match_*",
|
|
2070
|
+
"message": "No match condition — rule fires on every tool call"})
|
|
2071
|
+
|
|
2072
|
+
tokens = rule.get("match_tokens_before_gt")
|
|
2073
|
+
if tokens is not None and (not isinstance(tokens, int) or tokens <= 0):
|
|
2074
|
+
errors.append({"rule_id": rid, "field": "match_tokens_before_gt",
|
|
2075
|
+
"message": "must be a positive integer"})
|
|
2076
|
+
|
|
2077
|
+
# ── output ────────────────────────────────────────────────────────────────
|
|
2078
|
+
print(f"\n {BOLD}Policy lint{RESET} — {len(rules)} rule(s)\n")
|
|
2079
|
+
|
|
2080
|
+
if not errors and not warnings:
|
|
2081
|
+
print(f" {GREEN}No issues found{RESET}\n")
|
|
2082
|
+
return
|
|
2083
|
+
|
|
2084
|
+
for e in errors:
|
|
2085
|
+
print(f" {RED}ERROR{RESET} [{e['rule_id']}] {e['field']}: {e['message']}")
|
|
2086
|
+
for w in warnings:
|
|
2087
|
+
print(f" {YELLOW}WARN{RESET} [{w['rule_id']}] {w['field']}: {w['message']}")
|
|
2088
|
+
|
|
2089
|
+
print()
|
|
2090
|
+
if errors:
|
|
2091
|
+
sys.exit(1)
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
def cmd_guard_discover(args):
|
|
2095
|
+
"""Scan for AI agents across config files and processes, report Guard coverage."""
|
|
2096
|
+
import json as _json
|
|
2097
|
+
|
|
2098
|
+
cfg = _load_guard_config()
|
|
2099
|
+
api_url = _api_url(cfg)
|
|
2100
|
+
token = cfg.get("member_token", "")
|
|
2101
|
+
hdrs = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
2102
|
+
|
|
2103
|
+
# Config file scan — detect AI tools from known config dirs
|
|
2104
|
+
config_agents = []
|
|
2105
|
+
for name, config_path, mcp_key in _discover_config_agents():
|
|
2106
|
+
under = mcp_key
|
|
2107
|
+
config_agents.append({
|
|
2108
|
+
"name": name,
|
|
2109
|
+
"framework": name,
|
|
2110
|
+
"source": "config",
|
|
2111
|
+
"location": str(config_path),
|
|
2112
|
+
"evidence": {"config_path": str(config_path), "under_guard": under},
|
|
2113
|
+
"risk_score": 20 if under else 70,
|
|
2114
|
+
"under_guard": under,
|
|
2115
|
+
})
|
|
2116
|
+
|
|
2117
|
+
# Process scan
|
|
2118
|
+
process_agents: list[dict] = []
|
|
2119
|
+
if not getattr(args, "config_only", False):
|
|
2120
|
+
process_agents = _scan_processes()
|
|
2121
|
+
# Mark as under Guard if same framework is already registered
|
|
2122
|
+
registered_frameworks = {a["framework"] for a in config_agents if a["under_guard"]}
|
|
2123
|
+
for a in process_agents:
|
|
2124
|
+
a["under_guard"] = a["framework"] in registered_frameworks
|
|
2125
|
+
|
|
2126
|
+
all_agents = config_agents + process_agents
|
|
2127
|
+
total = len(all_agents)
|
|
2128
|
+
covered = sum(1 for a in all_agents if a["under_guard"])
|
|
2129
|
+
missing = total - covered
|
|
2130
|
+
pct = round(covered / total * 100) if total else 0
|
|
2131
|
+
|
|
2132
|
+
# Print coverage meter
|
|
2133
|
+
print(f"\n Discovered {total} AI agents across your environment\n")
|
|
2134
|
+
config_count = len(config_agents)
|
|
2135
|
+
process_count = len(process_agents)
|
|
2136
|
+
config_cov = sum(1 for a in config_agents if a["under_guard"])
|
|
2137
|
+
process_cov = sum(1 for a in process_agents if a["under_guard"])
|
|
2138
|
+
if config_count:
|
|
2139
|
+
print(f" Config files: {config_count:3d} agents ({config_cov} under Guard, {config_count - config_cov} not)")
|
|
2140
|
+
if process_count:
|
|
2141
|
+
print(f" Processes: {process_count:3d} running ({process_cov} under Guard, {process_count - process_cov} not)")
|
|
2142
|
+
print(f" {'─' * 52}")
|
|
2143
|
+
print(f" Guard coverage: {covered} of {total} agents ({pct}%)\n")
|
|
2144
|
+
if missing:
|
|
2145
|
+
print(f" Run: conduct guard discover --register to close the gap")
|
|
2146
|
+
print(f" GitHub scan available — coming in v2\n")
|
|
2147
|
+
|
|
2148
|
+
# POST to API
|
|
2149
|
+
try:
|
|
2150
|
+
payload = {"triggered_by": "cli", "agents": all_agents}
|
|
2151
|
+
resp = _req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token)
|
|
2152
|
+
except Exception:
|
|
2153
|
+
pass # local output still useful even if API is down
|
|
2154
|
+
|
|
2155
|
+
# Write report if requested
|
|
2156
|
+
report_path = getattr(args, "report", None)
|
|
2157
|
+
if report_path:
|
|
2158
|
+
report = {"total": total, "under_guard": covered, "missing": missing, "coverage_pct": pct, "agents": all_agents}
|
|
2159
|
+
Path(report_path).write_text(_json.dumps(report, indent=2))
|
|
2160
|
+
print(f" Report written to {report_path}")
|
|
2161
|
+
|
|
2162
|
+
|
|
1873
2163
|
def cmd_guard_booster_status(args):
|
|
1874
2164
|
"""Show whether booster is intercepting Read/Grep in this project."""
|
|
1875
2165
|
import shutil, sqlite3, subprocess
|
|
@@ -2023,6 +2313,10 @@ def dispatch_guard(args, guard_p):
|
|
|
2023
2313
|
cmd_guard_audit(args)
|
|
2024
2314
|
elif guard_command == "install":
|
|
2025
2315
|
cmd_guard_install(args)
|
|
2316
|
+
elif guard_command == "discover":
|
|
2317
|
+
cmd_guard_discover(args)
|
|
2318
|
+
elif guard_command == "lint":
|
|
2319
|
+
cmd_guard_lint(args)
|
|
2026
2320
|
elif guard_command == "booster-status":
|
|
2027
2321
|
cmd_guard_booster_status(args)
|
|
2028
2322
|
elif guard_command == "debug-hook":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conduct-cli
|
|
3
|
-
Version: 0.6.
|
|
4
|
-
Summary: CLI for Conduct AI — install agents, manage projects, run tests
|
|
3
|
+
Version: 0.6.30
|
|
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
|
|
7
7
|
Project-URL: Homepage, https://conductai.ai
|
|
@@ -22,6 +22,7 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: pyyaml>=6.0
|
|
24
24
|
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: psutil>=5.9
|
|
25
26
|
Provides-Extra: booster
|
|
26
27
|
Requires-Dist: agent-booster[watch]>=0.6.6; extra == "booster"
|
|
27
28
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|