conduct-cli 0.5.5__tar.gz → 0.5.6__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 (28) hide show
  1. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/PKG-INFO +1 -1
  2. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/pyproject.toml +1 -1
  3. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/hook_template.py +37 -1
  4. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  5. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
  6. conduct_cli-0.5.6/tests/test_bash_operator_signature.py +82 -0
  7. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/README.md +0 -0
  8. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/setup.cfg +0 -0
  9. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/setup.py +0 -0
  10. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/__init__.py +0 -0
  11. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/api.py +0 -0
  12. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/guard.py +0 -0
  13. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/guardmcp.py +0 -0
  14. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/hook_precompact_template.py +0 -0
  15. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/hook_session_start_template.py +0 -0
  16. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/hook_stop_template.py +0 -0
  17. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/main.py +0 -0
  18. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/mcp_server.py +0 -0
  19. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/memory.py +0 -0
  20. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli/paxel.py +0 -0
  21. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  22. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  23. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/requires.txt +0 -0
  24. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/src/conduct_cli.egg-info/top_level.txt +0 -0
  25. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/tests/test_guard_policy.py +0 -0
  26. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/tests/test_guard_savings.py +0 -0
  27. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/tests/test_hook_syntax.py +0 -0
  28. {conduct_cli-0.5.5 → conduct_cli-0.5.6}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: CLI for Conduct AI — 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.5.5"
7
+ version = "0.5.6"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -2,6 +2,7 @@
2
2
  """ConductGuard PreToolUse hook — enforces team policies, tracks all tool calls."""
3
3
  import json
4
4
  import re
5
+ import shlex
5
6
  import subprocess
6
7
  import sys
7
8
  import time
@@ -127,6 +128,35 @@ def _fetch_budget_status():
127
128
  try:
128
129
  from conduct_cli.guard import _check_policy
129
130
  except Exception:
131
+ def _bash_operator_signature(command):
132
+ """Extract argv[0] + subcommand + flag tokens per shell segment.
133
+ Skips quoted argument values so patterns don't match content inside --body/-m strings.
134
+ Returns space-joined signature across segments.
135
+ """
136
+ if not command:
137
+ return ""
138
+ segments = re.split(r'&&|\|\||\|(?!\|)|;', command)
139
+ parts = []
140
+ for seg in segments:
141
+ try:
142
+ tokens = shlex.split(seg.strip())
143
+ except ValueError:
144
+ continue
145
+ if not tokens:
146
+ continue
147
+ sig = [tokens[0]]
148
+ i = 1
149
+ # Subcommand: only barewords (no whitespace means wasn't a quoted multi-word value)
150
+ if i < len(tokens) and not tokens[i].startswith("-") and " " not in tokens[i]:
151
+ sig.append(tokens[i])
152
+ i += 1
153
+ # Flags: only real flag tokens, not flag-like strings inside quoted content
154
+ for t in tokens[i:]:
155
+ if t.startswith("-") and " " not in t:
156
+ sig.append(t)
157
+ parts.append(" ".join(sig))
158
+ return " ; ".join(parts)
159
+
130
160
  def _check_policy(tool_name, tool_input, tokens_before=0):
