conduct-cli 0.14.5__tar.gz → 0.14.7__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.
Files changed (60) hide show
  1. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/PKG-INFO +1 -1
  2. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/pyproject.toml +1 -1
  3. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/guard.py +11 -10
  4. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/posttooluse.py +12 -2
  5. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/pretooluse.py +7 -4
  6. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  7. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
  8. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_advisory_and_hook.py +17 -14
  9. conduct_cli-0.14.7/tests/test_sync_credential_isolation.py +44 -0
  10. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/LICENSE +0 -0
  11. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/README.md +0 -0
  12. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/setup.cfg +0 -0
  13. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/setup.py +0 -0
  14. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/__init__.py +0 -0
  15. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/api.py +0 -0
  16. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/guard_policy.md +0 -0
  17. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/__init__.py +0 -0
  18. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/base.py +0 -0
  19. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/precompact.py +0 -0
  20. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_parser.py +0 -0
  21. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_report_push.py +0 -0
  22. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_start.py +0 -0
  23. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/stop.py +0 -0
  24. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/log_util.py +0 -0
  25. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/main.py +0 -0
  26. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/memory.py +0 -0
  27. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/paxel.py +0 -0
  28. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/tool_groups.py +0 -0
  29. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  30. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  31. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/requires.txt +0 -0
  32. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/top_level.txt +0 -0
  33. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_api_client.py +0 -0
  34. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_bash_operator_signature.py +0 -0
  35. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_bash_scan_target.py +0 -0
  36. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_blast_radius.py +0 -0
  37. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_cli_matrix.py +0 -0
  38. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_cmd_run.py +0 -0
  39. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_command_word_matcher.py +0 -0
  40. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_drain_daemon_health.py +0 -0
  41. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_approvals.py +0 -0
  42. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_policy.py +0 -0
  43. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_savings.py +0 -0
  44. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_status.py +0 -0
  45. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_dispatch.py +0 -0
  46. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_journal.py +0 -0
  47. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_receipt.py +0 -0
  48. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_install_all.py +0 -0
  49. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_instruction_files.py +0 -0
  50. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_journal_drain.py +0 -0
  51. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_log_util.py +0 -0
  52. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_pretooluse_approval_e2e.py +0 -0
  53. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_pretooluse_check_policy.py +0 -0
  54. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_proxy_coverage.py +0 -0
  55. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_proxy_env.py +0 -0
  56. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_report_tool_coverage.py +0 -0
  57. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_session_start.py +0 -0
  58. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_signed_policy.py +0 -0
  59. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_switch.py +0 -0
  60. {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_token_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.14.5
3
+ Version: 0.14.7
4
4
  Summary: Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.14.5"
7
+ version = "0.14.7"
8
8
  description = "Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1232,17 +1232,21 @@ def _detect_ai_tools() -> list[dict]:
1232
1232
  if codex_dir.exists():
1233
1233
  config = codex_dir / "config.toml"
1234
1234
  is_desktop = _check_toml_str(config, "[desktop]")
1235
+ # Codex Desktop uses the same provider configuration as Codex CLI.
1236
+ # Do not report it as partial when the Conduct provider is selected.
1237
+ codex_proxy = _check_toml_str(config, 'model_provider = "conduct"')
1238
+ codex_mcp = _check_toml_str(config, "mcp_servers.conduct") or _check_toml_str(config, "conduct-mcp")
1235
1239
  if is_desktop:
1236
1240
  tools.append({
1237
1241
  "name": "codex-desktop",
1238
- "mcp_registered": _check_toml_str(config, "conduct-mcp"),
1242
+ "mcp_registered": codex_mcp,
1239
1243
  "hook_registered": _check_toml_str(config, "conductguard") or _check_toml_str(config, "conduct"),
1240
- "proxy_routed": False,
1244
+ "proxy_routed": codex_proxy,
1241
1245
  })
1242
1246
  else:
1243
1247
  tools.append({
1244
1248
  "name": "codex",
1245
- "mcp_registered": _check_toml_str(config, "conduct-mcp"),
1249
+ "mcp_registered": codex_mcp,
1246
1250
  "hook_registered": _check_toml_str(config, "conductguard") or _check_toml_str(config, "conduct"),
1247
1251
  "proxy_routed": _is_openai_proxied(),
1248
1252
  })
@@ -1546,26 +1550,23 @@ def cmd_guard_sync(args):
1546
1550
  _save_policy(policy)
1547
1551
  print(f" {GREEN}Policy refreshed:{RESET} {rule_count} hook rule(s). Proxy and non-hook rules run server-side.")
1548
1552
 
1549
- # Refresh agent token from server via refresh_token grant.
1550
- # Never rely on the locally-cached value — it may be stale or missing.
1553
+ # Installation metadata is not a refresh grant. Keep the access/refresh pair
1554
+ # together; another OAuth client's token must never replace this session.
1551
1555
  try:
1552
1556
  installed = _req(
1553
1557
  "GET",
1554
1558
  f"{base_url}/guard/config/installed?workspace_id={workspace_id}",
1555
1559
  token=_token,
1556
1560
  )
1557
- fresh_agent_token = installed.get("agent_token") or ""
1558
- if fresh_agent_token:
1559
- cfg["agent_token"] = fresh_agent_token
1560
1561
  if installed.get("user_email"):
1561
1562
  cfg["user_email"] = installed["user_email"]
1562
1563
  if installed.get("clerk_user_id"):
1563
1564
  cfg["clerk_user_id"] = installed["clerk_user_id"]
1564
1565
  _save_guard_config(cfg)
1565
- if not fresh_agent_token and not cfg.get("agent_token"):
1566
+ if not cfg.get("agent_token"):
1566
1567
  print(f" {YELLOW}Warning: server returned no token — proxy env may be stale{RESET}")
1567
1568
  except Exception as e:
1568
- print(f" {YELLOW}Warning: could not refresh token ({e}) — using cached value{RESET}")
1569
+ print(f" {YELLOW}Warning: could not fetch installation metadata ({e}){RESET}")
1569
1570
 
1570
1571
  # Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
1571
1572
  # routes through Conduct Guard. Customer-overridable via --proxy-url or
@@ -236,8 +236,16 @@ def post_codex_main() -> None:
236
236
  time.sleep(2)
237
237
  transcript_path = args.get("transcript_path", "")
238
238
  tokens_in, tokens_out = _scan_codex_tokens(transcript_path)
239
- if tokens_in or tokens_out:
240
- _post_usage(args.get("session_id"), args.get("tool_name"), tokens_in, tokens_out, None, args.get("blast_radius"))
239
+ _post_usage(
240
+ args.get("session_id"),
241
+ args.get("tool_name") or "unknown",
242
+ tokens_in,
243
+ tokens_out,
244
+ None,
245
+ args.get("blast_radius"),
246
+ args.get("execution_status"),
247
+ args.get("result_summary"),
248
+ )
241
249
  sys.exit(0)
242
250
 
243
251
 
@@ -280,6 +288,8 @@ def main() -> None:
280
288
  "tool_name": tool_name,
281
289
  "transcript_path": transcript_path,
282
290
  "blast_radius": blast_radius,
291
+ "execution_status": execution_status,
292
+ "result_summary": result_summary,
283
293
  }))
