conduct-cli 0.14.6__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.6 → conduct_cli-0.14.7}/PKG-INFO +1 -1
  2. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/pyproject.toml +1 -1
  3. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/guard.py +4 -7
  4. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/pretooluse.py +7 -4
  5. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  6. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
  7. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_advisory_and_hook.py +17 -14
  8. conduct_cli-0.14.7/tests/test_sync_credential_isolation.py +44 -0
  9. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/LICENSE +0 -0
  10. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/README.md +0 -0
  11. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/setup.cfg +0 -0
  12. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/setup.py +0 -0
  13. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/__init__.py +0 -0
  14. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/api.py +0 -0
  15. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/guard_policy.md +0 -0
  16. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/__init__.py +0 -0
  17. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/base.py +0 -0
  18. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/posttooluse.py +0 -0
  19. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/precompact.py +0 -0
  20. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_parser.py +0 -0
  21. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_report_push.py +0 -0
  22. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_start.py +0 -0
  23. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/stop.py +0 -0
  24. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/log_util.py +0 -0
  25. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/main.py +0 -0
  26. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/memory.py +0 -0
  27. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/paxel.py +0 -0
  28. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/tool_groups.py +0 -0
  29. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  30. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  31. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/requires.txt +0 -0
  32. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/top_level.txt +0 -0
  33. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_api_client.py +0 -0
  34. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_bash_operator_signature.py +0 -0
  35. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_bash_scan_target.py +0 -0
  36. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_blast_radius.py +0 -0
  37. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_cli_matrix.py +0 -0
  38. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_cmd_run.py +0 -0
  39. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_command_word_matcher.py +0 -0
  40. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_drain_daemon_health.py +0 -0
  41. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_approvals.py +0 -0
  42. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_policy.py +0 -0
  43. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_savings.py +0 -0
  44. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_status.py +0 -0
  45. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_dispatch.py +0 -0
  46. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_journal.py +0 -0
  47. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_receipt.py +0 -0
  48. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_install_all.py +0 -0
  49. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_instruction_files.py +0 -0
  50. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_journal_drain.py +0 -0
  51. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_log_util.py +0 -0
  52. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_pretooluse_approval_e2e.py +0 -0
  53. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_pretooluse_check_policy.py +0 -0
  54. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_proxy_coverage.py +0 -0
  55. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_proxy_env.py +0 -0
  56. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_report_tool_coverage.py +0 -0
  57. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_session_start.py +0 -0
  58. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_signed_policy.py +0 -0
  59. {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_switch.py +0 -0
  60. {conduct_cli-0.14.6 → 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.6
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.6"
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" }
@@ -1550,26 +1550,23 @@ def cmd_guard_sync(args):
1550
1550
  _save_policy(policy)
1551
1551
  print(f" {GREEN}Policy refreshed:{RESET} {rule_count} hook rule(s). Proxy and non-hook rules run server-side.")
1552
1552
 
1553
- # Refresh agent token from server via refresh_token grant.
1554
- # 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.
1555
1555
  try:
1556
1556
  installed = _req(
1557
1557
  "GET",
1558
1558
  f"{base_url}/guard/config/installed?workspace_id={workspace_id}",
1559
1559
  token=_token,
1560
1560
  )
1561
- fresh_agent_token = installed.get("agent_token") or ""
1562
- if fresh_agent_token:
1563
- cfg["agent_token"] = fresh_agent_token
1564
1561
  if installed.get("user_email"):
1565
1562
  cfg["user_email"] = installed["user_email"]
1566
1563
  if installed.get("clerk_user_id"):
1567
1564
  cfg["clerk_user_id"] = installed["clerk_user_id"]
1568
1565
  _save_guard_config(cfg)
1569
- if not fresh_agent_token and not cfg.get("agent_token"):
1566
+ if not cfg.get("agent_token"):
1570
1567
  print(f" {YELLOW}Warning: server returned no token — proxy env may be stale{RESET}")
1571
1568
  except Exception as e:
1572
- 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}")
1573
1570
 
1574
1571
  # Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
1575
1572
  # routes through Conduct Guard. Customer-overridable via --proxy-url or
@@ -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.6
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