conduct-cli 0.7.48__tar.gz → 0.8.1__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.48 → conduct_cli-0.8.1}/PKG-INFO +1 -1
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/pyproject.toml +1 -1
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/api.py +2 -4
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/guard.py +73 -75
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/base.py +3 -3
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/posttooluse.py +2 -2
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/pretooluse.py +2 -2
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/session_report_push.py +3 -3
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/session_start.py +2 -2
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/stop.py +2 -2
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/main.py +70 -77
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/mcp_server.py +1 -1
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/memory.py +4 -4
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/README.md +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/setup.cfg +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/setup.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_api_client.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_log_util.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/tests/test_switch.py +0 -0
- {conduct_cli-0.7.48 → conduct_cli-0.8.1}/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.1"
|
|
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
|
|
@@ -522,12 +523,19 @@ def _write_mcp_file(
|
|
|
522
523
|
|
|
523
524
|
|
|
524
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"
|
|
525
529
|
"Before executing any shell command, file write, network request, API call, or code change:\n"
|
|
526
|
-
"
|
|
527
|
-
"
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"-
|
|
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."
|
|
531
539
|
)
|
|
532
540
|
|
|
533
541
|
|
|
@@ -582,22 +590,10 @@ def _patch_tool_instruction_files(agent_token: str, api_url: str, dry_run: bool
|
|
|
582
590
|
MARKER_END = "<!-- /ConductGuard -->"
|
|
583
591
|
GUARD_RULES = _GUARD_RULES_TEXT
|
|
584
592
|
|
|
585
|
-
#
|
|
586
|
-
#
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
resp = _req("GET", f"{api_url}/team-os/instructions", token=agent_token)
|
|
590
|
-
if (resp.get("version") or "v0") != "v0":
|
|
591
|
-
team_content = (resp.get("content") or "").strip()
|
|
592
|
-
except Exception:
|
|
593
|
-
pass
|
|
594
|
-
|
|
595
|
-
parts = [MARKER_START, "## ConductGuard Policy"]
|
|
596
|
-
if team_content:
|
|
597
|
-
parts.append(team_content)
|
|
598
|
-
parts.append(GUARD_RULES)
|
|
599
|
-
parts.append(MARKER_END)
|
|
600
|
-
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])
|
|
601
597
|
|
|
602
598
|
repo_root: Path | None = None
|
|
603
599
|
for parent in [Path.cwd(), *Path.cwd().parents]:
|
|
@@ -751,12 +747,26 @@ def _install_codex_hook(hook_path: Path) -> None:
|
|
|
751
747
|
|
|
752
748
|
# ── HTTP helpers (no third-party deps — mirrors api.py style) ─────────────────
|
|
753
749
|
|
|
754
|
-
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()
|
|
755
767
|
headers = {"Content-Type": "application/json"}
|
|
756
768
|
if token:
|
|
757
769
|
headers["Authorization"] = f"Bearer {token}"
|
|
758
|
-
if api_key:
|
|
759
|
-
headers["X-Api-Key"] = api_key
|
|
760
770
|
data = json.dumps(body).encode() if body is not None else None
|
|
761
771
|
r = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
762
772
|
try:
|
|
@@ -892,10 +902,10 @@ def _install_claude_hook(hook_path: Path) -> None:
|
|
|
892
902
|
|
|
893
903
|
def cmd_guard_install(args):
|
|
894
904
|
"""Called automatically from `conduct login`. Sets up Guard: downloads policies, installs hook + MCP."""
|
|
895
|
-
|
|
896
|
-
server
|
|
905
|
+
agent_token = getattr(args, "agent_token", None)
|
|
906
|
+
server = (getattr(args, "server", None) or "https://api.conductai.ai").rstrip("/")
|
|
897
907
|
|
|
898
|
-
# Load workspace_id from ~/.conduct/config.json
|
|
908
|
+
# Load workspace_id (and fallback agent_token) from ~/.conduct/config.json
|
|
899
909
|
conduct_cfg_path = Path.home() / ".conduct" / "config.json"
|
|
900
910
|
conduct_cfg: dict = {}
|
|
901
911
|
if conduct_cfg_path.exists():
|
|
@@ -905,7 +915,9 @@ def cmd_guard_install(args):
|
|
|
905
915
|
pass
|
|
906
916
|
|
|
907
917
|
workspace_id = conduct_cfg.get("workspace")
|
|
908
|
-
if not
|
|
918
|
+
if not agent_token:
|
|
919
|
+
agent_token = conduct_cfg.get("agent_token")
|
|
920
|
+
if not workspace_id or not agent_token:
|
|
909
921
|
return # nothing to do
|
|
910
922
|
|
|
911
923
|
print(f" Setting up Guard…")
|
|
@@ -913,16 +925,16 @@ def cmd_guard_install(args):
|
|
|
913
925
|
result = _req(
|
|
914
926
|
"GET",
|
|
915
927
|
f"{server}/guard/config/installed?workspace_id={workspace_id}",
|
|
916
|
-
|
|
928
|
+
token=agent_token or None,
|
|
917
929
|
)
|
|
918
930
|
|
|
919
931
|
if not result.get("installed"):
|
|
920
932
|
print(f" {GRAY}Guard not installed for this workspace — skipping{RESET}")
|
|
921
933
|
return
|
|
922
934
|
|
|
923
|
-
|
|
924
|
-
user_email
|
|
925
|
-
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 ""
|
|
926
938
|
|
|
927
939
|
# Persona selection — prompt once, skip if already chosen
|
|
928
940
|
|
|
@@ -930,10 +942,9 @@ def cmd_guard_install(args):
|
|
|
930
942
|
import time as _time
|
|
931
943
|
_save_guard_config({
|
|
932
944
|
"workspace_id": workspace_id,
|
|
933
|
-
"agent_token":
|
|
945
|
+
"agent_token": resp_agent_token,
|
|
934
946
|
"user_email": user_email,
|
|
935
947
|
"clerk_user_id": clerk_user_id,
|
|
936
|
-
"api_key": api_key,
|
|
937
948
|
"api_url": server,
|
|
938
949
|
"last_synced_at": _time.time(),
|
|
939
950
|
})
|
|
@@ -1243,24 +1254,20 @@ def _report_tools_to_server() -> None:
|
|
|
1243
1254
|
base_url = _api_url(cfg)
|
|
1244
1255
|
email = cfg.get("user_email", "")
|
|
1245
1256
|
token = cfg.get("agent_token", "")
|
|
1246
|
-
api_key = cfg.get("api_key", "")
|
|
1247
|
-
|
|
1248
1257
|
if not email:
|
|
1249
1258
|
return
|
|
1250
|
-
|
|
1251
|
-
# Also pull conduct API key for X-Api-Key auth (member_token is not accepted by this endpoint)
|
|
1252
1259
|
conduct_cfg_path = Path.home() / ".conduct" / "config.json"
|
|
1253
|
-
|
|
1260
|
+
conduct_agent_token = ""
|
|
1254
1261
|
if conduct_cfg_path.exists():
|
|
1255
1262
|
try:
|
|
1256
|
-
|
|
1263
|
+
conduct_agent_token = json.loads(conduct_cfg_path.read_text()).get("agent_token", "")
|
|
1257
1264
|
except Exception:
|
|
1258
1265
|
pass
|
|
1259
1266
|
|
|
1260
1267
|
payload = json.dumps({"email": email, "tools": tools}).encode()
|
|
1261
1268
|
headers = {"Content-Type": "application/json"}
|
|
1262
|
-
if
|
|
1263
|
-
headers["
|
|
1269
|
+
if conduct_agent_token and conduct_agent_token.startswith("cond_live_"):
|
|
1270
|
+
headers["Authorization"] = f"Bearer {conduct_agent_token}"
|
|
1264
1271
|
elif token:
|
|
1265
1272
|
headers["Authorization"] = f"Bearer {token}"
|
|
1266
1273
|
req = urllib.request.Request(
|
|
@@ -1465,14 +1472,6 @@ def cmd_guard_sync(args):
|
|
|
1465
1472
|
except Exception as e:
|
|
1466
1473
|
print(f" {YELLOW}Warning: could not refresh token ({e}) — using cached value{RESET}")
|
|
1467
1474
|
|
|
1468
|
-
# Fetch team instructions version so session-start hook can detect staleness
|
|
1469
|
-
try:
|
|
1470
|
-
ver_resp = _req("GET", f"{base_url}/team-os/instructions/version", token=_token)
|
|
1471
|
-
if ver_resp.get("version"):
|
|
1472
|
-
_save_guard_config({"instructions_version": ver_resp["version"]})
|
|
1473
|
-
except Exception:
|
|
1474
|
-
pass # non-fatal
|
|
1475
|
-
|
|
1476
1475
|
# Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
|
|
1477
1476
|
# routes through Conduct Guard. Customer-overridable via --proxy-url or
|
|
1478
1477
|
# CONDUCT_PROXY_URL env var; otherwise fetched from server (workspace_config).
|
|
@@ -1868,7 +1867,7 @@ def _ensure_booster(root: Path) -> None:
|
|
|
1868
1867
|
pass
|
|
1869
1868
|
|
|
1870
1869
|
|
|
1871
|
-
def _report_savings(cfg: dict, base_url: str,
|
|
1870
|
+
def _report_savings(cfg: dict, base_url: str, agent_token: str = "") -> None:
|
|
1872
1871
|
import subprocess
|
|
1873
1872
|
|
|
1874
1873
|
rtk_data = {}
|
|
@@ -1929,12 +1928,10 @@ def _report_savings(cfg: dict, base_url: str, api_key: str = "") -> None:
|
|
|
1929
1928
|
}
|
|
1930
1929
|
|
|
1931
1930
|
try:
|
|
1932
|
-
agent_token_evt = cfg.get("agent_token", "")
|
|
1931
|
+
agent_token_evt = cfg.get("agent_token", "") or agent_token
|
|
1933
1932
|
headers = {"Content-Type": "application/json"}
|
|
1934
1933
|
if agent_token_evt:
|
|
1935
1934
|
headers["Authorization"] = f"Bearer {agent_token_evt}"
|
|
1936
|
-
elif api_key:
|
|
1937
|
-
headers["X-Api-Key"] = api_key
|
|
1938
1935
|
data = json.dumps(payload).encode()
|
|
1939
1936
|
req = urllib.request.Request(
|
|
1940
1937
|
f"{base_url}/guard/savings",
|
|
@@ -1963,13 +1960,13 @@ def cmd_guard_status(args):
|
|
|
1963
1960
|
cfg = _require_guard_config()
|
|
1964
1961
|
workspace_id = cfg.get("workspace_id")
|
|
1965
1962
|
user_email = cfg.get("user_email", "")
|
|
1966
|
-
|
|
1963
|
+
agent_token = cfg.get("agent_token", "")
|
|
1967
1964
|
base_url = _api_url(cfg)
|
|
1968
1965
|
|
|
1969
1966
|
# Auto-refresh user_email + clerk_user_id into config if missing
|
|
1970
|
-
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:
|
|
1971
1968
|
try:
|
|
1972
|
-
installed = _req("GET", f"{base_url}/guard/config/installed",
|
|
1969
|
+
installed = _req("GET", f"{base_url}/guard/config/installed", token=agent_token or None)
|
|
1973
1970
|
fetched_email = installed.get("user_email") or ""
|
|
1974
1971
|
fetched_clerk = installed.get("clerk_user_id") or ""
|
|
1975
1972
|
if fetched_email:
|
|
@@ -1993,7 +1990,7 @@ def cmd_guard_status(args):
|
|
|
1993
1990
|
spend = _req(
|
|
1994
1991
|
"GET",
|
|
1995
1992
|
f"{base_url}/guard/spend?workspace_id={workspace_id}",
|
|
1996
|
-
|
|
1993
|
+
token=agent_token or None,
|
|
1997
1994
|
)
|
|
1998
1995
|
except SystemExit:
|
|
1999
1996
|
pass
|
|
@@ -2013,7 +2010,7 @@ def cmd_guard_status(args):
|
|
|
2013
2010
|
f"&since={today_iso}"
|
|
2014
2011
|
f"&limit=20"
|
|
2015
2012
|
),
|
|
2016
|
-
|
|
2013
|
+
token=agent_token or None,
|
|
2017
2014
|
)
|
|
2018
2015
|
if not isinstance(events, list):
|
|
2019
2016
|
events = events.get("events", [])
|
|
@@ -2094,14 +2091,14 @@ def cmd_guard_status(args):
|
|
|
2094
2091
|
def cmd_guard_savings(args):
|
|
2095
2092
|
cfg = _require_guard_config()
|
|
2096
2093
|
workspace_id = cfg.get("workspace_id")
|
|
2097
|
-
|
|
2094
|
+
agent_token = cfg.get("agent_token", "")
|
|
2098
2095
|
base_url = _api_url(cfg)
|
|
2099
2096
|
|
|
2100
2097
|
try:
|
|
2101
2098
|
data = _req(
|
|
2102
2099
|
"GET",
|
|
2103
2100
|
f"{base_url}/guard/savings/team-summary?workspace_id={workspace_id}",
|
|
2104
|
-
|
|
2101
|
+
token=agent_token or None,
|
|
2105
2102
|
)
|
|
2106
2103
|
except Exception:
|
|
2107
2104
|
print(f"{RED}Failed to fetch team savings.{RESET}")
|
|
@@ -2112,9 +2109,11 @@ def cmd_guard_savings(args):
|
|
|
2112
2109
|
|
|
2113
2110
|
dev_count = data.get("developer_count", 0)
|
|
2114
2111
|
total_tok = data.get("total_tokens_saved", 0)
|
|
2112
|
+
total_usd = data.get("total_cost_saved_usd", 0.0)
|
|
2115
2113
|
per_day = data.get("per_day_usd", 0.0)
|
|
2116
2114
|
per_month = data.get("per_month_usd", 0.0)
|
|
2117
2115
|
per_year = data.get("per_year_usd", 0.0)
|
|
2116
|
+
days_obs = data.get("days_observed", 1)
|
|
2118
2117
|
tools = data.get("tools_installed", [])
|
|
2119
2118
|
avg_tok = total_tok // dev_count if dev_count else 0
|
|
2120
2119
|
avg_day_usd = round(per_day / dev_count, 2) if dev_count else 0.0
|
|
@@ -2122,8 +2121,8 @@ def cmd_guard_savings(args):
|
|
|
2122
2121
|
print()
|
|
2123
2122
|
print(f"{BOLD}Team Token Savings{RESET} ({dev_count} developer{'s' if dev_count != 1 else ''})")
|
|
2124
2123
|
print("─" * 52)
|
|
2125
|
-
print(f" Total tokens saved: {total_tok:>14,}")
|
|
2126
|
-
print(f"
|
|
2124
|
+
print(f" Total tokens saved: {total_tok:>14,} (${total_usd:,.2f} over {days_obs} day{'s' if days_obs != 1 else ''})")
|
|
2125
|
+
print(f" Daily rate: ${per_day:>8.2f}/day · projected ${per_month:,.0f}/month · ${per_year:,.0f}/year")
|
|
2127
2126
|
if dev_count:
|
|
2128
2127
|
print(f" Avg per developer: {avg_tok:>14,} tokens · ${avg_day_usd:.2f}/day")
|
|
2129
2128
|
if tools:
|
|
@@ -2161,7 +2160,7 @@ def cmd_guard_audit(args):
|
|
|
2161
2160
|
cfg = _require_guard_config()
|
|
2162
2161
|
workspace_id = cfg.get("workspace_id")
|
|
2163
2162
|
user_email = cfg.get("user_email", "")
|
|
2164
|
-
|
|
2163
|
+
agent_token = cfg.get("agent_token", "")
|
|
2165
2164
|
base_url = _api_url(cfg)
|
|
2166
2165
|
|
|
2167
2166
|
since_str = getattr(args, "since", None) or "24h"
|
|
@@ -2176,7 +2175,7 @@ def cmd_guard_audit(args):
|
|
|
2176
2175
|
f"&since={since_iso}"
|
|
2177
2176
|
f"&limit=50"
|
|
2178
2177
|
),
|
|
2179
|
-
|
|
2178
|
+
token=agent_token or None,
|
|
2180
2179
|
)
|
|
2181
2180
|
events = events_resp if isinstance(events_resp, list) else events_resp.get("events", [])
|
|
2182
2181
|
|
|
@@ -2486,7 +2485,7 @@ def _watch_loop():
|
|
|
2486
2485
|
c = _cfg()
|
|
2487
2486
|
api_url = _api_url(c)
|
|
2488
2487
|
token = c.get("agent_token", "")
|
|
2489
|
-
|
|
2488
|
+
agent_token = c.get("agent_token", "")
|
|
2490
2489
|
agents = []
|
|
2491
2490
|
for name, config_path, mcp_key in _discover_config_agents():
|
|
2492
2491
|
agents.append({
|
|
@@ -2506,7 +2505,7 @@ def _watch_loop():
|
|
|
2506
2505
|
pass
|
|
2507
2506
|
_req("POST", f"{api_url}/guard/discover/scan",
|
|
2508
2507
|
body={"triggered_by": "watch", "agents": agents},
|
|
2509
|
-
token=token
|
|
2508
|
+
token=token or agent_token)
|
|
2510
2509
|
except Exception:
|
|
2511
2510
|
pass
|
|
2512
2511
|
time.sleep(_WATCH_INTERVAL)
|
|
@@ -2662,7 +2661,6 @@ def cmd_guard_discover(args):
|
|
|
2662
2661
|
cfg = _load_guard_config()
|
|
2663
2662
|
api_url = _api_url(cfg)
|
|
2664
2663
|
token = cfg.get("agent_token", "")
|
|
2665
|
-
api_key = cfg.get("api_key", "")
|
|
2666
2664
|
hdrs = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
2667
2665
|
|
|
2668
2666
|
# Config file scan — detect AI tools from known config dirs
|
|
@@ -2722,7 +2720,7 @@ def cmd_guard_discover(args):
|
|
|
2722
2720
|
"proxy_routed": t.get("proxy_routed", False),
|
|
2723
2721
|
})
|
|
2724
2722
|
payload = {"triggered_by": "cli", "agents": agents_payload}
|
|
2725
|
-
_req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token
|
|
2723
|
+
_req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token)
|
|
2726
2724
|
except SystemExit:
|
|
2727
2725
|
pass # 401/network errors — local output still useful
|
|
2728
2726
|
|
|
@@ -2936,14 +2934,14 @@ def cmd_verify(args) -> None:
|
|
|
2936
2934
|
|
|
2937
2935
|
cfg = _require_guard_config()
|
|
2938
2936
|
workspace_id = cfg.get("workspace_id")
|
|
2939
|
-
|
|
2937
|
+
agent_token = cfg.get("agent_token", "")
|
|
2940
2938
|
base_url = _api_url(cfg)
|
|
2941
2939
|
|
|
2942
2940
|
# ── fetch evidence from API ───────────────────────────────────────────────
|
|
2943
2941
|
ev = _req(
|
|
2944
2942
|
"GET",
|
|
2945
2943
|
f"{base_url}/guard/verify/evidence?workspace_id={workspace_id}",
|
|
2946
|
-
|
|
2944
|
+
token=agent_token or None,
|
|
2947
2945
|
)
|
|
2948
2946
|
|
|
2949
2947
|
grade = ev.get("grade", "F")
|
|
@@ -2960,7 +2958,7 @@ def cmd_verify(args) -> None:
|
|
|
2960
2958
|
run_result = _req(
|
|
2961
2959
|
"POST",
|
|
2962
2960
|
f"{base_url}/guard/verify/run?workspace_id={workspace_id}",
|
|
2963
|
-
|
|
2961
|
+
token=agent_token or None,
|
|
2964
2962
|
)
|
|
2965
2963
|
if fmt == "json":
|
|
2966
2964
|
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",
|
|
@@ -109,12 +109,10 @@ def _resolve(args, key: str, config_key=None):
|
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def _require_auth(args):
|
|
112
|
-
"""Return (server, workspace_id,
|
|
112
|
+
"""Return (server, workspace_id, token) — exit if not configured."""
|
|
113
113
|
cfg = _load_config()
|
|
114
114
|
server = _resolve(args, "server") or cfg.get("api_url", "")
|
|
115
115
|
workspace = _resolve(args, "workspace") or cfg.get("workspace_id") or cfg.get("workspace")
|
|
116
|
-
api_key = _resolve(args, "api_key", "api_key")
|
|
117
|
-
# agent_token (new) takes precedence over legacy token
|
|
118
116
|
token = _resolve(args, "token") or cfg.get("agent_token")
|
|
119
117
|
|
|
120
118
|
if not server:
|
|
@@ -123,23 +121,21 @@ def _require_auth(args):
|
|
|
123
121
|
if not workspace:
|
|
124
122
|
print(f"{RED}No workspace set. Run: conduct login{RESET}")
|
|
125
123
|
sys.exit(1)
|
|
126
|
-
if not
|
|
124
|
+
if not token:
|
|
127
125
|
print(f"{RED}Not authenticated. Run: conduct login{RESET}")
|
|
128
126
|
sys.exit(1)
|
|
129
127
|
|
|
130
|
-
return server.rstrip("/"), workspace,
|
|
128
|
+
return server.rstrip("/"), workspace, token
|
|
131
129
|
|
|
132
130
|
|
|
133
131
|
# ── Stream helper ─────────────────────────────────────────────────────────────
|
|
134
132
|
|
|
135
|
-
def _stream_run(server: str, workflow_id: str, run_id: str, workspace_id: str, token=None
|
|
136
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
133
|
+
def _stream_run(server: str, workflow_id: str, run_id: str, workspace_id: str, token=None) -> bool:
|
|
134
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
137
135
|
# SSE endpoint reads auth from query params (EventSource can't set headers)
|
|
138
136
|
qs_parts = [f"workspace_id={workspace_id}"]
|
|
139
137
|
if token:
|
|
140
138
|
qs_parts.append(f"token={token}")
|
|
141
|
-
if api_key:
|
|
142
|
-
qs_parts.append(f"api_key={api_key}")
|
|
143
139
|
url = f"{server}/workflows/{workflow_id}/runs/{run_id}/stream?{'&'.join(qs_parts)}"
|
|
144
140
|
|
|
145
141
|
for data in api.stream(url, hdrs):
|
|
@@ -348,7 +344,7 @@ def _report_tool_coverage() -> None:
|
|
|
348
344
|
try:
|
|
349
345
|
cfg = _load_config()
|
|
350
346
|
server = cfg.get("server", "").rstrip("/")
|
|
351
|
-
api_key = cfg.get("
|
|
347
|
+
api_key = cfg.get("agent_token", "")
|
|
352
348
|
token = cfg.get("token", "")
|
|
353
349
|
email = cfg.get("email", "")
|
|
354
350
|
|
|
@@ -363,7 +359,7 @@ def _report_tool_coverage() -> None:
|
|
|
363
359
|
payload = json.dumps({"email": email, "tools": tools}).encode()
|
|
364
360
|
headers = {"Content-Type": "application/json"}
|
|
365
361
|
if api_key and api_key.startswith("cond_live_"):
|
|
366
|
-
headers["
|
|
362
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
367
363
|
elif token:
|
|
368
364
|
headers["Authorization"] = f"Bearer {token}"
|
|
369
365
|
req = urllib.request.Request(
|
|
@@ -594,8 +590,8 @@ def cmd_login(args):
|
|
|
594
590
|
|
|
595
591
|
|
|
596
592
|
def cmd_agents(args):
|
|
597
|
-
server, workspace_id,
|
|
598
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
593
|
+
server, workspace_id, token = _require_auth(args)
|
|
594
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
599
595
|
|
|
600
596
|
project_filter = getattr(args, "project", None)
|
|
601
597
|
url = f"{server}/workflows"
|
|
@@ -638,8 +634,8 @@ def cmd_agents(args):
|
|
|
638
634
|
|
|
639
635
|
|
|
640
636
|
def cmd_test(args):
|
|
641
|
-
server, workspace_id,
|
|
642
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
637
|
+
server, workspace_id, token = _require_auth(args)
|
|
638
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
643
639
|
|
|
644
640
|
agent_names = args.agents
|
|
645
641
|
run_all = getattr(args, "all", False)
|
|
@@ -717,10 +713,10 @@ def cmd_test(args):
|
|
|
717
713
|
})
|
|
718
714
|
return payload
|
|
719
715
|
|
|
720
|
-
_run_tests(server, workspace_id,
|
|
716
|
+
_run_tests(server, workspace_id, token, hdrs, targets, _build_payload, parallel=parallel)
|
|
721
717
|
|
|
722
718
|
|
|
723
|
-
def _run_tests(server, workspace_id,
|
|
719
|
+
def _run_tests(server, workspace_id, token, hdrs, targets, build_payload, *, parallel: bool = False):
|
|
724
720
|
import threading
|
|
725
721
|
|
|
726
722
|
if not parallel:
|
|
@@ -739,7 +735,7 @@ def _run_tests(server, workspace_id, api_key, token, hdrs, targets, build_payloa
|
|
|
739
735
|
run_id = run.get("run_id")
|
|
740
736
|
print(f" {GRAY}run: {run_id}{RESET}")
|
|
741
737
|
try:
|
|
742
|
-
ok = _stream_run(server, wf_id, run_id, workspace_id, token
|
|
738
|
+
ok = _stream_run(server, wf_id, run_id, workspace_id, token)
|
|
743
739
|
except Exception:
|
|
744
740
|
ok = _poll_run(server, wf_id, run_id, hdrs)
|
|
745
741
|
results.append((name, ok, run_id))
|
|
@@ -824,8 +820,8 @@ def _resolve_environment(server: str, workspace_id: str, hdrs: dict, name: str)
|
|
|
824
820
|
# ── Environment commands ──────────────────────────────────────────────────────
|
|
825
821
|
|
|
826
822
|
def cmd_environments(args):
|
|
827
|
-
server, workspace_id,
|
|
828
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
823
|
+
server, workspace_id, token = _require_auth(args)
|
|
824
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
829
825
|
envs = _list_environments(server, workspace_id, hdrs)
|
|
830
826
|
|
|
831
827
|
if not envs:
|
|
@@ -840,8 +836,8 @@ def cmd_environments(args):
|
|
|
840
836
|
|
|
841
837
|
|
|
842
838
|
def cmd_credentials(args):
|
|
843
|
-
server, workspace_id,
|
|
844
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
839
|
+
server, workspace_id, token = _require_auth(args)
|
|
840
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
845
841
|
env = _resolve_environment(server, workspace_id, hdrs, args.environment)
|
|
846
842
|
|
|
847
843
|
rows = api.req("GET", f"{server}/credentials/env-vars/{env['id']}", hdrs)
|
|
@@ -862,8 +858,8 @@ def cmd_credentials(args):
|
|
|
862
858
|
print()
|
|
863
859
|
|
|
864
860
|
|
|
865
|
-
def _do_set_credential(server, workspace_id,
|
|
866
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
861
|
+
def _do_set_credential(server, workspace_id, token, env_name, key, value):
|
|
862
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
867
863
|
env = _resolve_environment(server, workspace_id, hdrs, env_name)
|
|
868
864
|
|
|
869
865
|
existing = api.req("GET", f"{server}/credentials/env-vars/{env['id']}", hdrs)
|
|
@@ -875,8 +871,8 @@ def _do_set_credential(server, workspace_id, api_key, token, env_name, key, valu
|
|
|
875
871
|
print(f"{GREEN}✓ {key}{RESET} set in environment '{env_name}' {GRAY}({masked}){RESET}")
|
|
876
872
|
|
|
877
873
|
|
|
878
|
-
def _do_delete_credential(server, workspace_id,
|
|
879
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
874
|
+
def _do_delete_credential(server, workspace_id, token, env_name, key, yes):
|
|
875
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
880
876
|
env = _resolve_environment(server, workspace_id, hdrs, env_name)
|
|
881
877
|
|
|
882
878
|
existing = api.req("GET", f"{server}/credentials/env-vars/{env['id']}", hdrs)
|
|
@@ -898,12 +894,12 @@ def _do_delete_credential(server, workspace_id, api_key, token, env_name, key, y
|
|
|
898
894
|
|
|
899
895
|
def cmd_set(args):
|
|
900
896
|
if args.set_command == "credential":
|
|
901
|
-
server, workspace_id,
|
|
902
|
-
_do_set_credential(server, workspace_id,
|
|
897
|
+
server, workspace_id, token = _require_auth(args)
|
|
898
|
+
_do_set_credential(server, workspace_id, token,
|
|
903
899
|
args.environment, args.key, args.value)
|
|
904
900
|
elif args.set_command == "environment":
|
|
905
|
-
server, workspace_id,
|
|
906
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
901
|
+
server, workspace_id, token = _require_auth(args)
|
|
902
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
907
903
|
|
|
908
904
|
workflows = api.req("GET", f"{server}/workflows", hdrs)
|
|
909
905
|
wf = next((w for w in workflows if w["name"].lower() == args.agent.lower()), None)
|
|
@@ -936,8 +932,8 @@ def _resolve_project(server: str, workspace_id: str, hdrs: dict, name: str) -> d
|
|
|
936
932
|
|
|
937
933
|
|
|
938
934
|
def cmd_projects(args):
|
|
939
|
-
server, workspace_id,
|
|
940
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
935
|
+
server, workspace_id, token = _require_auth(args)
|
|
936
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
941
937
|
projects = _list_projects(server, workspace_id, hdrs)
|
|
942
938
|
|
|
943
939
|
if not projects:
|
|
@@ -953,8 +949,8 @@ def cmd_projects(args):
|
|
|
953
949
|
|
|
954
950
|
|
|
955
951
|
def cmd_create(args):
|
|
956
|
-
server, workspace_id,
|
|
957
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
952
|
+
server, workspace_id, token = _require_auth(args)
|
|
953
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
958
954
|
parts = args.create_args
|
|
959
955
|
|
|
960
956
|
if parts and parts[0] == "environment":
|
|
@@ -975,8 +971,8 @@ def cmd_create(args):
|
|
|
975
971
|
|
|
976
972
|
|
|
977
973
|
def cmd_delete(args):
|
|
978
|
-
server, workspace_id,
|
|
979
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
974
|
+
server, workspace_id, token = _require_auth(args)
|
|
975
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
980
976
|
parts = args.delete_args
|
|
981
977
|
|
|
982
978
|
if parts and parts[0] == "environment":
|
|
@@ -999,7 +995,7 @@ def cmd_delete(args):
|
|
|
999
995
|
if not env_name or not key:
|
|
1000
996
|
print(f"{RED}Usage: conduct delete credential --environment <name> --key <KEY>{RESET}")
|
|
1001
997
|
sys.exit(1)
|
|
1002
|
-
_do_delete_credential(server, workspace_id,
|
|
998
|
+
_do_delete_credential(server, workspace_id, token, env_name, key, args.yes)
|
|
1003
999
|
|
|
1004
1000
|
else:
|
|
1005
1001
|
# conduct delete [project] <name> [--yes] [--purge]
|
|
@@ -1035,8 +1031,8 @@ def cmd_delete(args):
|
|
|
1035
1031
|
# ── Playbook commands ─────────────────────────────────────────────────────────
|
|
1036
1032
|
|
|
1037
1033
|
def cmd_playbooks(args):
|
|
1038
|
-
server, workspace_id,
|
|
1039
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
1034
|
+
server, workspace_id, token = _require_auth(args)
|
|
1035
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
1040
1036
|
slug = getattr(args, "slug", None)
|
|
1041
1037
|
|
|
1042
1038
|
if slug:
|
|
@@ -1080,8 +1076,8 @@ def cmd_playbooks(args):
|
|
|
1080
1076
|
# ── Install command ───────────────────────────────────────────────────────────
|
|
1081
1077
|
|
|
1082
1078
|
def cmd_install(args):
|
|
1083
|
-
server, workspace_id,
|
|
1084
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
1079
|
+
server, workspace_id, token = _require_auth(args)
|
|
1080
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
1085
1081
|
|
|
1086
1082
|
slug = args.slug
|
|
1087
1083
|
|
|
@@ -1181,8 +1177,8 @@ def cmd_install(args):
|
|
|
1181
1177
|
# ── Reset command ─────────────────────────────────────────────────────────────
|
|
1182
1178
|
|
|
1183
1179
|
def cmd_reset(args):
|
|
1184
|
-
server, workspace_id,
|
|
1185
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
1180
|
+
server, workspace_id, token = _require_auth(args)
|
|
1181
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
1186
1182
|
proj = _resolve_project(server, workspace_id, hdrs, args.name)
|
|
1187
1183
|
project_id = proj["id"]
|
|
1188
1184
|
|
|
@@ -1268,8 +1264,8 @@ _FRIENDLY_NAMES = {
|
|
|
1268
1264
|
|
|
1269
1265
|
|
|
1270
1266
|
def cmd_install_all(args):
|
|
1271
|
-
server, workspace_id,
|
|
1272
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
1267
|
+
server, workspace_id, token = _require_auth(args)
|
|
1268
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
1273
1269
|
|
|
1274
1270
|
slugs = _ALL_SLUGS
|
|
1275
1271
|
|
|
@@ -1361,16 +1357,16 @@ def _atomic_write(path: Path, data: dict) -> None:
|
|
|
1361
1357
|
def cmd_switch(args):
|
|
1362
1358
|
cfg = _load_config()
|
|
1363
1359
|
server = cfg.get("server", "").rstrip("/")
|
|
1364
|
-
api_key = cfg.get("
|
|
1360
|
+
api_key = cfg.get("agent_token", "")
|
|
1365
1361
|
token = cfg.get("token", "")
|
|
1366
1362
|
|
|
1367
1363
|
if not server or (not api_key and not token):
|
|
1368
|
-
print(f"{RED}Not logged in. Run: conduct login
|
|
1364
|
+
print(f"{RED}Not logged in. Run: conduct login{RESET}")
|
|
1369
1365
|
sys.exit(1)
|
|
1370
1366
|
|
|
1371
1367
|
hdrs = {"Content-Type": "application/json"}
|
|
1372
1368
|
if api_key:
|
|
1373
|
-
hdrs["
|
|
1369
|
+
hdrs["Authorization"] = f"Bearer {api_key}"
|
|
1374
1370
|
elif token:
|
|
1375
1371
|
hdrs["Authorization"] = f"Bearer {token}"
|
|
1376
1372
|
|
|
@@ -1433,7 +1429,7 @@ def cmd_switch(args):
|
|
|
1433
1429
|
policy = _guard._req(
|
|
1434
1430
|
"GET",
|
|
1435
1431
|
f"{server}/guard/policies/sync?workspace_id={new_id}",
|
|
1436
|
-
|
|
1432
|
+
token=token,
|
|
1437
1433
|
)
|
|
1438
1434
|
_guard._save_policy(policy)
|
|
1439
1435
|
rule_count = len(policy.get("rules", []))
|
|
@@ -1451,13 +1447,13 @@ def cmd_whoami(args):
|
|
|
1451
1447
|
|
|
1452
1448
|
workspace_id = cfg.get("workspace", "")
|
|
1453
1449
|
server = cfg.get("server", "—")
|
|
1454
|
-
api_key = cfg.get("
|
|
1450
|
+
api_key = cfg.get("agent_token", "")
|
|
1455
1451
|
|
|
1456
1452
|
# Try to resolve workspace name from /projects
|
|
1457
1453
|
workspace_name = ""
|
|
1458
1454
|
if workspace_id and server != "—" and api_key:
|
|
1459
1455
|
try:
|
|
1460
|
-
hdrs = {"Content-Type": "application/json", "
|
|
1456
|
+
hdrs = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
|
1461
1457
|
projects = api.req("GET", f"{server.rstrip('/')}/projects", hdrs)
|
|
1462
1458
|
match = next((p for p in projects if str(p.get("id", "")) == str(workspace_id)), None)
|
|
1463
1459
|
if match:
|
|
@@ -1804,12 +1800,12 @@ def _render_table(rows: list[dict]) -> str:
|
|
|
1804
1800
|
def _fetch_runs(cfg: dict) -> list[dict]:
|
|
1805
1801
|
"""Fetch recent runs from GET /runs."""
|
|
1806
1802
|
server = cfg.get("server", "").rstrip("/")
|
|
1807
|
-
api_key = cfg.get("
|
|
1803
|
+
api_key = cfg.get("agent_token", "")
|
|
1808
1804
|
ws_id = cfg.get("workspace", "")
|
|
1809
1805
|
if not all([server, api_key, ws_id]):
|
|
1810
1806
|
return []
|
|
1811
1807
|
try:
|
|
1812
|
-
hdrs = {"Content-Type": "application/json", "
|
|
1808
|
+
hdrs = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
|
1813
1809
|
data = api.req("GET", f"{server}/runs?limit=15&workspace_id={ws_id}", hdrs)
|
|
1814
1810
|
runs = data if isinstance(data, list) else data.get("runs", data.get("items", []))
|
|
1815
1811
|
return runs[:15]
|
|
@@ -1820,12 +1816,12 @@ def _fetch_runs(cfg: dict) -> list[dict]:
|
|
|
1820
1816
|
def _fetch_guard_activity(cfg: dict, guard_cfg: dict) -> list[dict]:
|
|
1821
1817
|
"""Fetch recent Guard events grouped by developer."""
|
|
1822
1818
|
server = cfg.get("server", "").rstrip("/")
|
|
1823
|
-
api_key = cfg.get("
|
|
1819
|
+
api_key = cfg.get("agent_token", "")
|
|
1824
1820
|
ws_id = cfg.get("workspace", "")
|
|
1825
1821
|
if not all([server, api_key, ws_id]):
|
|
1826
1822
|
return []
|
|
1827
1823
|
try:
|
|
1828
|
-
hdrs = {"Content-Type": "application/json", "
|
|
1824
|
+
hdrs = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
|
1829
1825
|
data = api.req("GET", f"{server}/guard/events?workspace_id={ws_id}&limit=200", hdrs)
|
|
1830
1826
|
events = data if isinstance(data, list) else data.get("events", [])
|
|
1831
1827
|
# Group by user_email
|
|
@@ -2280,8 +2276,8 @@ def cmd_session_report(args):
|
|
|
2280
2276
|
|
|
2281
2277
|
# ── 5. Push report to Guard dashboard ────────────────────────────────────
|
|
2282
2278
|
try:
|
|
2283
|
-
server, workspace_id,
|
|
2284
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
2279
|
+
server, workspace_id, token = _require_auth(args)
|
|
2280
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
2285
2281
|
import getpass as _getpass
|
|
2286
2282
|
email = getattr(args, "developer", None) or _getpass.getuser()
|
|
2287
2283
|
payload = _json.dumps({
|
|
@@ -2313,8 +2309,8 @@ def cmd_session_report(args):
|
|
|
2313
2309
|
|
|
2314
2310
|
def cmd_emit_finding(args):
|
|
2315
2311
|
"""POST a security finding to /security-findings."""
|
|
2316
|
-
server, workspace_id,
|
|
2317
|
-
hdrs = api.headers(workspace_id, token, "application/json"
|
|
2312
|
+
server, workspace_id, token = _require_auth(args)
|
|
2313
|
+
hdrs = api.headers(workspace_id, token, "application/json")
|
|
2318
2314
|
|
|
2319
2315
|
from_stdin = getattr(args, "from_stdin", False)
|
|
2320
2316
|
|
|
@@ -2408,8 +2404,8 @@ def _prompt_issue_choice(issues: list) -> dict:
|
|
|
2408
2404
|
|
|
2409
2405
|
|
|
2410
2406
|
def cmd_run(args):
|
|
2411
|
-
server, workspace_id,
|
|
2412
|
-
json_h = api.headers(workspace_id, token, "application/json"
|
|
2407
|
+
server, workspace_id, token = _require_auth(args)
|
|
2408
|
+
json_h = api.headers(workspace_id, token, "application/json")
|
|
2413
2409
|
|
|
2414
2410
|
# Fix 1: --input values go into state["inputs"], not top-level state.
|
|
2415
2411
|
# {{inputs.key}} refs in YAML only resolve when the executor finds state["inputs"].
|
|
@@ -2548,7 +2544,7 @@ def cmd_run(args):
|
|
|
2548
2544
|
body["__max_turns"] = suggested
|
|
2549
2545
|
run = api.req("POST", f"{server}/workflows/{workflow_id}/trigger", json_h, body)
|
|
2550
2546
|
run_id = run.get("run_id") or run.get("id")
|
|
2551
|
-
_stream_run(server, workflow_id, run_id, workspace_id, token
|
|
2547
|
+
_stream_run(server, workflow_id, run_id, workspace_id, token)
|
|
2552
2548
|
|
|
2553
2549
|
|
|
2554
2550
|
# ── conduct sync / test-guard / test-security ────────────────────────────────
|
|
@@ -2630,7 +2626,7 @@ def cmd_test_security(args):
|
|
|
2630
2626
|
cfg = {}
|
|
2631
2627
|
|
|
2632
2628
|
workspace_id = cfg.get("workspace_id")
|
|
2633
|
-
api_key = cfg.get("
|
|
2629
|
+
api_key = cfg.get("agent_token", "")
|
|
2634
2630
|
user_email = cfg.get("user_email", "")
|
|
2635
2631
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
2636
2632
|
|
|
@@ -2647,7 +2643,7 @@ def cmd_test_security(args):
|
|
|
2647
2643
|
try:
|
|
2648
2644
|
req = urllib.request.Request(
|
|
2649
2645
|
f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
|
|
2650
|
-
headers={"
|
|
2646
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
2651
2647
|
method="DELETE",
|
|
2652
2648
|
)
|
|
2653
2649
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
@@ -2678,7 +2674,7 @@ def cmd_test_security(args):
|
|
|
2678
2674
|
req = urllib.request.Request(
|
|
2679
2675
|
f"{api_url}/security-findings?workspace_id={workspace_id}",
|
|
2680
2676
|
data=payload,
|
|
2681
|
-
headers={"Content-Type": "application/json", "
|
|
2677
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
2682
2678
|
method="POST",
|
|
2683
2679
|
)
|
|
2684
2680
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
@@ -2707,7 +2703,7 @@ def cmd_test_security_verify(args):
|
|
|
2707
2703
|
cfg = {}
|
|
2708
2704
|
|
|
2709
2705
|
workspace_id = cfg.get("workspace_id")
|
|
2710
|
-
api_key = cfg.get("
|
|
2706
|
+
api_key = cfg.get("agent_token", "")
|
|
2711
2707
|
user_email = cfg.get("user_email", "")
|
|
2712
2708
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
2713
2709
|
|
|
@@ -2727,7 +2723,7 @@ def cmd_test_security_verify(args):
|
|
|
2727
2723
|
req = urllib.request.Request(
|
|
2728
2724
|
f"{api_url}/secure/refresh-automation?workspace_id={workspace_id}",
|
|
2729
2725
|
data=b"{}",
|
|
2730
|
-
headers={"Content-Type": "application/json", "
|
|
2726
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
2731
2727
|
method="POST",
|
|
2732
2728
|
)
|
|
2733
2729
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
@@ -2743,7 +2739,7 @@ def cmd_test_security_verify(args):
|
|
|
2743
2739
|
try:
|
|
2744
2740
|
req = urllib.request.Request(
|
|
2745
2741
|
f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
|
|
2746
|
-
headers={"
|
|
2742
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
2747
2743
|
method="DELETE",
|
|
2748
2744
|
)
|
|
2749
2745
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
@@ -2773,7 +2769,7 @@ def cmd_test_security_verify(args):
|
|
|
2773
2769
|
req = urllib.request.Request(
|
|
2774
2770
|
f"{api_url}/security-findings?workspace_id={workspace_id}",
|
|
2775
2771
|
data=payload,
|
|
2776
|
-
headers={"Content-Type": "application/json", "
|
|
2772
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
2777
2773
|
method="POST",
|
|
2778
2774
|
)
|
|
2779
2775
|
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
@@ -2799,7 +2795,7 @@ def cmd_test_security_verify(args):
|
|
|
2799
2795
|
try:
|
|
2800
2796
|
req = urllib.request.Request(
|
|
2801
2797
|
f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security&limit=100",
|
|
2802
|
-
headers={"
|
|
2798
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
2803
2799
|
)
|
|
2804
2800
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
2805
2801
|
findings = _json.loads(resp.read())
|
|
@@ -2890,7 +2886,7 @@ def cmd_test_guard(args):
|
|
|
2890
2886
|
sys.exit(0)
|
|
2891
2887
|
|
|
2892
2888
|
workspace_id = cfg.get("workspace_id")
|
|
2893
|
-
api_key = cfg.get("
|
|
2889
|
+
api_key = cfg.get("agent_token", "")
|
|
2894
2890
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
2895
2891
|
|
|
2896
2892
|
print(f"\n{BOLD}▶ conduct test-guard — {len(rules)} rule(s){RESET}\n")
|
|
@@ -2934,7 +2930,7 @@ def cmd_test_guard(args):
|
|
|
2934
2930
|
req = urllib.request.Request(
|
|
2935
2931
|
f"{api_url}/guard/events/test?workspace_id={workspace_id}",
|
|
2936
2932
|
data=payload,
|
|
2937
|
-
headers={"Content-Type": "application/json", "
|
|
2933
|
+
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
|
|
2938
2934
|
method="POST",
|
|
2939
2935
|
)
|
|
2940
2936
|
with urllib.request.urlopen(req, timeout=5):
|
|
@@ -3009,12 +3005,10 @@ def _check_guard_setup(command: str) -> None:
|
|
|
3009
3005
|
def cmd_skill(args):
|
|
3010
3006
|
"""conduct skill list | install <slug> | uninstall <slug>"""
|
|
3011
3007
|
skill_command = getattr(args, "skill_command", None)
|
|
3012
|
-
server, workspace,
|
|
3008
|
+
server, workspace, token = _require_auth(args)
|
|
3013
3009
|
|
|
3014
3010
|
headers = {"Content-Type": "application/json"}
|
|
3015
|
-
if
|
|
3016
|
-
headers["X-Api-Key"] = api_key
|
|
3017
|
-
elif token:
|
|
3011
|
+
if token:
|
|
3018
3012
|
headers["Authorization"] = f"Bearer {token}"
|
|
3019
3013
|
|
|
3020
3014
|
def _req(method, path, expect=None):
|
|
@@ -3086,7 +3080,6 @@ def main():
|
|
|
3086
3080
|
)
|
|
3087
3081
|
# Global overrides (optional — config file is preferred)
|
|
3088
3082
|
parser.add_argument("--server", help="API URL (default: from ~/.conduct/config.json)")
|
|
3089
|
-
parser.add_argument("--api-key", dest="api_key", help="CLI API key")
|
|
3090
3083
|
parser.add_argument("--token", help=argparse.SUPPRESS)
|
|
3091
3084
|
parser.add_argument("--workspace", help="Workspace ID")
|
|
3092
3085
|
|
|
@@ -294,7 +294,7 @@ def main() -> None:
|
|
|
294
294
|
cfg = _load_conduct_config()
|
|
295
295
|
server = cfg.get("server", "").rstrip("/")
|
|
296
296
|
workspace_id = cfg.get("workspace", "")
|
|
297
|
-
api_key = cfg.get("
|
|
297
|
+
api_key = cfg.get("agent_token")
|
|
298
298
|
token = cfg.get("token")
|
|
299
299
|
|
|
300
300
|
for raw in sys.stdin:
|
|
@@ -45,7 +45,7 @@ def post_session_to_api(session_id: str, transcript_path: str | None, repo: str
|
|
|
45
45
|
if not cfg:
|
|
46
46
|
return False
|
|
47
47
|
server = cfg.get("server", "").rstrip("/")
|
|
48
|
-
api_key = cfg.get("
|
|
48
|
+
api_key = cfg.get("agent_token", "")
|
|
49
49
|
workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
|
|
50
50
|
if not server or not workspace_id:
|
|
51
51
|
return False
|
|
@@ -96,7 +96,7 @@ def post_session_to_api(session_id: str, transcript_path: str | None, repo: str
|
|
|
96
96
|
data=payload,
|
|
97
97
|
headers={
|
|
98
98
|
"Content-Type": "application/json",
|
|
99
|
-
"
|
|
99
|
+
"Authorization": f"Bearer {api_key}",
|
|
100
100
|
"X-Workspace-ID": workspace_id,
|
|
101
101
|
},
|
|
102
102
|
method="POST",
|
|
@@ -116,7 +116,7 @@ def search_team_memory(query: str, repo: str | None = None, limit: int = 5) -> l
|
|
|
116
116
|
if not cfg:
|
|
117
117
|
return []
|
|
118
118
|
server = cfg.get("server", "").rstrip("/")
|
|
119
|
-
api_key = cfg.get("
|
|
119
|
+
api_key = cfg.get("agent_token", "")
|
|
120
120
|
workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
|
|
121
121
|
if not server or not workspace_id:
|
|
122
122
|
return []
|
|
@@ -130,7 +130,7 @@ def search_team_memory(query: str, repo: str | None = None, limit: int = 5) -> l
|
|
|
130
130
|
req = urllib.request.Request(
|
|
131
131
|
url,
|
|
132
132
|
headers={
|
|
133
|
-
"
|
|
133
|
+
"Authorization": f"Bearer {api_key}",
|
|
134
134
|
"X-Workspace-ID": workspace_id,
|
|
135
135
|
},
|
|
136
136
|
)
|
|
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
|