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.
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/PKG-INFO +1 -1
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/pyproject.toml +1 -1
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/guard.py +4 -7
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/pretooluse.py +7 -4
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_advisory_and_hook.py +17 -14
- conduct_cli-0.14.7/tests/test_sync_credential_isolation.py +44 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/LICENSE +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/README.md +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/setup.cfg +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/setup.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_api_client.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_bash_scan_target.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_cli_matrix.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_approvals.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_guard_status.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_journal.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_hook_receipt.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_install_all.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_log_util.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_pretooluse_approval_e2e.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_pretooluse_check_policy.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_session_start.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.14.6 → conduct_cli-0.14.7}/tests/test_switch.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
#
|
|
1554
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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.
|
|
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
|
|
@@ -308,7 +308,8 @@ class TestWarnDedup:
|
|
|
308
308
|
posted = self._run_warn(monkeypatch, "sess-abc")
|
|
309
309
|
assert "warned" in posted
|
|
310
310
|
|
|
311
|
-
|
|
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
|
|
336
|
-
monkeypatch.setattr(pt, "post_event", lambda tn, ti, d, r, m, s, **kw: posted.append(d))
|
|
337
|
-
monkeypatch.setattr(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|