conduct-cli 0.7.46__tar.gz → 0.8.0__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.46 → conduct_cli-0.8.0}/PKG-INFO +1 -1
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/pyproject.toml +1 -1
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/api.py +2 -4
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/guard.py +113 -96
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/base.py +3 -3
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/posttooluse.py +2 -2
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/pretooluse.py +2 -2
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_report_push.py +3 -3
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_start.py +2 -2
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/stop.py +2 -2
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/main.py +70 -77
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/mcp_server.py +1 -1
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/memory.py +4 -4
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/README.md +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/setup.cfg +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/setup.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_api_client.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_log_util.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_switch.py +0 -0
- {conduct_cli-0.7.46 → conduct_cli-0.8.0}/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
|
+
version = "0.8.0"
|
|
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" }
|
|
@@ -22,11 +22,9 @@ def _friendly(code: int, detail: str) -> str:
|
|
|
22
22
|
return detail
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def headers(workspace_id: str, token=None, content_type="application/json"
|
|
25
|
+
def headers(workspace_id: str, token=None, content_type="application/json") -> dict:
|
|
26
26
|
h = {"Content-Type": content_type, "X-Workspace-Id": workspace_id}
|
|
27
|
-
if
|
|
28
|
-
h["X-Api-Key"] = api_key
|
|
29
|
-
elif token:
|
|
27
|
+
if token:
|
|
30
28
|
h["Authorization"] = f"Bearer {token}"
|
|
31
29
|
return h
|
|
32
30
|
|
|
@@ -276,11 +276,12 @@ _PERSONA_LABELS = {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
|
|
279
|
-
def _ensure_persona(workspace_id: str,
|
|
279
|
+
def _ensure_persona(workspace_id: str, base_url: str) -> str:
|
|
280
280
|
"""Prompt for persona if none is set yet. Saves choice to guard config and API.
|
|
281
281
|
|
|
282
282
|
Returns the active persona name. Skips prompt silently if already set.
|
|
283
283
|
"""
|
|
284
|
+
agent_token = _default_agent_token() or ""
|
|
284
285
|
cfg = _load_guard_config()
|
|
285
286
|
if cfg.get("persona"):
|
|
286
287
|
return cfg["persona"]
|
|
@@ -310,7 +311,7 @@ def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
|
|
|
310
311
|
"PATCH",
|
|
311
312
|
f"{base_url}/guard/config/persona",
|
|
312
313
|
body={"persona": chosen},
|
|
313
|
-
|
|
314
|
+
token=agent_token or None,
|
|
314
315
|
)
|
|
315
316
|
except Exception:
|
|
316
317
|
pass # non-fatal — local config still records the choice
|
|
@@ -352,7 +353,7 @@ def _require_guard_config() -> dict:
|
|
|
352
353
|
if not cfg or not ws:
|
|
353
354
|
print(f"{RED}Guard not connected. Run: conduct login{RESET}", file=sys.stderr)
|
|
354
355
|
sys.exit(0)
|
|
355
|
-
if not cfg.get("agent_token")
|
|
356
|
+
if not cfg.get("agent_token"):
|
|
356
357
|
print(f"{RED}Guard config is missing credentials. Run: conduct login{RESET}", file=sys.stderr)
|
|
357
358
|
sys.exit(0)
|
|
358
359
|
return cfg
|
|
@@ -474,40 +475,67 @@ def _patch_claude_desktop_proxy(api_url: str, agent_token: str) -> None:
|
|
|
474
475
|
|
|
475
476
|
|
|
476
477
|
def _patch_copilot_mcp(agent_token: str, api_url: str) -> None:
|
|
477
|
-
"""Keep ~/.copilot/mcp-config.json in sync with current agent token.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
the way Claude Code / Cursor can, so we write the SSE entry with a fresh token.
|
|
481
|
-
"""
|
|
482
|
-
cfg_path = Path.home() / ".copilot" / "mcp-config.json"
|
|
483
|
-
if not cfg_path.exists():
|
|
484
|
-
return
|
|
485
|
-
try:
|
|
486
|
-
cfg = json.loads(cfg_path.read_text())
|
|
487
|
-
except (json.JSONDecodeError, OSError):
|
|
488
|
-
cfg = {}
|
|
489
|
-
mcp = cfg.setdefault("mcpServers", {})
|
|
490
|
-
entry = {
|
|
478
|
+
"""Keep ~/.copilot/mcp-config.json and any .mcp.json in cwd in sync with current agent token."""
|
|
479
|
+
import shutil
|
|
480
|
+
sse_entry = {
|
|
491
481
|
"type": "sse",
|
|
492
482
|
"url": f"{api_url}/guard/mcp",
|
|
493
483
|
"headers": {"Authorization": f"Bearer {agent_token}"},
|
|
494
484
|
"tools": ["**"],
|
|
495
485
|
}
|
|
496
|
-
if
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
486
|
+
booster_entry = {"command": "booster", "args": ["serve"]} if shutil.which("booster") else None
|
|
487
|
+
|
|
488
|
+
# ~/.copilot/mcp-config.json (global Copilot config)
|
|
489
|
+
global_path = Path.home() / ".copilot" / "mcp-config.json"
|
|
490
|
+
if global_path.exists():
|
|
491
|
+
_write_mcp_file(global_path, "conduct-guard", sse_entry, booster_entry, "GitHub Copilot (global)")
|
|
492
|
+
|
|
493
|
+
# .mcp.json in cwd (project-level, picked up by VS Code Copilot)
|
|
494
|
+
local_path = Path.cwd() / ".mcp.json"
|
|
495
|
+
if local_path.exists():
|
|
496
|
+
_write_mcp_file(local_path, "conduct-guard", sse_entry, booster_entry, "GitHub Copilot (.mcp.json)")
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _write_mcp_file(
|
|
500
|
+
cfg_path: Path,
|
|
501
|
+
guard_key: str,
|
|
502
|
+
sse_entry: dict,
|
|
503
|
+
booster_entry: dict | None,
|
|
504
|
+
label: str,
|
|
505
|
+
) -> None:
|
|
506
|
+
try:
|
|
507
|
+
cfg = json.loads(cfg_path.read_text())
|
|
508
|
+
except (json.JSONDecodeError, OSError):
|
|
509
|
+
cfg = {}
|
|
510
|
+
mcp = cfg.setdefault("mcpServers", {})
|
|
511
|
+
changed = False
|
|
512
|
+
if mcp.get(guard_key) != sse_entry:
|
|
513
|
+
mcp[guard_key] = sse_entry
|
|
514
|
+
changed = True
|
|
515
|
+
print(f" {GREEN}conduct-guard MCP registered in {label}{RESET}")
|
|
516
|
+
else:
|
|
517
|
+
print(f" {GRAY}conduct-guard MCP already registered in {label}{RESET}")
|
|
518
|
+
if booster_entry and mcp.get("agent-booster") != booster_entry:
|
|
519
|
+
mcp["agent-booster"] = booster_entry
|
|
520
|
+
changed = True
|
|
521
|
+
if changed:
|
|
522
|
+
cfg_path.write_text(json.dumps(cfg, indent=2))
|
|
502
523
|
|
|
503
524
|
|
|
504
525
|
_GUARD_RULES_TEXT = (
|
|
526
|
+
"## ConductGuard — AI Governance Policy\n\n"
|
|
527
|
+
"A ConductGuard MCP server is connected to this session (server name: `conduct-guard`).\n"
|
|
528
|
+
"All Guard policies are enforced through it. You MUST follow these rules:\n\n"
|
|
505
529
|
"Before executing any shell command, file write, network request, API call, or code change:\n"
|
|
506
|
-
"
|
|
507
|
-
"
|
|
508
|
-
"
|
|
509
|
-
"
|
|
510
|
-
"-
|
|
530
|
+
"1. Call the `guard_check` tool from the `conduct-guard` MCP server with the action and parameters.\n"
|
|
531
|
+
"2. If the response is BLOCKED: stop immediately and explain the policy rule to the user. Do not proceed.\n"
|
|
532
|
+
"3. If the response is WARNING: proceed but surface the warning message to the user.\n"
|
|
533
|
+
"4. If the response is ALLOWED: proceed normally.\n\n"
|
|
534
|
+
"Always-on rules (no guard_check needed, enforce unconditionally):\n"
|
|
535
|
+
"- Never write credentials, API keys, tokens, or secrets to any file or output.\n"
|
|
536
|
+
"- Never send PII (emails, names, payment data) to external endpoints.\n\n"
|
|
537
|
+
"Use `guard_status` (conduct-guard MCP) to show the user their current policy and team info.\n"
|
|
538
|
+
"Use `guard_activity` at the start of every session with a one-line summary of what you are doing."
|
|
511
539
|
)
|
|
512
540
|
|
|
513
541
|
|
|
@@ -562,22 +590,10 @@ def _patch_tool_instruction_files(agent_token: str, api_url: str, dry_run: bool
|
|
|
562
590
|
MARKER_END = "<!-- /ConductGuard -->"
|
|
563
591
|
GUARD_RULES = _GUARD_RULES_TEXT
|
|
564
592
|
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
resp = _req("GET", f"{api_url}/team-os/instructions", token=agent_token)
|
|
570
|
-
if (resp.get("version") or "v0") != "v0":
|
|
571
|
-
team_content = (resp.get("content") or "").strip()
|
|
572
|
-
except Exception:
|
|
573
|
-
pass
|
|
574
|
-
|
|
575
|
-
parts = [MARKER_START, "## ConductGuard Policy"]
|
|
576
|
-
if team_content:
|
|
577
|
-
parts.append(team_content)
|
|
578
|
-
parts.append(GUARD_RULES)
|
|
579
|
-
parts.append(MARKER_END)
|
|
580
|
-
guard_block = "\n".join(parts)
|
|
593
|
+
# Guard-only for now — team-os instructions are hidden pending redesign.
|
|
594
|
+
# This block just ensures every AI tool knows to invoke Guard via hooks
|
|
595
|
+
# or the conduct-guard MCP before taking action.
|
|
596
|
+
guard_block = "\n".join([MARKER_START, "## ConductGuard Policy", GUARD_RULES, MARKER_END])
|
|
581
597
|
|
|
582
598
|
repo_root: Path | None = None
|
|
583
599
|
for parent in [Path.cwd(), *Path.cwd().parents]:
|
|
@@ -731,12 +747,26 @@ def _install_codex_hook(hook_path: Path) -> None:
|
|
|
731
747
|
|
|
732
748
|
# ── HTTP helpers (no third-party deps — mirrors api.py style) ─────────────────
|
|
733
749
|
|
|
734
|
-
def
|
|
750
|
+
def _default_agent_token() -> str | None:
|
|
751
|
+
"""Read the agent_token minted by `conduct login` from ~/.conduct/config.json.
|
|
752
|
+
This is the CLI's single canonical credential — every call to the central
|
|
753
|
+
API should carry it via Authorization: Bearer."""
|
|
754
|
+
try:
|
|
755
|
+
cfg_path = Path.home() / ".conduct" / "config.json"
|
|
756
|
+
if cfg_path.exists():
|
|
757
|
+
return json.loads(cfg_path.read_text()).get("agent_token") or None
|
|
758
|
+
except Exception:
|
|
759
|
+
pass
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def _req(method: str, url: str, body=None, token: str = None, timeout: int = 20) -> dict:
|
|
764
|
+
# If no explicit token passed, fall back to the login-minted agent_token.
|
|
765
|
+
if not token:
|
|
766
|
+
token = _default_agent_token()
|
|
735
767
|
headers = {"Content-Type": "application/json"}
|
|
736
768
|
if token:
|
|
737
769
|
headers["Authorization"] = f"Bearer {token}"
|
|
738
|
-
if api_key:
|
|
739
|
-
headers["X-Api-Key"] = api_key
|
|
740
770
|
data = json.dumps(body).encode() if body is not None else None
|
|
741
771
|
r = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
742
772
|
try:
|
|
@@ -872,10 +902,10 @@ def _install_claude_hook(hook_path: Path) -> None:
|
|
|
872
902
|
|
|
873
903
|
def cmd_guard_install(args):
|
|
874
904
|
"""Called automatically from `conduct login`. Sets up Guard: downloads policies, installs hook + MCP."""
|
|
875
|
-
|
|
876
|
-
server
|
|
905
|
+
agent_token = getattr(args, "agent_token", None)
|
|
906
|
+
server = (getattr(args, "server", None) or "https://api.conductai.ai").rstrip("/")
|
|
877
907
|
|
|
878
|
-
# Load workspace_id from ~/.conduct/config.json
|
|
908
|
+
# Load workspace_id (and fallback agent_token) from ~/.conduct/config.json
|
|
879
909
|
conduct_cfg_path = Path.home() / ".conduct" / "config.json"
|
|
880
910
|
conduct_cfg: dict = {}
|
|
881
911
|
if conduct_cfg_path.exists():
|
|
@@ -885,7 +915,9 @@ def cmd_guard_install(args):
|
|
|
885
915
|
pass
|
|
886
916
|
|
|
887
917
|
workspace_id = conduct_cfg.get("workspace")
|
|
888
|
-
if not
|
|
918
|
+
if not agent_token:
|
|
919
|
+
agent_token = conduct_cfg.get("agent_token")
|
|
920
|
+
if not workspace_id or not agent_token:
|
|
889
921
|
return # nothing to do
|
|
890
922
|
|
|
891
923
|
print(f" Setting up Guard…")
|
|
@@ -893,16 +925,16 @@ def cmd_guard_install(args):
|
|
|
893
925
|
result = _req(
|
|
894
926
|
"GET",
|
|
895
927
|
f"{server}/guard/config/installed?workspace_id={workspace_id}",
|
|
896
|
-
|
|
928
|
+
token=agent_token or None,
|
|
897
929
|
)
|
|
898
930
|
|
|
899
931
|
if not result.get("installed"):
|
|
900
932
|
print(f" {GRAY}Guard not installed for this workspace — skipping{RESET}")
|
|
901
933
|
return
|
|
902
934
|
|
|
903
|
-
|
|
904
|
-
user_email
|
|
905
|
-
clerk_user_id
|
|
935
|
+
resp_agent_token = result.get("agent_token") or agent_token
|
|
936
|
+
user_email = result.get("user_email") or ""
|
|
937
|
+
clerk_user_id = result.get("clerk_user_id") or ""
|
|
906
938
|
|
|
907
939
|
# Persona selection — prompt once, skip if already chosen
|
|
908
940
|
|
|
@@ -910,10 +942,9 @@ def cmd_guard_install(args):
|
|
|
910
942
|
import time as _time
|
|
911
943
|
_save_guard_config({
|
|
912
944
|
"workspace_id": workspace_id,
|
|
913
|
-
"agent_token":
|
|
945
|
+
"agent_token": resp_agent_token,
|
|
914
946
|
"user_email": user_email,
|
|
915
947
|
"clerk_user_id": clerk_user_id,
|
|
916
|
-
"api_key": api_key,
|
|
917
948
|
"api_url": server,
|
|
918
949
|
"last_synced_at": _time.time(),
|
|
919
950
|
})
|
|
@@ -1084,7 +1115,7 @@ def _detect_ai_tools() -> list[dict]:
|
|
|
1084
1115
|
def _check_json_mcp(path: Path) -> bool:
|
|
1085
1116
|
try:
|
|
1086
1117
|
d = json.loads(path.read_text()) if path.exists() else {}
|
|
1087
|
-
return "conduct" in d.get("mcpServers", {})
|
|
1118
|
+
return any("conduct" in k for k in d.get("mcpServers", {}))
|
|
1088
1119
|
except Exception:
|
|
1089
1120
|
return False
|
|
1090
1121
|
|
|
@@ -1191,7 +1222,7 @@ def _detect_ai_tools() -> list[dict]:
|
|
|
1191
1222
|
vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
|
|
1192
1223
|
try:
|
|
1193
1224
|
d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
|
|
1194
|
-
mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
|
|
1225
|
+
mcp_reg = any("conduct" in k for k in d.get("mcp", {}).get("servers", {}))
|
|
1195
1226
|
except Exception:
|
|
1196
1227
|
mcp_reg = False
|
|
1197
1228
|
# Also check ~/.copilot/mcp-config.json (SSE entry written by guard sync)
|
|
@@ -1223,24 +1254,20 @@ def _report_tools_to_server() -> None:
|
|
|
1223
1254
|
base_url = _api_url(cfg)
|
|
1224
1255
|
email = cfg.get("user_email", "")
|
|
1225
1256
|
token = cfg.get("agent_token", "")
|
|
1226
|
-
api_key = cfg.get("api_key", "")
|
|
1227
|
-
|
|
1228
1257
|
if not email:
|
|
1229
1258
|
return
|
|
1230
|
-
|
|
1231
|
-
# Also pull conduct API key for X-Api-Key auth (member_token is not accepted by this endpoint)
|
|
1232
1259
|
conduct_cfg_path = Path.home() / ".conduct" / "config.json"
|
|
1233
|
-
|
|
1260
|
+
conduct_agent_token = ""
|
|
1234
1261
|
if conduct_cfg_path.exists():
|
|
1235
1262
|
try:
|
|
1236
|
-
|
|
1263
|
+
conduct_agent_token = json.loads(conduct_cfg_path.read_text()).get("agent_token", "")
|
|
1237
1264
|
except Exception:
|
|
1238
1265
|
pass
|
|
1239
1266
|
|
|
1240
1267
|
payload = json.dumps({"email": email, "tools": tools}).encode()
|
|
1241
1268
|
headers = {"Content-Type": "application/json"}
|
|
1242
|
-
if
|
|
1243
|
-
headers["
|
|
1269
|
+
if conduct_agent_token and conduct_agent_token.startswith("cond_live_"):
|
|
1270
|
+
headers["Authorization"] = f"Bearer {conduct_agent_token}"
|
|
1244
1271
|
elif token:
|
|
1245
1272
|
headers["Authorization"] = f"Bearer {token}"
|
|
1246
1273
|
req = urllib.request.Request(
|
|
@@ -1445,14 +1472,6 @@ def cmd_guard_sync(args):
|
|
|
1445
1472
|
except Exception as e:
|
|
1446
1473
|
print(f" {YELLOW}Warning: could not refresh token ({e}) — using cached value{RESET}")
|
|
1447
1474
|
|
|
1448
|
-
# Fetch team instructions version so session-start hook can detect staleness
|
|
1449
|
-
try:
|
|
1450
|
-
ver_resp = _req("GET", f"{base_url}/team-os/instructions/version", token=_token)
|
|
1451
|
-
if ver_resp.get("version"):
|
|
1452
|
-
_save_guard_config({"instructions_version": ver_resp["version"]})
|
|
1453
|
-
except Exception:
|
|
1454
|
-
pass # non-fatal
|
|
1455
|
-
|
|
1456
1475
|
# Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
|
|
1457
1476
|
# routes through Conduct Guard. Customer-overridable via --proxy-url or
|
|
1458
1477
|
# CONDUCT_PROXY_URL env var; otherwise fetched from server (workspace_config).
|
|
@@ -1848,7 +1867,7 @@ def _ensure_booster(root: Path) -> None:
|
|
|
1848
1867
|
pass
|
|
1849
1868
|
|
|
1850
1869
|
|
|
1851
|
-
def _report_savings(cfg: dict, base_url: str,
|
|
1870
|
+
def _report_savings(cfg: dict, base_url: str, agent_token: str = "") -> None:
|
|
1852
1871
|
import subprocess
|
|
1853
1872
|
|
|
1854
1873
|
rtk_data = {}
|
|
@@ -1909,12 +1928,10 @@ def _report_savings(cfg: dict, base_url: str, api_key: str = "") -> None:
|
|
|
1909
1928
|
}
|
|
1910
1929
|
|
|
1911
1930
|
try:
|
|
1912
|
-
agent_token_evt = cfg.get("agent_token", "")
|
|
1931
|
+
agent_token_evt = cfg.get("agent_token", "") or agent_token
|
|
1913
1932
|
headers = {"Content-Type": "application/json"}
|
|
1914
1933
|
if agent_token_evt:
|
|
1915
1934
|
headers["Authorization"] = f"Bearer {agent_token_evt}"
|
|
1916
|
-
elif api_key:
|
|
1917
|
-
headers["X-Api-Key"] = api_key
|
|
1918
1935
|
data = json.dumps(payload).encode()
|
|
1919
1936
|
req = urllib.request.Request(
|
|
1920
1937
|
f"{base_url}/guard/savings",
|
|
@@ -1943,13 +1960,13 @@ def cmd_guard_status(args):
|
|
|
1943
1960
|
cfg = _require_guard_config()
|
|
1944
1961
|
workspace_id = cfg.get("workspace_id")
|
|
1945
1962
|
user_email = cfg.get("user_email", "")
|
|
1946
|
-
|
|
1963
|
+
agent_token = cfg.get("agent_token", "")
|
|
1947
1964
|
base_url = _api_url(cfg)
|
|
1948
1965
|
|
|
1949
1966
|
# Auto-refresh user_email + clerk_user_id into config if missing
|
|
1950
|
-
if (not user_email or not cfg.get("clerk_user_id")) and
|
|
1967
|
+
if (not user_email or not cfg.get("clerk_user_id")) and agent_token:
|
|
1951
1968
|
try:
|
|
1952
|
-
installed = _req("GET", f"{base_url}/guard/config/installed",
|
|
1969
|
+
installed = _req("GET", f"{base_url}/guard/config/installed", token=agent_token or None)
|
|
1953
1970
|
fetched_email = installed.get("user_email") or ""
|
|
1954
1971
|
fetched_clerk = installed.get("clerk_user_id") or ""
|
|
1955
1972
|
if fetched_email:
|
|
@@ -1973,7 +1990,7 @@ def cmd_guard_status(args):
|
|
|
1973
1990
|
spend = _req(
|
|
1974
1991
|
"GET",
|
|
1975
1992
|
f"{base_url}/guard/spend?workspace_id={workspace_id}",
|
|
1976
|
-
|
|
1993
|
+
token=agent_token or None,
|
|
1977
1994
|
)
|
|
1978
1995
|
except SystemExit:
|
|
1979
1996
|
pass
|
|
@@ -1993,7 +2010,7 @@ def cmd_guard_status(args):
|
|
|
1993
2010
|
f"&since={today_iso}"
|
|
1994
2011
|
f"&limit=20"
|
|
1995
2012
|
),
|
|
1996
|
-
|
|
2013
|
+
token=agent_token or None,
|
|
1997
2014
|
)
|
|
1998
2015
|
if not isinstance(events, list):
|
|
1999
2016
|
events = events.get("events", [])
|
|
@@ -2074,14 +2091,14 @@ def cmd_guard_status(args):
|
|
|
2074
2091
|
def cmd_guard_savings(args):
|
|
2075
2092
|
cfg = _require_guard_config()
|
|
2076
2093
|
workspace_id = cfg.get("workspace_id")
|
|
2077
|
-
|
|
2094
|
+
agent_token = cfg.get("agent_token", "")
|
|
2078
2095
|
base_url = _api_url(cfg)
|
|
2079
2096
|
|
|
2080
2097
|
try:
|
|
2081
2098
|
data = _req(
|
|
2082
2099
|
"GET",
|
|
2083
2100
|
f"{base_url}/guard/savings/team-summary?workspace_id={workspace_id}",
|
|
2084
|
-
|
|
2101
|
+
token=agent_token or None,
|
|
2085
2102
|
)
|
|
2086
2103
|
except Exception:
|
|
2087
2104
|
print(f"{RED}Failed to fetch team savings.{RESET}")
|
|
@@ -2141,7 +2158,7 @@ def cmd_guard_audit(args):
|
|
|
2141
2158
|
cfg = _require_guard_config()
|
|
2142
2159
|
workspace_id = cfg.get("workspace_id")
|
|
2143
2160
|
user_email = cfg.get("user_email", "")
|
|
2144
|
-
|
|
2161
|
+
agent_token = cfg.get("agent_token", "")
|
|
2145
2162
|
base_url = _api_url(cfg)
|
|
2146
2163
|
|
|
2147
2164
|
since_str = getattr(args, "since", None) or "24h"
|
|
@@ -2156,7 +2173,7 @@ def cmd_guard_audit(args):
|
|
|
2156
2173
|
f"&since={since_iso}"
|
|
2157
2174
|
f"&limit=50"
|
|
2158
2175
|
),
|
|
2159
|
-
|
|
2176
|
+
token=agent_token or None,
|
|
2160
2177
|
)
|
|
2161
2178
|
events = events_resp if isinstance(events_resp, list) else events_resp.get("events", [])
|
|
2162
2179
|
|
|
@@ -2321,7 +2338,7 @@ def _discover_config_agents() -> list[tuple]:
|
|
|
2321
2338
|
return "conduct" in text and "mcp_servers" in text
|
|
2322
2339
|
import json as _j
|
|
2323
2340
|
d = _j.loads(text)
|
|
2324
|
-
return "conduct" in d.get("mcpServers", {})
|
|
2341
|
+
return any("conduct" in k for k in d.get("mcpServers", {}))
|
|
2325
2342
|
except Exception:
|
|
2326
2343
|
return False
|
|
2327
2344
|
|
|
@@ -2347,6 +2364,7 @@ def _discover_config_agents() -> list[tuple]:
|
|
|
2347
2364
|
("codex", home / ".codex", home / ".codex" / "config.toml", "mcp"),
|
|
2348
2365
|
("cursor", home / ".cursor", home / ".cursor" / "mcp.json", "mcp"),
|
|
2349
2366
|
("windsurf", home / ".codeium" / "windsurf", home / ".codeium" / "windsurf" / "mcp_config.json", "mcp"),
|
|
2367
|
+
("copilot", home / ".copilot", home / ".copilot" / "mcp-config.json", "mcp"),
|
|
2350
2368
|
]
|
|
2351
2369
|
for name, check_dir, config_path, _ in TOOLS:
|
|
2352
2370
|
if check_dir.exists():
|
|
@@ -2465,7 +2483,7 @@ def _watch_loop():
|
|
|
2465
2483
|
c = _cfg()
|
|
2466
2484
|
api_url = _api_url(c)
|
|
2467
2485
|
token = c.get("agent_token", "")
|
|
2468
|
-
|
|
2486
|
+
agent_token = c.get("agent_token", "")
|
|
2469
2487
|
agents = []
|
|
2470
2488
|
for name, config_path, mcp_key in _discover_config_agents():
|
|
2471
2489
|
agents.append({
|
|
@@ -2485,7 +2503,7 @@ def _watch_loop():
|
|
|
2485
2503
|
pass
|
|
2486
2504
|
_req("POST", f"{api_url}/guard/discover/scan",
|
|
2487
2505
|
body={"triggered_by": "watch", "agents": agents},
|
|
2488
|
-
token=token
|
|
2506
|
+
token=token or agent_token)
|
|
2489
2507
|
except Exception:
|
|
2490
2508
|
pass
|
|
2491
2509
|
time.sleep(_WATCH_INTERVAL)
|
|
@@ -2641,7 +2659,6 @@ def cmd_guard_discover(args):
|
|
|
2641
2659
|
cfg = _load_guard_config()
|
|
2642
2660
|
api_url = _api_url(cfg)
|
|
2643
2661
|
token = cfg.get("agent_token", "")
|
|
2644
|
-
api_key = cfg.get("api_key", "")
|
|
2645
2662
|
hdrs = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
2646
2663
|
|
|
2647
2664
|
# Config file scan — detect AI tools from known config dirs
|
|
@@ -2701,7 +2718,7 @@ def cmd_guard_discover(args):
|
|
|
2701
2718
|
"proxy_routed": t.get("proxy_routed", False),
|
|
2702
2719
|
})
|
|
2703
2720
|
payload = {"triggered_by": "cli", "agents": agents_payload}
|
|
2704
|
-
_req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token
|
|
2721
|
+
_req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token)
|
|
2705
2722
|
except SystemExit:
|
|
2706
2723
|
pass # 401/network errors — local output still useful
|
|
2707
2724
|
|
|
@@ -2915,14 +2932,14 @@ def cmd_verify(args) -> None:
|
|
|
2915
2932
|
|
|
2916
2933
|
cfg = _require_guard_config()
|
|
2917
2934
|
workspace_id = cfg.get("workspace_id")
|
|
2918
|
-
|
|
2935
|
+
agent_token = cfg.get("agent_token", "")
|
|
2919
2936
|
base_url = _api_url(cfg)
|
|
2920
2937
|
|
|
2921
2938
|
# ── fetch evidence from API ───────────────────────────────────────────────
|
|
2922
2939
|
ev = _req(
|
|
2923
2940
|
"GET",
|
|
2924
2941
|
f"{base_url}/guard/verify/evidence?workspace_id={workspace_id}",
|
|
2925
|
-
|
|
2942
|
+
token=agent_token or None,
|
|
2926
2943
|
)
|
|
2927
2944
|
|
|
2928
2945
|
grade = ev.get("grade", "F")
|
|
@@ -2939,7 +2956,7 @@ def cmd_verify(args) -> None:
|
|
|
2939
2956
|
run_result = _req(
|
|
2940
2957
|
"POST",
|
|
2941
2958
|
f"{base_url}/guard/verify/run?workspace_id={workspace_id}",
|
|
2942
|
-
|
|
2959
|
+
token=agent_token or None,
|
|
2943
2960
|
)
|
|
2944
2961
|
if fmt == "json":
|
|
2945
2962
|
print(_json.dumps(run_result, indent=2))
|
|
@@ -148,14 +148,14 @@ def _refresh_policy_from_api() -> None:
|
|
|
148
148
|
try:
|
|
149
149
|
cfg = load_config()
|
|
150
150
|
workspace_id = cfg.get("workspace_id")
|
|
151
|
-
|
|
151
|
+
agent_token = cfg.get("agent_token", "")
|
|
152
152
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
153
|
-
if not
|
|
153
|
+
if not agent_token:
|
|
154
154
|
return
|
|
155
155
|
url = f"{api_url}/guard/policies/sync"
|
|
156
156
|
if workspace_id:
|
|
157
157
|
url += f"?workspace_id={workspace_id}"
|
|
158
|
-
req = urllib.request.Request(url, headers={"
|
|
158
|
+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {agent_token}"})
|
|
159
159
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
160
160
|
data = resp.read()
|
|
161
161
|
POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -92,7 +92,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
|
|
|
92
92
|
if not cfg.get("security_emit_enabled", False):
|
|
93
93
|
return
|
|
94
94
|
workspace_id = cfg.get("workspace_id")
|
|
95
|
-
|
|
95
|
+
agent_token = cfg.get("agent_token", "")
|
|
96
96
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
97
97
|
if not workspace_id:
|
|
98
98
|
return
|
|
@@ -138,7 +138,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
|
|
|
138
138
|
"try:\n"
|
|
139
139
|
f" req = urllib.request.Request(\"{api_url}/security-findings?workspace_id={workspace_id}\","
|
|
140
140
|
f" data={repr(payload.encode())}, headers={{\"Content-Type\": \"application/json\","
|
|
141
|
-
f" \"
|
|
141
|
+
f" \"Authorization\": \"Bearer {agent_token}\"}}, method=\"POST\")\n"
|
|
142
142
|
" urllib.request.urlopen(req, timeout=5)\n"
|
|
143
143
|
"except: pass\n"
|
|
144
144
|
)
|
|
@@ -156,7 +156,7 @@ def _maybe_sync_policy() -> None:
|
|
|
156
156
|
try:
|
|
157
157
|
cfg = load_config()
|
|
158
158
|
workspace_id = cfg.get("workspace_id") or cfg.get("workspace")
|
|
159
|
-
|
|
159
|
+
agent_token = cfg.get("agent_token", "")
|
|
160
160
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
161
161
|
if not workspace_id:
|
|
162
162
|
return
|
|
@@ -175,7 +175,7 @@ def _maybe_sync_policy() -> None:
|
|
|
175
175
|
if time.time() - cache.get("ts", 0) < VERSION_CACHE_TTL:
|
|
176
176
|
return
|
|
177
177
|
url = f"{api_url}/guard/policies/sync?workspace_id={workspace_id}"
|
|
178
|
-
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {
|
|
178
|
+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {agent_token}"} if agent_token else {})
|
|
179
179
|
with urllib.request.urlopen(req, timeout=2) as resp:
|
|
180
180
|
remote = json.loads(resp.read())
|
|
181
181
|
|
|
@@ -33,7 +33,7 @@ def _load_config() -> dict:
|
|
|
33
33
|
def run(session_id: str = "") -> None:
|
|
34
34
|
cfg = _load_config()
|
|
35
35
|
server = (cfg.get("api_url") or cfg.get("server") or "https://api.conductai.ai").rstrip("/")
|
|
36
|
-
|
|
36
|
+
agent_token = cfg.get("agent_token", "")
|
|
37
37
|
member_token = cfg.get("member_token", "")
|
|
38
38
|
workspace_id = cfg.get("workspace_id", "")
|
|
39
39
|
|
|
@@ -115,8 +115,8 @@ def run(session_id: str = "") -> None:
|
|
|
115
115
|
}
|
|
116
116
|
if member_token:
|
|
117
117
|
headers["Authorization"] = f"Bearer {member_token}"
|
|
118
|
-
elif
|
|
119
|
-
headers["
|
|
118
|
+
elif agent_token:
|
|
119
|
+
headers["Authorization"] = f"Bearer {agent_token}"
|
|
120
120
|
|
|
121
121
|
req = urllib.request.Request(
|
|
122
122
|
f"{server}/guard/session-reports",
|
|
@@ -124,7 +124,7 @@ def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
|
|
|
124
124
|
return
|
|
125
125
|
cfg = json.loads(cfg_path.read_text())
|
|
126
126
|
server = cfg.get("server", "").rstrip("/")
|
|
127
|
-
|
|
127
|
+
agent_token = cfg.get("agent_token", "")
|
|
128
128
|
workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
|
|
129
129
|
if not server:
|
|
130
130
|
return
|
|
@@ -145,7 +145,7 @@ def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
|
|
|
145
145
|
data=payload,
|
|
146
146
|
headers={
|
|
147
147
|
"Content-Type": "application/json",
|
|
148
|
-
"
|
|
148
|
+
"Authorization": f"Bearer {agent_token}",
|
|
149
149
|
"X-Workspace-ID": workspace_id,
|
|
150
150
|
},
|
|
151
151
|
method="PATCH",
|
|
@@ -26,7 +26,7 @@ def _post_intent(session_id: str, ai_tool: str, cwd: str | None) -> None:
|
|
|
26
26
|
return
|
|
27
27
|
cfg = _json.loads(cfg_path.read_text())
|
|
28
28
|
server = cfg.get("server", "").rstrip("/")
|
|
29
|
-
|
|
29
|
+
agent_token = cfg.get("agent_token", "")
|
|
30
30
|
workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
|
|
31
31
|
if not server or not session_id:
|
|
32
32
|
return
|
|
@@ -43,7 +43,7 @@ def _post_intent(session_id: str, ai_tool: str, cwd: str | None) -> None:
|
|
|
43
43
|
data=payload,
|
|
44
44
|
headers={
|
|
45
45
|
"Content-Type": "application/json",
|
|
46
|
-
"
|
|
46
|
+
"Authorization": f"Bearer {agent_token}",
|
|
47
47
|
"X-Workspace-ID": workspace_id,
|
|
48
48
|
},
|
|
49
49
|
method="PATCH",
|