conduct-cli 0.7.41__tar.gz → 0.7.43__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.7.41 → conduct_cli-0.7.43}/PKG-INFO +1 -1
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/pyproject.toml +1 -1
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/guard.py +73 -43
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/session_start.py +29 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/README.md +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/setup.cfg +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/setup.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_api_client.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_log_util.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_switch.py +0 -0
- {conduct_cli-0.7.41 → conduct_cli-0.7.43}/tests/test_token_lifecycle.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.7.
|
|
7
|
+
version = "0.7.43"
|
|
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" }
|
|
@@ -441,7 +441,7 @@ def _register_mcp(workspace_id: str, agent_token: str, api_url: str) -> None:
|
|
|
441
441
|
|
|
442
442
|
# Cursor global rules — write Guard policies as user rules so they apply across all projects.
|
|
443
443
|
_patch_cursor_global_rules()
|
|
444
|
-
|
|
444
|
+
_patch_tool_instruction_files(agent_token, api_url)
|
|
445
445
|
|
|
446
446
|
|
|
447
447
|
def _patch_claude_desktop_proxy(api_url: str, agent_token: str) -> None:
|
|
@@ -541,59 +541,81 @@ def _patch_cursor_global_rules() -> None:
|
|
|
541
541
|
print(f" {GREEN}Cursor global rules updated with ConductGuard policy{RESET}")
|
|
542
542
|
|
|
543
543
|
|
|
544
|
-
def
|
|
545
|
-
"""Write ConductGuard
|
|
544
|
+
def _patch_tool_instruction_files(agent_token: str, api_url: str) -> None:
|
|
545
|
+
"""Write ConductGuard policy block into instruction files for all AI tools.
|
|
546
546
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
547
|
+
Targets:
|
|
548
|
+
CLAUDE.md — Claude Code + Claude Desktop
|
|
549
|
+
.github/copilot-instructions.md — Copilot + VS Code
|
|
550
|
+
AGENTS.md — Codex CLI + Codex Desktop
|
|
551
|
+
.cursorrules — Cursor
|
|
552
|
+
.windsurfrules — Windsurf
|
|
553
|
+
|
|
554
|
+
Merge strategy (never clobbers existing content):
|
|
555
|
+
First sync : appends block between <!-- ConductGuard --> markers
|
|
556
|
+
Re-sync : replaces only the block, leaves surrounding content untouched
|
|
551
557
|
"""
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
import re as _re
|
|
559
|
+
|
|
560
|
+
MARKER_START = "<!-- ConductGuard — managed by conduct guard sync -->"
|
|
561
|
+
MARKER_END = "<!-- /ConductGuard -->"
|
|
562
|
+
GUARD_RULES = (
|
|
555
563
|
"Before executing any shell command, file write, network request, API call, or code change:\n"
|
|
556
564
|
"- Call `guard_check` (conductguard MCP tool) with the action and parameters.\n"
|
|
557
565
|
"- If BLOCKED: stop immediately and explain the policy rule to the user.\n"
|
|
558
|
-
"- If WARNING: proceed but surface the warning.\n"
|
|
559
|
-
"-
|
|
560
|
-
"Never
|
|
561
|
-
"Never send PII (emails, names, payment data) to external endpoints.\n"
|
|
562
|
-
"<!-- /ConductGuard -->"
|
|
566
|
+
"- If WARNING: proceed but surface the warning. If ALLOWED: proceed normally.\n"
|
|
567
|
+
"- Never write credentials, API keys, or secrets to files or output.\n"
|
|
568
|
+
"- Never send PII (emails, names, payment data) to external endpoints."
|
|
563
569
|
)
|
|
564
570
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
571
|
+
# Pull team instructions from API (non-fatal)
|
|
572
|
+
team_content = ""
|
|
573
|
+
try:
|
|
574
|
+
resp = _req("GET", f"{api_url}/team-os/instructions", token=agent_token)
|
|
575
|
+
team_content = (resp.get("content") or "").strip()
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
parts = [MARKER_START, "## ConductGuard Policy"]
|
|
580
|
+
if team_content:
|
|
581
|
+
parts.append(team_content)
|
|
582
|
+
parts.append(GUARD_RULES)
|
|
583
|
+
parts.append(MARKER_END)
|
|
584
|
+
guard_block = "\n".join(parts)
|
|
585
|
+
|
|
586
|
+
repo_root: Path | None = None
|
|
587
|
+
for parent in [Path.cwd(), *Path.cwd().parents]:
|
|
588
|
+
if (parent / ".git").exists() or (parent / ".github").is_dir():
|
|
589
|
+
repo_root = parent
|
|
572
590
|
break
|
|
573
|
-
# Global fallback
|
|
574
|
-
candidates.append(Path.home() / ".github" / "copilot-instructions.md")
|
|
575
591
|
|
|
576
|
-
|
|
577
|
-
|
|
592
|
+
# (repo path, global fallback, label)
|
|
593
|
+
targets = [
|
|
594
|
+
(repo_root / "CLAUDE.md" if repo_root else None, Path.home() / "CLAUDE.md", "CLAUDE.md"),
|
|
595
|
+
(repo_root / ".github" / "copilot-instructions.md" if repo_root else None, Path.home() / ".github" / "copilot-instructions.md", "Copilot instructions"),
|
|
596
|
+
(repo_root / "AGENTS.md" if repo_root else None, Path.home() / ".codex" / "instructions.md", "AGENTS.md"),
|
|
597
|
+
(repo_root / ".cursorrules" if repo_root else None, Path.home() / ".cursor" / "rules" / "conduct.md", ".cursorrules"),
|
|
598
|
+
(repo_root / ".windsurfrules" if repo_root else None, Path.home() / ".windsurf"/ "rules" / "conduct.md", ".windsurfrules"),
|
|
599
|
+
]
|
|
600
|
+
pattern = _re.compile(_re.escape(MARKER_START) + r".*?" + _re.escape(MARKER_END), _re.DOTALL)
|
|
601
|
+
|
|
602
|
+
for repo_path, global_path, label in targets:
|
|
603
|
+
path = repo_path if (repo_path and repo_path.exists()) else global_path
|
|
604
|
+
if not path.exists() and repo_path:
|
|
605
|
+
path = repo_path
|
|
578
606
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
579
607
|
existing = path.read_text() if path.exists() else ""
|
|
580
|
-
if
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
# No .github/ dir found anywhere — write global
|
|
592
|
-
global_path = Path.home() / ".github" / "copilot-instructions.md"
|
|
593
|
-
global_path.parent.mkdir(parents=True, exist_ok=True)
|
|
594
|
-
global_path.write_text(GUARD_BLOCK)
|
|
595
|
-
print(f" {GREEN}Copilot instructions written ({global_path}){RESET}")
|
|
596
|
-
|
|
608
|
+
if MARKER_START in existing:
|
|
609
|
+
updated = pattern.sub(guard_block, existing)
|
|
610
|
+
if updated == existing:
|
|
611
|
+
print(f" {GRAY}{label} already up to date{RESET}")
|
|
612
|
+
else:
|
|
613
|
+
path.write_text(updated)
|
|
614
|
+
print(f" {GREEN}{label} updated{RESET}")
|
|
615
|
+
else:
|
|
616
|
+
sep = "\n\n" if existing.strip() else ""
|
|
617
|
+
path.write_text((existing.rstrip() + sep + guard_block + "\n").lstrip())
|
|
618
|
+
print(f" {GREEN}{label} — ConductGuard block added{RESET}")
|
|
597
619
|
|
|
598
620
|
def _install_codex_hook(hook_path: Path) -> None:
|
|
599
621
|
"""Register PreToolUse and PostToolUse hooks in ~/.codex/hooks.json."""
|
|
@@ -1376,6 +1398,14 @@ def cmd_guard_sync(args):
|
|
|
1376
1398
|
except Exception as e:
|
|
1377
1399
|
print(f" {YELLOW}Warning: could not refresh token ({e}) — using cached value{RESET}")
|
|
1378
1400
|
|
|
1401
|
+
# Fetch team instructions version so session-start hook can detect staleness
|
|
1402
|
+
try:
|
|
1403
|
+
ver_resp = _req("GET", f"{base_url}/team-os/instructions/version", token=_token)
|
|
1404
|
+
if ver_resp.get("version"):
|
|
1405
|
+
_save_guard_config({"instructions_version": ver_resp["version"]})
|
|
1406
|
+
except Exception:
|
|
1407
|
+
pass # non-fatal
|
|
1408
|
+
|
|
1379
1409
|
# Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
|
|
1380
1410
|
# routes through Conduct Guard. Customer-overridable via --proxy-url or
|
|
1381
1411
|
# CONDUCT_PROXY_URL env var; otherwise fetched from server (workspace_config).
|
|
@@ -57,6 +57,34 @@ def _check_proxy_token() -> None:
|
|
|
57
57
|
pass
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _check_instructions_staleness() -> None:
|
|
61
|
+
"""Warn if team AI instructions have been updated since last sync.
|
|
62
|
+
|
|
63
|
+
Reads local instructions_version from config, hits GET /team-os/instructions/version,
|
|
64
|
+
prints one-line warning if stale. Timeout 2s — never blocks the session.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
import urllib.request as _req
|
|
68
|
+
cfg = load_config()
|
|
69
|
+
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
70
|
+
agent_token = cfg.get("agent_token", "")
|
|
71
|
+
local_version = cfg.get("instructions_version")
|
|
72
|
+
if not agent_token or not local_version:
|
|
73
|
+
return # not synced yet — nothing to compare
|
|
74
|
+
|
|
75
|
+
request = _req.Request(
|
|
76
|
+
f"{api_url}/team-os/instructions/version",
|
|
77
|
+
headers={"Authorization": f"Bearer {agent_token}"},
|
|
78
|
+
)
|
|
79
|
+
with _req.urlopen(request, timeout=2) as resp:
|
|
80
|
+
data = json.loads(resp.read())
|
|
81
|
+
remote_version = data.get("version")
|
|
82
|
+
if remote_version and remote_version != local_version:
|
|
83
|
+
print(f"⚠ Team AI instructions updated ({local_version} → {remote_version}) — run `conduct guard sync` to pull latest\n")
|
|
84
|
+
except Exception:
|
|
85
|
+
pass # always silent — session-start must never fail
|
|
86
|
+
|
|
87
|
+
|
|
60
88
|
def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
|
|
61
89
|
"""Parse + PATCH the most recent completed session (the one that just ended).
|
|
62
90
|
|
|
@@ -155,6 +183,7 @@ def main() -> None:
|
|
|
155
183
|
pass
|
|
156
184
|
|
|
157
185
|
_check_proxy_token()
|
|
186
|
+
_check_instructions_staleness()
|
|
158
187
|
|
|
159
188
|
if not SNAPSHOT_PATH.exists():
|
|
160
189
|
sys.exit(0)
|
|
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
|
|
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
|