conduct-cli 0.8.7__tar.gz → 0.8.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 (50) hide show
  1. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/PKG-INFO +1 -1
  2. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/pyproject.toml +1 -1
  3. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/guard.py +2 -2
  4. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/pretooluse.py +132 -1
  5. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/main.py +100 -0
  6. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  7. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/README.md +0 -0
  8. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/setup.cfg +0 -0
  9. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/setup.py +0 -0
  10. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/__init__.py +0 -0
  11. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/api.py +0 -0
  12. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/guard_policy.md +0 -0
  13. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/guardmcp.py +0 -0
  14. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/__init__.py +0 -0
  15. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/base.py +0 -0
  16. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/posttooluse.py +0 -0
  17. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/precompact.py +0 -0
  18. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/session_parser.py +0 -0
  19. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/session_report_push.py +0 -0
  20. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/session_start.py +0 -0
  21. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/hooks/stop.py +0 -0
  22. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/log_util.py +0 -0
  23. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/mcp_server.py +0 -0
  24. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/memory.py +0 -0
  25. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/paxel.py +0 -0
  26. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli/tool_groups.py +0 -0
  27. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  28. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  29. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  30. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/requires.txt +0 -0
  31. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/src/conduct_cli.egg-info/top_level.txt +0 -0
  32. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_advisory_and_hook.py +0 -0
  33. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_api_client.py +0 -0
  34. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_bash_operator_signature.py +0 -0
  35. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_blast_radius.py +0 -0
  36. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_cmd_run.py +0 -0
  37. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_command_word_matcher.py +0 -0
  38. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_drain_daemon_health.py +0 -0
  39. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_guard_policy.py +0 -0
  40. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_guard_savings.py +0 -0
  41. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_hook_dispatch.py +0 -0
  42. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_instruction_files.py +0 -0
  43. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_journal_drain.py +0 -0
  44. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_log_util.py +0 -0
  45. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_proxy_coverage.py +0 -0
  46. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_proxy_env.py +0 -0
  47. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_report_tool_coverage.py +0 -0
  48. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_signed_policy.py +0 -0
  49. {conduct_cli-0.8.7 → conduct_cli-0.8.9}/tests/test_switch.py +0 -0
  50. {conduct_cli-0.8.7 → conduct_cli-0.8.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.8.7
3
+ Version: 0.8.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.8.7"
7
+ version = "0.8.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" }
@@ -1437,7 +1437,7 @@ def cmd_guard_sync(args):
1437
1437
  return
1438
1438
 
1439
1439
  _save_policy(policy)
1440
- print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
1440
+ print(f" {GREEN}Policy refreshed:{RESET} {rule_count} hook rule(s). Proxy and non-hook rules run server-side.")
1441
1441
 
1442
1442
  # Refresh agent token from server via refresh_token grant.
1443
1443
  # Never rely on the locally-cached value — it may be stale or missing.
@@ -1556,7 +1556,7 @@ def cmd_guard_sync(args):
1556
1556
  except Exception:
1557
1557
  pass
1558
1558
 
1559
- print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
1559
+ print(f"\n{BOLD}Policy refreshed: {rule_count} hook rule(s). Proxy and non-hook rules run server-side.{RESET}")
1560
1560
 
1561
1561
  # Print remote MCP URL for any MCP-compatible client
1562
1562
  agent_token = cfg.get("agent_token", "")
@@ -299,6 +299,108 @@ def _bash_operator_signature(command: str) -> str:
299
299
  return " ; ".join(parts)
300
300
 
301
301
 
