conduct-cli 0.5.6__tar.gz → 0.5.8__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.5.6 → conduct_cli-0.5.8}/PKG-INFO +1 -1
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/pyproject.toml +1 -1
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/guard.py +6 -1
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/hook_template.py +36 -1
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/main.py +16 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/README.md +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/setup.cfg +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/setup.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.5.6 → conduct_cli-0.5.8}/tests/test_switch.py +0 -0
|
@@ -572,8 +572,13 @@ def cmd_guard_install(args):
|
|
|
572
572
|
api_key=api_key,
|
|
573
573
|
)
|
|
574
574
|
_save_policy(policy)
|
|
575
|
+
# Mirror fail_mode into guard config so the hook can enforce it
|
|
576
|
+
# even when POLICY_PATH is missing or unreadable.
|
|
577
|
+
cfg = _load_guard_config()
|
|
578
|
+
cfg["fail_mode"] = policy.get("fail_mode", "fail_open")
|
|
579
|
+
_save_guard_config(cfg)
|
|
575
580
|
rule_count = len(policy.get("rules", []))
|
|
576
|
-
print(f" {GREEN}Guard policies:{RESET} {rule_count} rule(s) active")
|
|
581
|
+
print(f" {GREEN}Guard policies:{RESET} {rule_count} rule(s) active · fail mode: {cfg['fail_mode']}")
|
|
577
582
|
except SystemExit:
|
|
578
583
|
rule_count = 0
|
|
579
584
|
|
|
@@ -86,7 +86,17 @@ def _maybe_sync_policy():
|
|
|
86
86
|
POLICY_PATH.write_text(json.dumps(remote, indent=2))
|
|
87
87
|
VERSION_CACHE_PATH.write_text(json.dumps({"ts": time.time(), "version": remote_version}))
|
|
88
88
|
except Exception:
|
|
89
|
-
pass #
|
|
89
|
+
pass # sync failure does not directly block; policy file may still be present
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_fail_mode():
|
|
93
|
+
"""Read fail_mode from ~/.conduct/config.json. Default: fail_open."""
|
|
94
|
+
try:
|
|
95
|
+
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
96
|
+
except Exception:
|
|
97
|
+
return "fail_open"
|
|
98
|
+
mode = cfg.get("fail_mode", "fail_open")
|
|
99
|
+
return mode if mode in ("fail_open", "fail_closed") else "fail_open"
|
|
90
100
|
|
|
91
101
|
|
|
92
102
|
def _load_budget_cache():
|
|
@@ -685,10 +695,35 @@ def main():
|
|
|
685
695
|
# Policy version check (cached 60s) — auto-syncs if server version differs
|
|
686
696
|
_maybe_sync_policy()
|
|
687
697
|
|
|
698
|
+
# Fail-closed gate: if the local policy file is missing and the workspace
|
|
699
|
+
# is configured for fail_closed, block every tool call with a clear
|
|
700
|
+
# message. fail_open (default) silently allows.
|
|
701
|
+
if _get_fail_mode() == "fail_closed" and not POLICY_PATH.exists():
|
|
702
|
+
tool_name = (data.get("tool_name") or "").lower()
|
|
703
|
+
tool_input = data.get("tool_input") or {}
|
|
704
|
+
session_id = data.get("session_id")
|
|
705
|
+
msg = "[ConductGuard] Policy cache unavailable — fail-closed mode is on. Run `conduct guard sync` or contact your admin."
|
|
706
|
+
print(msg)
|
|
707
|
+
print(msg, file=sys.stderr)
|
|
708
|
+
_post_event(tool_name, tool_input, "blocked", "guard-unavailable", msg, session_id=session_id)
|
|
709
|
+
sys.exit(2)
|
|
710
|
+
|
|
688
711
|
# Hard budget cap (cached 5 min)
|
|
689
712
|
hard_blocked, reason = _load_budget_cache()
|
|
690
713
|
if hard_blocked is None:
|
|
691
714
|
hard_blocked, reason = _fetch_budget_status()
|
|
715
|
+
# Fail-closed: if the budget check returned default (None reason) AND
|
|
716
|
+
# we just hit the API for a fresh value, the API was unreachable.
|
|
717
|
+
# Treat as unavailable and block.
|
|
718
|
+
if not hard_blocked and reason is None and _get_fail_mode() == "fail_closed":
|
|
719
|
+
tool_name = (data.get("tool_name") or "").lower()
|
|
720
|
+
tool_input = data.get("tool_input") or {}
|
|
721
|
+
session_id = data.get("session_id")
|
|
722
|
+
msg = "[ConductGuard] Spend budget check unavailable — fail-closed mode is on."
|
|
723
|
+
print(msg)
|
|
724
|
+
print(msg, file=sys.stderr)
|
|
725
|
+
_post_event(tool_name, tool_input, "blocked", "guard-unavailable", msg, session_id=session_id)
|
|
726
|
+
sys.exit(2)
|
|
692
727
|
if hard_blocked:
|
|
693
728
|
msg = f"[ConductGuard] {reason or 'Budget hard cap reached. Contact your manager.'}"
|
|
694
729
|
print(msg)
|
|
@@ -2431,6 +2431,22 @@ def cmd_run(args):
|
|
|
2431
2431
|
hint = "export GITHUB_TOKEN=<token>" if not gh_token else "workflow has no github_hook_repo"
|
|
2432
2432
|
print(f" {GRAY}No GitHub token — using test payload. ({hint}){RESET}\n")
|
|
2433
2433
|
|
|
2434
|
+
# #734: validate required inputs locally before POST — fail fast with clear error.
|
|
2435
|
+
try:
|
|
2436
|
+
vres = api.req("POST", f"{server}/workflows/{workflow_id}/validate-inputs", json_h,
|
|
2437
|
+
{"inputs": body.get("inputs") or {}, "phase": "run"})
|
|
2438
|
+
missing = (vres or {}).get("missing") or []
|
|
2439
|
+
if missing:
|
|
2440
|
+
print(f"{RED}✗ Missing required inputs:{RESET}")
|
|
2441
|
+
for m in missing:
|
|
2442
|
+
print(f" - {m['label']} ({m['key']})")
|
|
2443
|
+
print(f"\n{GRAY}Provide with --input {missing[0]['key']}=<value>{RESET}")
|
|
2444
|
+
sys.exit(2)
|
|
2445
|
+
except SystemExit:
|
|
2446
|
+
raise
|
|
2447
|
+
except Exception:
|
|
2448
|
+
pass # validate endpoint is best-effort; backend will re-check on /trigger
|
|
2449
|
+
|
|
2434
2450
|
# Fix 3: /trigger returns run_id, not id.
|
|
2435
2451
|
if getattr(args, "max_turns", None):
|
|
2436
2452
|
body["__max_turns"] = args.max_turns
|
|
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
|