284
294
  subprocess.Popen(
285
295
  [sys.executable, str(_this_file), "post-codex", str(pending)],
@@ -828,9 +828,12 @@ def main() -> None:
828
828
  sys.exit(2)
829
829
 
830
830
  decision = {"block": "blocked", "warn": "warned"}.get(action, "allowed")
831
- if action == "warn" and session_id and rule_id and _already_warned_this_session(session_id, rule_id):
832
- sys.exit(0)
833
- if action == "warn" and session_id and rule_id:
831
+ # Deduplicate the warning message, never the audit event.
832
+ suppress_warning = bool(
833
+ action == "warn" and session_id and rule_id
834
+ and _already_warned_this_session(session_id, rule_id)
835
+ )
836
+ if action == "warn" and session_id and rule_id and not suppress_warning:
834
837
  _record_session_warn(session_id, rule_id)
835
838
 
836
839
  # Pre-mint the audit-row id ONLY on block. That way the receipt URL
@@ -855,7 +858,7 @@ def main() -> None:
855
858
  print(msg)
856
859
  print(_boxed(msg, receipt_url), file=sys.stderr)
857
860
  sys.exit(2)
858
- if action == "warn":
861
+ if action == "warn" and not suppress_warning:
859
862
  print(f"[ConductGuard] {message}")
860
863
 
861
864
  sys.exit(0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.14.5
3
+ Version: 0.14.7
4
4
  Summary: Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: Apache-2.0
@@ -54,4 +54,5 @@ tests/test_report_tool_coverage.py
54
54
  tests/test_session_start.py
55
55
  tests/test_signed_policy.py
56
56
  tests/test_switch.py
57
+ tests/test_sync_credential_isolation.py
57
58
  tests/test_token_lifecycle.py
@@ -308,7 +308,8 @@ class TestWarnDedup:
308
308
  posted = self._run_warn(monkeypatch, "sess-abc")
309
309
  assert "warned" in posted
310
310
 
311
- def test_second_warn_same_session_skips(self, monkeypatch, tmp_path):
311
+ @pytest.mark.parametrize("tool", ["Bash", "exec_command", "apply_patch"])
312
+ def test_repeated_warn_records_every_call_but_prints_once(self, monkeypatch, capsys, tool):
312
313
  import conduct_cli.hooks.pretooluse as pt
313
314
  warns: dict[str, set] = {}
314
315
 
@@ -321,9 +322,6 @@ class TestWarnDedup:
321
322
  monkeypatch.setattr(pt, "_already_warned_this_session", _fake_already)
322
323
  monkeypatch.setattr(pt, "_record_session_warn", _fake_record)
323
324
 
324
- # First call records the warn
325
- _fake_record("sess-xyz", "no-echo")
326
-
327
325
  monkeypatch.setattr(pt, "check_policy", lambda tn, ti, **kw: ({}, "warn", "no-echo", "watch out"))
328
326
  monkeypatch.setattr(pt, "_get_fail_mode", lambda: "fail_open")
329
327
  monkeypatch.setattr(pt, "_get_advisory_mode", lambda: False)
@@ -332,16 +330,21 @@ class TestWarnDedup:
332
330
  monkeypatch.setattr(pt, "_maybe_sync_policy", lambda: None)
333
331
  monkeypatch.setattr(pt, "_should_periodic_flush", lambda: False)
334
332
 
335
- posted: list[str] = []
336
- monkeypatch.setattr(pt, "post_event", lambda tn, ti, d, r, m, s, **kw: posted.append(d))
337
- monkeypatch.setattr(sys, "stdin", StringIO(_hook_input(session="sess-xyz")))
338
-
339
- try:
340
- pt.main()
341
- except SystemExit:
342
- pass
343
-
344
- assert posted == [], "second warn same session should be skipped"
333
+ posted = []
334
+ monkeypatch.setattr(pt, "post_event", lambda tn, ti, d, r, m, s, **kw: posted.append((tn, ti, d, r, s)))
335
+ monkeypatch.setattr(pt, "record_hook_heartbeat", lambda *args: None)
336
+
337
+ for index, session in enumerate(["sess-xyz", "sess-xyz", "sess-new"]):
338
+ command = f"echo call-{index}"
339
+ monkeypatch.setattr(sys, "stdin", StringIO(_hook_input(tool=tool, cmd=command, session=session)))
340
+ with pytest.raises(SystemExit) as exc:
341
+ pt.main()
342
+ assert exc.value.code == 0
343
+ assert posted[-1] == (tool.lower(), {"command": command}, "warned", "no-echo", session)
344
+ assert len(posted) == index + 1
345
+ output = capsys.readouterr()
346
+ assert output.out == ("" if index == 1 else "[ConductGuard] watch out\n")
347
+ assert output.err == ""
345
348
 
346
349
 
347
350
  # ── advisory_mode mirrored on sync ────────────────────────────────────────────
@@ -0,0 +1,44 @@
1
+ """A metadata fetch must never swap out a client's access/refresh pair."""
2
+ from types import SimpleNamespace
3
+ from unittest.mock import Mock
4
+
5
+ import pytest
6
+
7
+ from conduct_cli import guard
8
+
9
+
10
+ def test_sync_preserves_credentials_when_install_status_returns_another_token(monkeypatch):
11
+ cfg = {
12
+ "agent_token": "cond_agt_current-session",
13
+ "refresh_token": "cond_ref_current-session",
14
+ "workspace_id": "11111111-1111-4111-8111-111111111111",
15
+ "token_expires_at": "2099-01-01T00:00:00+00:00",
16
+ }
17
+ original = cfg.copy()
18
+ monkeypatch.setattr(guard, "_proactive_token_refresh", lambda: None)
19
+ monkeypatch.setattr(guard, "_require_guard_config", lambda: cfg)
20
+ monkeypatch.setattr(guard, "_api_url", lambda _: "https://api.example.test")
21
+ monkeypatch.setattr(guard, "_check_and_upgrade_packages", lambda: None)
22
+ monkeypatch.setattr(guard, "_save_policy", lambda _: None)
23
+ request = Mock(side_effect=[{"rules": []}, {
24
+ "agent_token": "cond_agt_another-client",
25
+ "user_email": "synthetic@example.test",
26
+ "clerk_user_id": "synthetic",
27
+ }])
28
+ monkeypatch.setattr(guard, "_req", request)
29
+
30
+ class MetadataSaved(BaseException):
31
+ pass
32
+
33
+ def save(value):
34
+ for key, expected in original.items():
35
+ assert value[key] == expected
36
+ assert value["clerk_user_id"] == "synthetic"
37
+ raise MetadataSaved()
38
+
39
+ monkeypatch.setattr(guard, "_save_guard_config", save)
40
+ # Stop before machine configuration, process launches or external requests.
41
+ with pytest.raises(MetadataSaved):
42
+ guard.cmd_guard_sync(SimpleNamespace(dry_run=False))
43
+ assert request.call_count == 2
44
+ assert all(call.kwargs["token"] == original["agent_token"] for call in request.call_args_list)
File without changes
File without changes
File without changes
File without changes