conduct-cli 0.4.3__tar.gz → 0.4.5__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.4.3 → conduct_cli-0.4.5}/PKG-INFO +1 -1
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/pyproject.toml +2 -1
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli/guard.py +129 -20
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/entry_points.txt +1 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/README.md +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/setup.cfg +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/setup.py +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.3 → conduct_cli-0.4.5}/src/conduct_cli.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
8
8
|
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -33,6 +33,7 @@ Repository = "https://github.com/sseshachala/conductai"
|
|
|
33
33
|
[project.scripts]
|
|
34
34
|
conduct = "conduct_cli.main:main"
|
|
35
35
|
conductguard-mcp = "conduct_cli.guardmcp:main"
|
|
36
|
+
conductguard-post = "conduct_cli.guard:post_usage_main"
|
|
36
37
|
|
|
37
38
|
[tool.setuptools.packages.find]
|
|
38
39
|
where = ["src"]
|
|
@@ -125,7 +125,7 @@ def _detect_ai_tool():
|
|
|
125
125
|
return "unknown"
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
128
|
+
def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, tool_use_id=None):
|
|
129
129
|
try:
|
|
130
130
|
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
131
131
|
except Exception:
|
|
@@ -144,6 +144,7 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
|
144
144
|
"decision": decision,
|
|
145
145
|
"rule_id": rule_id,
|
|
146
146
|
"rule_message": message,
|
|
147
|
+
"tool_use_id": tool_use_id,
|
|
147
148
|
})
|
|
148
149
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
149
150
|
script = (
|
|
@@ -162,6 +163,55 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None):
|
|
|
162
163
|
)
|
|
163
164
|
|
|
164
165
|
|
|
166
|
+
def _post_usage(tool_use_id, tokens_input, tokens_output, duration_ms, ai_tool):
|
|
167
|
+
"""Fire-and-forget POST to /guard/events/usage"""
|
|
168
|
+
try:
|
|
169
|
+
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
170
|
+
except Exception:
|
|
171
|
+
return
|
|
172
|
+
workspace_id = cfg.get("workspace_id")
|
|
173
|
+
if not workspace_id or not tool_use_id:
|
|
174
|
+
return
|
|
175
|
+
payload = json.dumps({
|
|
176
|
+
"workspace_id": workspace_id,
|
|
177
|
+
"tool_use_id": tool_use_id,
|
|
178
|
+
"tokens_input": tokens_input,
|
|
179
|
+
"tokens_output": tokens_output,
|
|
180
|
+
"duration_ms": duration_ms,
|
|
181
|
+
"ai_tool": _detect_ai_tool(),
|
|
182
|
+
})
|
|
183
|
+
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
184
|
+
script = (
|
|
185
|
+
"import urllib.request\\n"
|
|
186
|
+
"try:\\n"
|
|
187
|
+
f" req = urllib.request.Request(\\"{api_url}/guard/events/usage\\","
|
|
188
|
+
f" data={repr(payload.encode())}, headers={{\\\"Content-Type\\\": \\\"application/json\\\"}}, method=\\"POST\\")\\n"
|
|
189
|
+
" urllib.request.urlopen(req, timeout=5)\\n"
|
|
190
|
+
"except: pass\\n"
|
|
191
|
+
)
|
|
192
|
+
subprocess.Popen(
|
|
193
|
+
[sys.executable, "-c", script],
|
|
194
|
+
stdout=subprocess.DEVNULL,
|
|
195
|
+
stderr=subprocess.DEVNULL,
|
|
196
|
+
start_new_session=True,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def post_usage_main():
|
|
201
|
+
"""PostToolUse hook entrypoint — captures token usage and duration."""
|
|
202
|
+
try:
|
|
203
|
+
data = json.load(sys.stdin)
|
|
204
|
+
except Exception:
|
|
205
|
+
sys.exit(0)
|
|
206
|
+
tool_use_id = data.get("tool_use_id")
|
|
207
|
+
usage = data.get("usage") or {}
|
|
208
|
+
tokens_input = usage.get("input_tokens", 0)
|
|
209
|
+
tokens_output = usage.get("output_tokens", 0)
|
|
210
|
+
duration_ms = data.get("duration_ms")
|
|
211
|
+
_post_usage(tool_use_id, tokens_input, tokens_output, duration_ms, _detect_ai_tool())
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
|
|
165
215
|
def main():
|
|
166
216
|
try:
|
|
167
217
|
data = json.load(sys.stdin)
|
|
@@ -178,12 +228,13 @@ def main():
|
|
|
178
228
|
|
|
179
229
|
tool_name = (data.get("tool_name") or "").lower()
|
|
180
230
|
tool_input = data.get("tool_input") or {}
|
|
231
|
+
tool_use_id = data.get("tool_use_id")
|
|
181
232
|
|
|
182
233
|
_, action, rule_id, message = _check_policy(tool_name, tool_input)
|
|
183
234
|
|
|
184
235
|
# Always post an event — "allowed" for normal calls, "blocked"/"warned" for violations
|
|
185
236
|
decision = {"block": "blocked", "warn": "warned", "approval": "blocked"}.get(action, "allowed")
|
|
186
|
-
_post_event(tool_name, tool_input, decision, rule_id, message)
|
|
237
|
+
_post_event(tool_name, tool_input, decision, rule_id, message, tool_use_id=tool_use_id)
|
|
187
238
|
|
|
188
239
|
if action == "block":
|
|
189
240
|
print(f"[ConductGuard] {message}")
|
|
@@ -265,7 +316,7 @@ def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
|
|
|
265
316
|
|
|
266
317
|
|
|
267
318
|
def _install_codex_hook(hook_path: Path) -> None:
|
|
268
|
-
"""Register
|
|
319
|
+
"""Register PreToolUse and PostToolUse hooks in ~/.codex/hooks.json."""
|
|
269
320
|
codex_hooks = Path.home() / ".codex" / "hooks.json"
|
|
270
321
|
if not (Path.home() / ".codex").exists():
|
|
271
322
|
return # Codex not installed
|
|
@@ -277,20 +328,42 @@ def _install_codex_hook(hook_path: Path) -> None:
|
|
|
277
328
|
except json.JSONDecodeError:
|
|
278
329
|
hooks = {}
|
|
279
330
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
331
|
+
hook_section = hooks.setdefault("hooks", {})
|
|
332
|
+
|
|
333
|
+
# PreToolUse
|
|
334
|
+
pre_cmd = f"python3 {hook_path}"
|
|
335
|
+
pre = hook_section.setdefault("PreToolUse", [])
|
|
336
|
+
pre_already = any(
|
|
337
|
+
e.get("command") == pre_cmd
|
|
284
338
|
for h in pre
|
|
285
339
|
for e in h.get("hooks", [])
|
|
286
340
|
)
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
changed = False
|
|
342
|
+
if not pre_already:
|
|
343
|
+
pre.append({"matcher": ".*", "hooks": [{"type": "command", "command": pre_cmd}]})
|
|
344
|
+
changed = True
|
|
345
|
+
|
|
346
|
+
# PostToolUse
|
|
347
|
+
post_cmd = "conductguard-post"
|
|
348
|
+
post = hook_section.setdefault("PostToolUse", [])
|
|
349
|
+
post_already = any(
|
|
350
|
+
e.get("command") == post_cmd
|
|
351
|
+
for h in post
|
|
352
|
+
for e in h.get("hooks", [])
|
|
353
|
+
)
|
|
354
|
+
if not post_already:
|
|
355
|
+
post.append({"matcher": "*", "hooks": [{"type": "command", "command": post_cmd}]})
|
|
356
|
+
changed = True
|
|
357
|
+
|
|
358
|
+
if changed:
|
|
289
359
|
codex_hooks.parent.mkdir(parents=True, exist_ok=True)
|
|
290
360
|
codex_hooks.write_text(json.dumps(hooks, indent=2))
|
|
291
|
-
|
|
361
|
+
if not pre_already:
|
|
362
|
+
print(f" {GREEN}Codex PreToolUse hook registered{RESET}")
|
|
363
|
+
if not post_already:
|
|
364
|
+
print(f" {GREEN}Codex PostToolUse hook registered{RESET}")
|
|
292
365
|
else:
|
|
293
|
-
print(f" {GRAY}Codex
|
|
366
|
+
print(f" {GRAY}Codex hooks already registered{RESET}")
|
|
294
367
|
|
|
295
368
|
|
|
296
369
|
# ── HTTP helpers (no third-party deps — mirrors api.py style) ─────────────────
|
|
@@ -342,7 +415,7 @@ def _parse_since(since_str: str) -> str:
|
|
|
342
415
|
# ── Hook helpers ─────────────────────────────────────────────────────────────
|
|
343
416
|
|
|
344
417
|
def _install_claude_hook(hook_path: Path) -> None:
|
|
345
|
-
"""Register
|
|
418
|
+
"""Register PreToolUse and PostToolUse hooks in ~/.claude/settings.json."""
|
|
346
419
|
claude_settings = Path.home() / ".claude" / "settings.json"
|
|
347
420
|
settings: dict = {}
|
|
348
421
|
if claude_settings.exists():
|
|
@@ -352,21 +425,41 @@ def _install_claude_hook(hook_path: Path) -> None:
|
|
|
352
425
|
settings = {}
|
|
353
426
|
|
|
354
427
|
hooks = settings.setdefault("hooks", {})
|
|
355
|
-
pre = hooks.setdefault("PreToolUse", [])
|
|
356
|
-
cmd = f"python3 {hook_path}"
|
|
357
428
|
|
|
358
|
-
|
|
359
|
-
|
|
429
|
+
# PreToolUse — existing hook script
|
|
430
|
+
pre = hooks.setdefault("PreToolUse", [])
|
|
431
|
+
pre_cmd = f"python3 {hook_path}"
|
|
432
|
+
pre_already = any(
|
|
433
|
+
e.get("command") == pre_cmd
|
|
360
434
|
for h in pre
|
|
361
435
|
for e in h.get("hooks", [])
|
|
362
436
|
)
|
|
363
|
-
|
|
364
|
-
|
|
437
|
+
changed = False
|
|
438
|
+
if not pre_already:
|
|
439
|
+
pre.append({"matcher": ".*", "hooks": [{"type": "command", "command": pre_cmd}]})
|
|
440
|
+
changed = True
|
|
441
|
+
|
|
442
|
+
# PostToolUse — conductguard-post entrypoint
|
|
443
|
+
post = hooks.setdefault("PostToolUse", [])
|
|
444
|
+
post_cmd = "conductguard-post"
|
|
445
|
+
post_already = any(
|
|
446
|
+
e.get("command") == post_cmd
|
|
447
|
+
for h in post
|
|
448
|
+
for e in h.get("hooks", [])
|
|
449
|
+
)
|
|
450
|
+
if not post_already:
|
|
451
|
+
post.append({"matcher": "*", "hooks": [{"type": "command", "command": post_cmd}]})
|
|
452
|
+
changed = True
|
|
453
|
+
|
|
454
|
+
if changed:
|
|
365
455
|
claude_settings.parent.mkdir(parents=True, exist_ok=True)
|
|
366
456
|
claude_settings.write_text(json.dumps(settings, indent=2))
|
|
367
|
-
|
|
457
|
+
if not pre_already:
|
|
458
|
+
print(f" {GREEN}Claude Code PreToolUse hook registered{RESET}")
|
|
459
|
+
if not post_already:
|
|
460
|
+
print(f" {GREEN}Claude Code PostToolUse hook registered{RESET}")
|
|
368
461
|
else:
|
|
369
|
-
print(f" {GRAY}Claude Code
|
|
462
|
+
print(f" {GRAY}Claude Code hooks already registered{RESET}")
|
|
370
463
|
|
|
371
464
|
|
|
372
465
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
@@ -529,6 +622,22 @@ def cmd_guard_status(args):
|
|
|
529
622
|
api_key = cfg.get("api_key", "")
|
|
530
623
|
base_url = _api_url(cfg)
|
|
531
624
|
|
|
625
|
+
# Auto-refresh user_email into config if it was installed before this was wired up
|
|
626
|
+
if not user_email and api_key:
|
|
627
|
+
try:
|
|
628
|
+
installed = _req("GET", f"{base_url}/guard/config/installed", api_key=api_key)
|
|
629
|
+
fetched_email = installed.get("user_email") or ""
|
|
630
|
+
if fetched_email:
|
|
631
|
+
cfg["user_email"] = fetched_email
|
|
632
|
+
_save_guard_config(cfg)
|
|
633
|
+
# Rewrite hook script so future events carry the email
|
|
634
|
+
hook_path = GUARD_DIR / "hook.py"
|
|
635
|
+
hook_path.write_text(_HOOK_SCRIPT)
|
|
636
|
+
hook_path.chmod(0o755)
|
|
637
|
+
user_email = fetched_email
|
|
638
|
+
except Exception:
|
|
639
|
+
pass
|
|
640
|
+
|
|
532
641
|
# Load local policy for rule count
|
|
533
642
|
rule_count = 0
|
|
534
643
|
if POLICY_PATH.exists():
|
|
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
|