conduct-cli 0.9.8__tar.gz → 0.9.9__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 (54) hide show
  1. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/PKG-INFO +1 -1
  2. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/pyproject.toml +1 -1
  3. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/main.py +33 -0
  4. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  5. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_switch.py +93 -0
  6. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/README.md +0 -0
  7. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/setup.cfg +0 -0
  8. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/setup.py +0 -0
  9. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/__init__.py +0 -0
  10. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/api.py +0 -0
  11. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guard.py +0 -0
  12. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guard_policy.md +0 -0
  13. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guardmcp.py +0 -0
  14. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/__init__.py +0 -0
  15. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/base.py +0 -0
  16. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/posttooluse.py +0 -0
  17. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/precompact.py +0 -0
  18. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/pretooluse.py +0 -0
  19. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_parser.py +0 -0
  20. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_report_push.py +0 -0
  21. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_start.py +0 -0
  22. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/stop.py +0 -0
  23. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/log_util.py +0 -0
  24. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/mcp_server.py +0 -0
  25. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/memory.py +0 -0
  26. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/paxel.py +0 -0
  27. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/tool_groups.py +0 -0
  28. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  29. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  30. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  31. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/requires.txt +0 -0
  32. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/top_level.txt +0 -0
  33. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_advisory_and_hook.py +0 -0
  34. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_api_client.py +0 -0
  35. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_bash_operator_signature.py +0 -0
  36. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_bash_scan_target.py +0 -0
  37. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_blast_radius.py +0 -0
  38. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_cli_matrix.py +0 -0
  39. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_cmd_run.py +0 -0
  40. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_command_word_matcher.py +0 -0
  41. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_conductguard_mcp_stdio.py +0 -0
  42. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_drain_daemon_health.py +0 -0
  43. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_approvals.py +0 -0
  44. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_policy.py +0 -0
  45. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_savings.py +0 -0
  46. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_hook_dispatch.py +0 -0
  47. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_instruction_files.py +0 -0
  48. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_journal_drain.py +0 -0
  49. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_log_util.py +0 -0
  50. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_proxy_coverage.py +0 -0
  51. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_proxy_env.py +0 -0
  52. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_report_tool_coverage.py +0 -0
  53. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_signed_policy.py +0 -0
  54. {conduct_cli-0.9.8 → conduct_cli-0.9.9}/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.9.8
3
+ Version: 0.9.9
4
4
  Summary: CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.9.8"
7
+ version = "0.9.9"
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" }
@@ -552,6 +552,10 @@ def cmd_login(args):
552
552
  api_url = (getattr(args, "server", None) or _load_config().get("api_url", _DEFAULT_API_URL)).rstrip("/")
553
553
  web_url = _DEFAULT_WEB_URL
554
554
 
555
+ # Capture the workspace the user was on BEFORE login — so we can restore
556
+ # it after the login flow (which always returns the account's default).
557
+ prior_ws = _load_config().get("workspace_id") or _load_config().get("workspace") or ""
558
+
555
559
  # --token escape hatch: paste agent_token directly (no browser)
556
560
  manual_token = getattr(args, "token", None)
557
561
  if manual_token:
@@ -576,6 +580,28 @@ def cmd_login(args):
576
580
  cfg.pop("api_key", None)
577
581
  _atomic_write(CONFIG_PATH, cfg)
578
582
 
583
+ # Restore prior workspace if login returned a different default. Otherwise
584
+ # every `conduct login` silently blows away the last `conduct switch`.
585
+ if prior_ws and str(prior_ws) != str(result["workspace_id"]):
586
+ try:
587
+ _hdrs_restore = {"Authorization": f"Bearer {result['agent_token']}", "Content-Type": "application/json"}
588
+ _r = api.req("POST", f"{api_url}/auth/switch-workspace", _hdrs_restore, {"workspace_id": prior_ws})
589
+ if _r and _r.get("agent_token"):
590
+ cfg["agent_token"] = _r["agent_token"]
591
+ if _r.get("refresh_token"):
592
+ cfg["refresh_token"] = _r["refresh_token"]
593
+ cfg["workspace"] = prior_ws
594
+ cfg["workspace_id"] = prior_ws
595
+ cfg["token_expires_at"] = (_dt.datetime.now(_dt.timezone.utc) + _dt.timedelta(seconds=int(_r.get("expires_in", 28800)))).isoformat()
596
+ _atomic_write(CONFIG_PATH, cfg)
597
+ # Update `result` so subsequent code (name display, sync) targets prior workspace
598
+ result["workspace_id"] = prior_ws
599
+ result["agent_token"] = cfg["agent_token"]
600
+ except SystemExit:
601
+ pass # not a member of prior workspace anymore, or endpoint missing — keep server default
602
+ except Exception:
603
+ pass
604
+
579
605
  # Fetch workspaces so we can display the current one by name and nudge
580
606
  # the user if their account has more than one (cross-surface switch drift).
581
607
  try:
@@ -1458,6 +1484,13 @@ def cmd_switch(args):
1458
1484
  cfg["workspace"] = new_id; cfg["workspace_id"] = new_id
1459
1485
  _atomic_write(CONFIG_PATH, cfg)
1460
1486
 
1487
+ # Fire coverage POST so the new workspace's "developer connected" widget
1488
+ # populates immediately. Without this the switch works but the widget
1489
+ # stays empty until the next `conduct login`.
1490
+ try:
1491
+ _report_tool_coverage()
1492
+ except Exception:
1493
+ pass
1461
1494
 
