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.
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/PKG-INFO +1 -1
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/pyproject.toml +1 -1
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/guard.py +11 -10
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/posttooluse.py +12 -2
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/pretooluse.py +7 -4
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- {conduct_cli-0.14.5 → 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.5 → conduct_cli-0.14.7}/LICENSE +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/README.md +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/setup.cfg +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/setup.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_api_client.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_bash_scan_target.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_cli_matrix.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_approvals.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_guard_status.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_journal.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_hook_receipt.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_install_all.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_log_util.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_pretooluse_approval_e2e.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_pretooluse_check_policy.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_session_start.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.14.5 → conduct_cli-0.14.7}/tests/test_switch.py +0 -0
- {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.
|
|
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" }
|
|
@@ -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":
|
|
1242
|
+
"mcp_registered": codex_mcp,
|
|
1239
1243
|
"hook_registered": _check_toml_str(config, "conductguard") or _check_toml_str(config, "conduct"),
|
|
1240
|
-
"proxy_routed":
|
|
1244
|
+
"proxy_routed": codex_proxy,
|
|
1241
1245
|
})
|
|
1242
1246
|
else:
|
|
1243
1247
|
tools.append({
|
|
1244
1248
|
"name": "codex",
|
|
1245
|
-
"mcp_registered":
|
|
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
|
-
#
|
|
1550
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|