131
161
  """Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
132
162
  if not POLICY_PATH.exists():
@@ -137,7 +167,13 @@ except Exception:
137
167
  return None, "allow", None, None
138
168
 
139
169
  rules = policy.get("rules", [])
140
- input_text = json.dumps(tool_input)
170
+ # For Bash, match against operator signature (argv[0] + subcommand + flags) — not raw JSON.
171
+ # Prevents false positives where dangerous patterns appear inside quoted argument values
172
+ # (e.g. `gh issue create --body "...rm -rf..."` should not match no-rm-rf).
173
+ if tool_name == "Bash" and tool_input.get("command"):
174
+ input_text = _bash_operator_signature(tool_input["command"])
175
+ else:
176
+ input_text = json.dumps(tool_input)
141
177
  path_fields = [str(tool_input.get(f, "")) for f in ["file_path", "path", "command"]]
142
178
 
143
179
  for rule in rules:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.5.5
3
+ Version: 0.5.6
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -19,6 +19,7 @@ src/conduct_cli.egg-info/dependency_links.txt
19
19
  src/conduct_cli.egg-info/entry_points.txt
20
20
  src/conduct_cli.egg-info/requires.txt
21
21
  src/conduct_cli.egg-info/top_level.txt
22
+ tests/test_bash_operator_signature.py
22
23
  tests/test_guard_policy.py
23
24
  tests/test_guard_savings.py
24
25
  tests/test_hook_syntax.py
@@ -0,0 +1,82 @@
1
+ """Tests for _bash_operator_signature — fixes #728 false positives."""
2
+ import re
3
+ import shlex
4
+
5
+
6
+ def _bash_operator_signature(command):
7
+ """Same logic as hook_template.py. Kept inline for unit testing."""
8
+ if not command:
9
+ return ""
10
+ segments = re.split(r'&&|\|\||\|(?!\|)|;', command)
11
+ parts = []
12
+ for seg in segments:
13
+ try:
14
+ tokens = shlex.split(seg.strip())
15
+ except ValueError:
16
+ continue
17
+ if not tokens:
18
+ continue
19
+ sig = [tokens[0]]
20
+ i = 1
21
+ # Subcommand: next token only if it's a bareword (no whitespace = wasn't a multi-word quoted value)
22
+ if i < len(tokens) and not tokens[i].startswith("-") and " " not in tokens[i]:
23
+ sig.append(tokens[i])
24
+ i += 1
25
+ # Flags only if they're real flags (no whitespace = not embedded in quoted content)
26
+ for t in tokens[i:]:
27
+ if t.startswith("-") and " " not in t:
28
+ sig.append(t)
29
+ parts.append(" ".join(sig))
30
+ return " ; ".join(parts)
31
+
32
+
33
+ def assert_match(pattern, command, should_match):
34
+ sig = _bash_operator_signature(command)
35
+ matched = bool(re.search(pattern, sig, re.IGNORECASE))
36
+ assert matched is should_match, f"{command!r} → sig={sig!r}, pattern={pattern!r}, expected={should_match}, got={matched}"
37
+
38
+
39
+ def test_rm_rf_actual_command_fires():
40
+ assert_match(r"\brm\s+-rf\b", "rm -rf /tmp/build", True)
41
+
42
+
43
+ def test_rm_rf_in_gh_body_does_not_fire():
44
+ assert_match(r"\brm\s+-rf\b", 'gh issue create --body "contains rm -rf example"', False)
45
+
46
+
47
+ def test_rm_rf_in_git_commit_does_not_fire():
48
+ assert_match(r"\brm\s+-rf\b", 'git commit -m "fix: remove rm -rf call from script"', False)
49
+
50
+
51
+ def test_force_push_fires():
52
+ assert_match(r"git\s+push.*--force", "git push --force origin main", True)
53
+
54
+
55
+ def test_force_push_in_commit_does_not_fire():
56
+ assert_match(r"git\s+push.*--force", 'git commit -m "add force-push docs"', False)
57
+
58
+
59
+ def test_vercel_prod_fires():
60
+ assert_match(r"vercel.*--prod", "vercel deploy --prod", True)
61
+
62
+
63
+ def test_vercel_prod_in_echo_does_not_fire():
64
+ assert_match(r"vercel.*--prod", 'echo "vercel deploy --prod example"', False)
65
+
66
+
67
+ def test_chained_rm_rf_fires():
68
+ assert_match(r"\brm\s+-rf\b", "git add . && rm -rf node_modules", True)
69
+
70
+
71
+ def test_chained_rm_in_commit_does_not_fire():
72
+ assert_match(r"\brm\s+-rf\b", 'git add . && git commit -m "rm -rf cleanup"', False)
73
+
74
+
75
+ def test_empty_command():
76
+ assert _bash_operator_signature("") == ""
77
+
78
+
79
+ def test_unterminated_quote_does_not_crash():
80
+ # Should skip the segment, not raise
81
+ sig = _bash_operator_signature('git commit -m "unterminated')
82
+ assert isinstance(sig, str)
File without changes
File without changes
File without changes