1462
1495
  # Re-sync Guard policies for the new workspace
1463
1496
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.9.8
3
+ Version: 0.9.9
4
4
  Summary: CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -377,3 +377,96 @@ def test_switch_falls_back_gracefully_when_remint_fails(tmp_path, monkeypatch, c
377
377
  assert updated_cfg["workspace_id"] == "ab1b2c3d-0000-0000-0000-000000000002"
378
378
  out = capsys.readouterr().out
379
379
  assert "falling back to config-only switch" in out
380
+
381
+
382
+ # ---------------------------------------------------------------------------
383
+ # 0.9.9 — cmd_switch must fire _report_tool_coverage so the new workspace's
384
+ # "developer connected" widget populates immediately (not just after next login)
385
+ # ---------------------------------------------------------------------------
386
+
387
+ def test_switch_fires_tool_coverage(tmp_path, monkeypatch, capsys):
388
+ from conduct_cli import main as m
389
+ from conduct_cli import guard as g
390
+
391
+ cfg_path = tmp_path / "conduct" / "config.json"
392
+ cfg_path.parent.mkdir(parents=True)
393
+ cfg_path.write_text(json.dumps({
394
+ "api_url": "https://api.conductai.ai",
395
+ "agent_token": "cond_agt_test",
396
+ "workspace": "ef0a7e36-0000-0000-0000-000000000001",
397
+ }))
398
+ args = _make_args(workspace="Marketing")
399
+
400
+ def _api_req(method, url, hdrs, body=None, timeout=30):
401
+ if method == "GET" and url.endswith("/projects"):
402
+ return _fake_workspaces()
403
+ if method == "POST" and url.endswith("/auth/switch-workspace"):
404
+ return {"agent_token": "cond_agt_new", "expires_in": 28800, "workspace_id": body["workspace_id"], "user_id": "u1"}
405
+ raise AssertionError(f"unexpected call {method} {url}")
406
+
407
+ with (
408
+ patch.object(m, "CONFIG_PATH", cfg_path),
409
+ patch("pathlib.Path.home", return_value=tmp_path),
410
+ patch.object(m.api, "req", side_effect=_api_req),
411
+ patch.object(g, "_req", return_value={"version": "1", "rules": []}),
412
+ patch.object(g, "_save_policy"),
413
+ patch.object(m, "_report_tool_coverage") as coverage,
414
+ ):
415
+ m.cmd_switch(args)
416
+
417
+ coverage.assert_called_once()
418
+
419
+
420
+ # ---------------------------------------------------------------------------
421
+ # 0.9.9 — cmd_login must preserve the prior workspace_id if it differs from
422
+ # the server-returned default. Otherwise every re-login blows away the user's
423
+ # last `conduct switch`.
424
+ # ---------------------------------------------------------------------------
425
+
426
+ def test_login_preserves_prior_workspace(tmp_path, capsys):
427
+ from conduct_cli import main as m
428
+
429
+ cfg_path = tmp_path / "config.json"
430
+ # Prior state: user was on Marketing (via a previous switch)
431
+ cfg_path.write_text(json.dumps({
432
+ "api_url": "https://api.conductai.ai",
433
+ "agent_token": "cond_agt_old",
434
+ "workspace": "ab1b2c3d-0000-0000-0000-000000000002",
435
+ "workspace_id": "ab1b2c3d-0000-0000-0000-000000000002",
436
+ }))
437
+ args = _make_args(server=None, token=None)
438
+
439
+ # Login returns Engineering (server default) — but prior was Marketing
440
+ fake_login_result = {
441
+ "agent_token": "cond_agt_from_login_engineering",
442
+ "workspace_id": "ef0a7e36-0000-0000-0000-000000000001",
443
+ }
444
+ remint_response = {
445
+ "agent_token": "cond_agt_reminted_for_marketing",
446
+ "refresh_token": "cond_ref_reminted",
447
+ "expires_in": 28800,
448
+ "workspace_id": "ab1b2c3d-0000-0000-0000-000000000002",
449
+ "user_id": "u1",
450
+ }
451
+
452
+ def _api_req(method, url, hdrs, body=None, timeout=30):
453
+ if method == "POST" and url.endswith("/auth/switch-workspace"):
454
+ # Should be called with prior_ws (Marketing UUID)
455
+ assert body == {"workspace_id": "ab1b2c3d-0000-0000-0000-000000000002"}
456
+ return remint_response
457
+ if method == "GET" and url.endswith("/projects"):
458
+ return _fake_workspaces()
459
+ raise AssertionError(f"unexpected call {method} {url}")
460
+
461
+ with (
462
+ patch.object(m, "CONFIG_PATH", cfg_path),
463
+ patch.object(m, "_web_login_flow", return_value=fake_login_result),
464
+ patch.object(m.api, "req", side_effect=_api_req),
465
+ patch("conduct_cli.guard.cmd_guard_sync", return_value=None),
466
+ ):
467
+ m.cmd_login(args)
468
+
469
+ updated_cfg = json.loads(cfg_path.read_text())
470
+ assert updated_cfg["workspace_id"] == "ab1b2c3d-0000-0000-0000-000000000002"
471
+ assert updated_cfg["agent_token"] == "cond_agt_reminted_for_marketing"
472
+ assert updated_cfg["refresh_token"] == "cond_ref_reminted"
File without changes
File without changes
File without changes