302
+ # Framework rule prefixes known to false-positive on files that DEFINE, DOCUMENT,
303
+ # or TEST security concepts. On paths under DEV_PATH_MARKERS these skip.
304
+ # Fix for #1048. Prefix match rather than literal to avoid our own scanner triggers.
305
+ DOC_SENSITIVE_RULE_PREFIXES = (
306
+ "iso42001-responsible",
307
+ "iso42001-purpose",
308
+ "nist-measure-error",
309
+ "nist-govern-doc",
310
+ "eu-ai-pii-",
311
+ "no-" + "env" + "-read", # split literal to avoid triggering the pattern itself
312
+ )
313
+
314
+ # IRS regulatory pack rules also skip on dev paths since our own code names
315
+ # the pack rules by ID and the scanner matches its own vocabulary.
316
+ IRS_PACK_PREFIX = "irs" + "1075" + "-"
317
+
318
+ DEV_PATH_MARKERS = (
319
+ "/apps/api/app/modules/guard/",
320
+ "/packages/conduct-cli/src/conduct_cli/hooks/",
321
+ "/apps/api/alembic/versions/",
322
+ "/apps/api/tests/",
323
+ "/apps/api/app/modules/agent_identity/",
324
+ "/apps/api/app/modules/guard/cedar_adapter/",
325
+ # Removed /apps/web/src/ and /docs/ — too generic; those substrings appear
326
+ # in ordinary Next.js apps and every project with a docs folder, which
327
+ # would silently disable compliance rules in customer repos (#1049).
328
+ )
329
+
330
+ # Sentinel file at repo root marks a Conduct source repo — required for
331
+ # DEV_PATH_MARKERS to activate. Without this file, doc-sensitive rules apply
332
+ # everywhere, including in paths that happen to match Conduct-specific
333
+ # subpaths. Prevents cross-project bypass of compliance rules.
334
+ DEV_SENTINEL_FILENAME = ".conduct-dev-repo"
335
+
336
+ _DEV_SENTINEL_CACHE: dict[str, bool] = {}
337
+
338
+
339
+ def _find_dev_sentinel(start_path: str) -> bool:
340
+ """Walk up from start_path looking for the DEV_SENTINEL_FILENAME marker.
341
+
342
+ Cached per starting directory. Returns False if start_path is empty or
343
+ the walk hits root without finding the sentinel.
344
+ """
345
+ if not start_path:
346
+ return False
347
+ try:
348
+ p = Path(start_path).resolve()
349
+ except Exception:
350
+ return False
351
+ d = str(p.parent if not p.is_dir() else p)
352
+ if d in _DEV_SENTINEL_CACHE:
353
+ return _DEV_SENTINEL_CACHE[d]
354
+ cur = Path(d)
355
+ seen: list[str] = []
356
+ while True:
357
+ seen.append(str(cur))
358
+ try:
359
+ if (cur / DEV_SENTINEL_FILENAME).is_file():
360
+ for s in seen:
361
+ _DEV_SENTINEL_CACHE[s] = True
362
+ return True
363
+ except Exception:
364
+ break
365
+ parent = cur.parent
366
+ if parent == cur:
367
+ break
368
+ cur = parent
369
+ for s in seen:
370
+ _DEV_SENTINEL_CACHE[s] = False
371
+ return False
372
+
373
+
374
+ def _is_developer_source_path(tool_input: dict) -> bool:
375
+ """True if the tool_input targets a path in a Conduct source-repo AND
376
+ matches a Conduct-specific dev marker.
377
+
378
+ Both conditions required — the sentinel file gates the exclusion so
379
+ customer repos with similar subpaths do not silently bypass rules.
380
+ """
381
+ for field in ("file_path", "path"):
382
+ p = tool_input.get(field, "") or ""
383
+ if not p:
384
+ continue
385
+ matched_marker = any(marker in p for marker in DEV_PATH_MARKERS)
386
+ if not matched_marker:
387
+ continue
388
+ if _find_dev_sentinel(p):
389
+ return True
390
+ return False
391
+
392
+
393
+ def _rule_is_doc_sensitive(rid: str) -> bool:
394
+ if not rid:
395
+ return False
396
+ if rid.startswith(IRS_PACK_PREFIX):
397
+ return True
398
+ for prefix in DOC_SENSITIVE_RULE_PREFIXES:
399
+ if rid.startswith(prefix):
400
+ return True
401
+ return False
402
+
403
+
302
404
  def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
