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.
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/PKG-INFO +1 -1
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/pyproject.toml +1 -1
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/main.py +33 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_switch.py +93 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/README.md +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/setup.cfg +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/setup.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_api_client.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_bash_scan_target.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_cli_matrix.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_conductguard_mcp_stdio.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_approvals.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_log_util.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.9.8 → conduct_cli-0.9.9}/tests/test_token_lifecycle.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.9.
|
|
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:
|
|
@@ -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
|
|
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
|