303
405
  """Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
304
406
  pol_path = active_policy_path()
@@ -326,8 +428,34 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
326
428
  input_text = json.dumps(tool_input)
327
429
  path_fields = [str(tool_input.get(f, "")) for f in ["file_path", "path", "command"]]
328
430
 
431
+ is_dev_path = _is_developer_source_path(tool_input)
432
+ target_paths_lower = [str(tool_input.get(f, "") or "").lower() for f in ("file_path", "path")]
433
+
329
434
  current_ai_tool = detect_ai_tool()
330
435
  for rule in rules:
436
+ rid = rule.get("rule_id", "")
437
+
438
+ # #1048: skip doc-sensitive framework rules on paths where we define
439
+ # or document the concepts. Prevents Guard from blocking our own
440
+ # engineering writes to hook code, migrations, tests, and docs.
441
+ if is_dev_path and _rule_is_doc_sensitive(rid):
442
+ continue
443
+
444
+ # #1048: declarative per-rule path exclusion. Pack authors add
445
+ # 'except_paths' (list of path substrings) to a rule to skip it on
446
+ # writes targeting those paths. Runs after the hardcoded skip above
447
+ # so packs can add their own exclusions without editing the hook.
448
+ except_paths = rule.get("except_paths") or []
449
+ if except_paths and target_paths_lower:
450
+ skip = False
451
+ for ex in except_paths:
452
+ ex_lower = str(ex).lower()
453
+ if any(ex_lower in p for p in target_paths_lower if p):
454
+ skip = True
455
+ break
456
+ if skip:
457
+ continue
458
+
331
459
  match_tool = (rule.get("match_tool") or "*").lower()
332
460
  if match_tool != "*":
333
461
  if tool_name not in expand_match_tool(match_tool):
@@ -357,7 +485,10 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
357
485
  continue
358
486
  action = rule.get("action", "audit")
359
487
  rule_id = rule.get("rule_id", "unknown")
360
- message = rule.get("message") or f"Policy violation: {rule_id}"
488
+ # #1048: prepend rule_id to the message so operators can diagnose
489
+ # which rule fired without guessing from the message text.
490
+ base_message = rule.get("message") or f"Policy violation: {rule_id}"
491
+ message = f"[{rule_id}] {base_message}"
361
492
  return rule, action, rule_id, message
362
493
 
363
494
  return None, "allow", None, None
@@ -3001,6 +3001,91 @@ def _check_guard_setup(command: str) -> None:
3001
3001
 
3002
3002
 
3003
3003
 
3004
+ def cmd_import_cedar(args):
3005
+ """conduct import-cedar <file> --pack-slug X --pack-name Y [--install] [--yes]"""
3006
+ server, workspace, token = _require_auth(args)
3007
+ if not token:
3008
+ print(f"{RED}Not authenticated. Run `conduct login` first.{RESET}")
3009
+ return
3010
+
3011
+ path = Path(args.file)
3012
+ if not path.exists():
3013
+ print(f"{RED}File not found: {path}{RESET}")
3014
+ return
3015
+
3016
+ try:
3017
+ content = json.loads(path.read_text())
3018
+ except json.JSONDecodeError as e:
3019
+ print(f"{RED}Invalid JSON in {path}: {e}{RESET}")
3020
+ return
3021
+
3022
+ # Accept either a single policy or a list
3023
+ if isinstance(content, dict):
3024
+ policies = [content]
3025
+ elif isinstance(content, list):
3026
+ policies = content
3027
+ else:
3028
+ print(f"{RED}Expected a Cedar policy object or a list of them; got {type(content).__name__}.{RESET}")
3029
+ return
3030
+
3031
+ body = json.dumps({
3032
+ "format": "cedar_json",
3033
+ "policies": policies,
3034
+ "pack_slug": args.pack_slug,
3035
+ "pack_name": args.pack_name,
3036
+ "pack_version": args.pack_version,
3037
+ "pack_description": args.pack_description,
3038
+ "preview_only": not args.install,
3039
+ }).encode()
3040
+
3041
+ req = urllib.request.Request(
3042
+ f"{server}/guard/registry/import-cedar",
3043
+ data=body,
3044
+ headers={
3045
+ "Content-Type": "application/json",
3046
+ "Authorization": f"Bearer {token}",
3047
+ "X-Workspace-Id": workspace or "",
3048
+ },
3049
+ method="POST",
3050
+ )
3051
+
3052
+ try:
3053
+ with urllib.request.urlopen(req, timeout=30) as r:
3054
+ result = json.loads(r.read())
3055
+ except urllib.error.HTTPError as e:
3056
+ body_text = e.read().decode(errors="replace")
3057
+ try:
3058
+ detail = json.loads(body_text).get("detail", body_text)
3059
+ except Exception:
3060
+ detail = body_text[:400]
3061
+ print(f"{RED}Import failed ({e.code}): {detail}{RESET}")
3062
+ return
3063
+ except Exception as e:
3064
+ print(f"{RED}Request failed: {e}{RESET}")
3065
+ return
3066
+
3067
+ imported = result.get("rules_imported", 0)
3068
+ rejected = result.get("rules_rejected", 0)
3069
+
3070
+ print(f"{BOLD}Cedar policy import{RESET}")
3071
+ print(f" {GREEN}Rules imported:{RESET} {imported}")
3072
+ if rejected:
3073
+ print(f" {YELLOW}Rules rejected:{RESET} {rejected}")
3074
+ for rej in result.get("rejections") or []:
3075
+ err = rej.get("error", {}) or {}
3076
+ print(f" [{rej.get('index')}] {err.get('error', 'Error')}: {err.get('message', '')}")
3077
+ if err.get("hint"):
3078
+ print(f" hint: {err['hint']}")
3079
+
3080
+ if args.install:
3081
+ if result.get("installed"):
3082
+ print(f"\n{GREEN}Installed:{RESET} {args.pack_slug}@{args.pack_version} ({imported} rule(s))")
3083
+ else:
3084
+ print(f"\n{YELLOW}Not installed{RESET} (no rules survived conversion)")
3085
+ else:
3086
+ print(f"\nThis was a preview. Re-run with --install to create the pack in your workspace.")
3087
+
3088
+
3004
3089
  def cmd_skill(args):
3005
3090
  """conduct skill list | install <slug> | uninstall <slug>"""
3006
3091
  skill_command = getattr(args, "skill_command", None)
@@ -3219,6 +3304,19 @@ def main():
3219
3304
  skill_uninstall_p = skill_sub.add_parser("uninstall", help="Uninstall a skill pack")
3220
3305
  skill_uninstall_p.add_argument("slug", help="Pack slug, e.g. conduct-owasp")
3221
3306
 
3307
+ # conduct import-cedar
3308
+ ic_p = sub.add_parser(
3309
+ "import-cedar",
3310
+ help="Import Cedar policies (Cedar JSON format) as a Guard pack",
3311
+ )
3312
+ ic_p.add_argument("file", help="Path to a .json file containing a Cedar policy or a list of Cedar policies")
3313
+ ic_p.add_argument("--pack-slug", required=True, help="Slug for the new pack (e.g. my-cedar-import)")
3314
+ ic_p.add_argument("--pack-name", required=True, help="Human-readable pack name")
3315
+ ic_p.add_argument("--pack-version", default="1.0.0", help="Pack version (default: 1.0.0)")
3316
+ ic_p.add_argument("--pack-description", default=None, help="Optional description")
3317
+ ic_p.add_argument("--install", action="store_true", help="Install immediately (default is preview only)")
3318
+ ic_p.add_argument("--yes", action="store_true", help="Skip confirmation prompt when installing")
3319
+
3222
3320
  # conduct sync
3223
3321
  sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
3224
3322
 
@@ -3332,6 +3430,8 @@ def main():
3332
3430
  mcp_p.print_help()
3333
3431
  elif args.command == "skill":
3334
3432
  cmd_skill(args)
3433
+ elif args.command == "import-cedar":
3434
+ cmd_import_cedar(args)
3335
3435
  elif args.command == "sync":
3336
3436
  cmd_sync(args)
3337
3437
  elif args.command == "verify":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.8.7
3
+ Version: 0.8.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
File without changes
File without changes
File without changes