conduct-cli 0.4.94__tar.gz → 0.4.96__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.94 → conduct_cli-0.4.96}/PKG-INFO +1 -1
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/pyproject.toml +1 -1
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/guard.py +8 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/guardmcp.py +157 -9
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/hook_template.py +40 -1
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/main.py +3 -3
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/README.md +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/setup.cfg +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/setup.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.94 → conduct_cli-0.4.96}/tests/test_switch.py +0 -0
|
@@ -761,6 +761,14 @@ def cmd_guard_sync(args):
|
|
|
761
761
|
|
|
762
762
|
print(f"\n{BOLD}Policy refreshed ({rule_count} rule(s)).{RESET}")
|
|
763
763
|
|
|
764
|
+
# Print Claude.ai remote MCP URL — requires one-time browser paste
|
|
765
|
+
member_token = cfg.get("member_token", "")
|
|
766
|
+
if workspace_id and member_token:
|
|
767
|
+
mcp_url = f"https://api.conductai.ai/guard/mcp?workspace_id={workspace_id}&token={member_token}"
|
|
768
|
+
print(f"\n{BOLD}Claude.ai{RESET} (one-time browser setup):")
|
|
769
|
+
print(f" Settings → MCP Servers → Add custom server → paste:")
|
|
770
|
+
print(f"\n {CYAN}{mcp_url}{RESET}\n")
|
|
771
|
+
|
|
764
772
|
|
|
765
773
|
def _ensure_booster(root: Path) -> None:
|
|
766
774
|
"""Auto-init and background-index booster if installed but not yet set up."""
|
|
@@ -16,6 +16,7 @@ import argparse
|
|
|
16
16
|
import json
|
|
17
17
|
import re
|
|
18
18
|
import sys
|
|
19
|
+
import uuid
|
|
19
20
|
import urllib.request
|
|
20
21
|
import urllib.error
|
|
21
22
|
from pathlib import Path
|
|
@@ -26,6 +27,12 @@ CONFIG_PATH = GUARD_DIR / "config.json"
|
|
|
26
27
|
|
|
27
28
|
PROTOCOL_VERSION = "2024-11-05"
|
|
28
29
|
|
|
30
|
+
# Session ID shared across all events in this MCP server process
|
|
31
|
+
_SESSION_ID = str(uuid.uuid4())
|
|
32
|
+
|
|
33
|
+
# Populated during MCP initialize handshake — used to identify the surface
|
|
34
|
+
_CLIENT_INFO: dict = {}
|
|
35
|
+
|
|
29
36
|
_TOOLS = [
|
|
30
37
|
{
|
|
31
38
|
"name": "guard_status",
|
|
@@ -38,20 +45,28 @@ _TOOLS = [
|
|
|
38
45
|
{
|
|
39
46
|
"name": "guard_check",
|
|
40
47
|
"description": (
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
48
|
+
"ALWAYS call this before executing any of the following actions: "
|
|
49
|
+
"running shell commands, reading or writing files, accessing the network, "
|
|
50
|
+
"calling external APIs, modifying code, deleting data, or any action that "
|
|
51
|
+
"affects the filesystem or environment. "
|
|
52
|
+
"This enforces your team's ConductGuard security policy — the response will "
|
|
53
|
+
"be ALLOWED, BLOCKED, or WARNING. "
|
|
54
|
+
"If BLOCKED: stop immediately and tell the user the policy rule that blocked it. "
|
|
55
|
+
"If WARNING: proceed but surface the warning to the user. "
|
|
56
|
+
"If ALLOWED: proceed normally. "
|
|
57
|
+
"Pass tool_name as the action you are about to take (e.g. 'bash', 'read_file', "
|
|
58
|
+
"'write_file', 'curl', 'git', 'npm') and tool_input as the relevant parameters."
|
|
44
59
|
),
|
|
45
60
|
"inputSchema": {
|
|
46
61
|
"type": "object",
|
|
47
62
|
"properties": {
|
|
48
63
|
"tool_name": {
|
|
49
64
|
"type": "string",
|
|
50
|
-
"description": "
|
|
65
|
+
"description": "The action you are about to take (e.g. bash, read_file, write_file, curl, git)",
|
|
51
66
|
},
|
|
52
67
|
"tool_input": {
|
|
53
68
|
"type": "object",
|
|
54
|
-
"description": "
|
|
69
|
+
"description": "Relevant parameters — e.g. {\"command\": \"rm -rf /\"} or {\"file_path\": \"/etc/passwd\"}",
|
|
55
70
|
},
|
|
56
71
|
},
|
|
57
72
|
"required": ["tool_name"],
|
|
@@ -65,6 +80,39 @@ _TOOLS = [
|
|
|
65
80
|
),
|
|
66
81
|
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
67
82
|
},
|
|
83
|
+
{
|
|
84
|
+
"name": "guard_enable",
|
|
85
|
+
"description": (
|
|
86
|
+
"Call this when the user asks to 'enable conductguard', 'load mcp', 'activate guard', "
|
|
87
|
+
"or any similar onboarding request. Confirms ConductGuard is connected, returns the "
|
|
88
|
+
"number of active policy rules, and provides the Project Instruction snippet the user "
|
|
89
|
+
"should paste into their Claude.ai Project settings to make guard_check fire automatically."
|
|
90
|
+
),
|
|
91
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "guard_activity",
|
|
95
|
+
"description": (
|
|
96
|
+
"ALWAYS call this at the start of every conversation, immediately after the user sends "
|
|
97
|
+
"their first message. Pass a one-line summary of what the user is asking you to do. "
|
|
98
|
+
"This logs session intent to the team's ConductGuard audit trail so admins can see "
|
|
99
|
+
"what work is being done across the team's AI usage."
|
|
100
|
+
),
|
|
101
|
+
"inputSchema": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"summary": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "One-line summary of what the user is asking you to do in this conversation.",
|
|
107
|
+
},
|
|
108
|
+
"category": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Optional category: coding, debugging, review, research, writing, devops, security, other",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
"required": ["summary"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
68
116
|
]
|
|
69
117
|
|
|
70
118
|
|
|
@@ -88,6 +136,64 @@ def _load_config() -> dict:
|
|
|
88
136
|
return {}
|
|
89
137
|
|
|
90
138
|
|
|
139
|
+
def _detect_surface(client_info: dict) -> str:
|
|
140
|
+
"""Map MCP clientInfo.name → ai_tool label sent to Guard API."""
|
|
141
|
+
name = (client_info.get("name") or "").lower()
|
|
142
|
+
if "desktop" in name:
|
|
143
|
+
return "claude_desktop"
|
|
144
|
+
if "work" in name or "teams" in name or "enterprise" in name:
|
|
145
|
+
return "claude_work"
|
|
146
|
+
if "claude" in name:
|
|
147
|
+
return "claude_chat"
|
|
148
|
+
if "codex" in name:
|
|
149
|
+
return "codex"
|
|
150
|
+
if "cursor" in name:
|
|
151
|
+
return "cursor"
|
|
152
|
+
if "windsurf" in name:
|
|
153
|
+
return "windsurf"
|
|
154
|
+
return "unknown"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _post_audit_event(
|
|
158
|
+
tool_name: str,
|
|
159
|
+
tool_input: dict,
|
|
160
|
+
decision: str,
|
|
161
|
+
rule_id: str | None,
|
|
162
|
+
workspace_id: str,
|
|
163
|
+
token: str,
|
|
164
|
+
ai_tool: str,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Fire-and-forget: post a guard audit event to the Conduct API."""
|
|
167
|
+
if not workspace_id:
|
|
168
|
+
return
|
|
169
|
+
cfg = _load_config()
|
|
170
|
+
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
171
|
+
payload = json.dumps({
|
|
172
|
+
"workspace_id": workspace_id,
|
|
173
|
+
"clerk_user_id": cfg.get("user_email", ""),
|
|
174
|
+
"user_email": cfg.get("user_email", ""),
|
|
175
|
+
"ai_tool": ai_tool,
|
|
176
|
+
"tool_call": tool_name,
|
|
177
|
+
"input_summary": json.dumps(tool_input)[:200],
|
|
178
|
+
"decision": decision,
|
|
179
|
+
"rule_id": rule_id,
|
|
180
|
+
"hook_session_id": _SESSION_ID,
|
|
181
|
+
}).encode()
|
|
182
|
+
try:
|
|
183
|
+
req = urllib.request.Request(
|
|
184
|
+
f"{api_url}/guard/events",
|
|
185
|
+
data=payload,
|
|
186
|
+
headers={
|
|
187
|
+
"Content-Type": "application/json",
|
|
188
|
+
"Authorization": f"Bearer {token}",
|
|
189
|
+
},
|
|
190
|
+
method="POST",
|
|
191
|
+
)
|
|
192
|
+
urllib.request.urlopen(req, timeout=5)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass # never crash the MCP server over telemetry
|
|
195
|
+
|
|
196
|
+
|
|
91
197
|
def _match_policy(tool_name: str, tool_input: dict) -> dict | None:
|
|
92
198
|
"""Return the first matching rule dict, or None if no rule fires."""
|
|
93
199
|
policy = _load_policy()
|
|
@@ -137,12 +243,13 @@ def _handle_guard_status(workspace_id: str) -> str:
|
|
|
137
243
|
}, indent=2)
|
|
138
244
|
|
|
139
245
|
|
|
140
|
-
def _handle_guard_check(arguments: dict) -> str:
|
|
246
|
+
def _handle_guard_check(arguments: dict, workspace_id: str, token: str, ai_tool: str) -> str:
|
|
141
247
|
tool_name = arguments.get("tool_name", "")
|
|
142
248
|
tool_input = arguments.get("tool_input") or {}
|
|
143
249
|
|
|
144
250
|
rule = _match_policy(tool_name, tool_input)
|
|
145
251
|
if rule is None:
|
|
252
|
+
_post_audit_event(tool_name, tool_input, "allowed", None, workspace_id, token, ai_tool)
|
|
146
253
|
return f"ALLOWED — no policy rule matches '{tool_name}'."
|
|
147
254
|
|
|
148
255
|
action = rule.get("action", "audit")
|
|
@@ -150,9 +257,13 @@ def _handle_guard_check(arguments: dict) -> str:
|
|
|
150
257
|
message = rule.get("message") or f"Policy violation ({rule_id})"
|
|
151
258
|
|
|
152
259
|
if action == "block":
|
|
260
|
+
_post_audit_event(tool_name, tool_input, "blocked", rule_id, workspace_id, token, ai_tool)
|
|
153
261
|
return f"BLOCKED — {message} [rule: {rule_id}]"
|
|
154
262
|
if action in ("warn", "approval"):
|
|
263
|
+
_post_audit_event(tool_name, tool_input, "warned", rule_id, workspace_id, token, ai_tool)
|
|
155
264
|
return f"WARNING — {message} [rule: {rule_id}]"
|
|
265
|
+
|
|
266
|
+
_post_audit_event(tool_name, tool_input, "audited", rule_id, workspace_id, token, ai_tool)
|
|
156
267
|
return f"AUDITED — {message} [rule: {rule_id}]"
|
|
157
268
|
|
|
158
269
|
|
|
@@ -181,13 +292,35 @@ def _handle_guard_sync(workspace_id: str, token: str) -> str:
|
|
|
181
292
|
return f"Sync failed — {e}"
|
|
182
293
|
|
|
183
294
|
|
|
184
|
-
def _dispatch_tool(name: str, arguments: dict, workspace_id: str, token: str) -> str:
|
|
295
|
+
def _dispatch_tool(name: str, arguments: dict, workspace_id: str, token: str, ai_tool: str) -> str:
|
|
185
296
|
if name == "guard_status":
|
|
186
297
|
return _handle_guard_status(workspace_id)
|
|
187
298
|
if name == "guard_check":
|
|
188
|
-
return _handle_guard_check(arguments)
|
|
299
|
+
return _handle_guard_check(arguments, workspace_id, token, ai_tool)
|
|
189
300
|
if name == "guard_sync":
|
|
190
301
|
return _handle_guard_sync(workspace_id, token)
|
|
302
|
+
if name == "guard_activity":
|
|
303
|
+
summary = arguments.get("summary", "")
|
|
304
|
+
category = arguments.get("category", "other")
|
|
305
|
+
_post_audit_event("guard_activity", {"summary": summary, "category": category}, "allowed", None, workspace_id, token, ai_tool)
|
|
306
|
+
return f"Activity logged — '{summary}'"
|
|
307
|
+
if name == "guard_enable":
|
|
308
|
+
policy = _load_policy()
|
|
309
|
+
rule_count = len(policy.get("rules", []))
|
|
310
|
+
snippet = (
|
|
311
|
+
"You have ConductGuard active. "
|
|
312
|
+
"ALWAYS call guard_check before running shell commands, reading or writing files, "
|
|
313
|
+
"calling external APIs, or any action that affects the filesystem or environment. "
|
|
314
|
+
"If the response is BLOCKED, stop immediately and explain the policy rule to the user. "
|
|
315
|
+
"If WARNING, proceed but surface the warning."
|
|
316
|
+
)
|
|
317
|
+
return (
|
|
318
|
+
f"✓ ConductGuard is connected — {rule_count} active rule(s).\n\n"
|
|
319
|
+
f"To make Guard fire automatically on every conversation, paste this into your "
|
|
320
|
+
f"Claude.ai Project Instructions (Projects → your project → Instructions):\n\n"
|
|
321
|
+
f"---\n{snippet}\n---\n\n"
|
|
322
|
+
f"Until then, Guard is active for this conversation only."
|
|
323
|
+
)
|
|
191
324
|
return f"Unknown tool: {name}"
|
|
192
325
|
|
|
193
326
|
|
|
@@ -217,6 +350,7 @@ def main() -> None:
|
|
|
217
350
|
cfg = _load_config()
|
|
218
351
|
workspace_id = args.workspace or cfg.get("workspace_id", "")
|
|
219
352
|
token = args.token or cfg.get("member_token", "")
|
|
353
|
+
ai_tool = "unknown" # resolved from clientInfo during initialize
|
|
220
354
|
|
|
221
355
|
for raw in sys.stdin:
|
|
222
356
|
raw = raw.strip()
|
|
@@ -233,10 +367,24 @@ def main() -> None:
|
|
|
233
367
|
params = msg.get("params") or {}
|
|
234
368
|
|
|
235
369
|
if method == "initialize":
|
|
370
|
+
client_info = params.get("clientInfo") or {}
|
|
371
|
+
_CLIENT_INFO.update(client_info)
|
|
372
|
+
ai_tool = _detect_surface(client_info)
|
|
236
373
|
_ok(msg_id, {
|
|
237
374
|
"protocolVersion": PROTOCOL_VERSION,
|
|
238
375
|
"capabilities": {"tools": {}},
|
|
239
376
|
"serverInfo": {"name": "conductguard", "version": "1.0.0"},
|
|
377
|
+
"instructions": (
|
|
378
|
+
"ConductGuard is active and enforcing your team's security policy. "
|
|
379
|
+
"ALWAYS call guard_activity at the start of every conversation with a one-line summary of what the user is asking you to do. "
|
|
380
|
+
"ALWAYS call guard_check before executing any of the following: "
|
|
381
|
+
"shell commands, file reads or writes, network requests, external API calls, "
|
|
382
|
+
"code modifications, or any action that affects the filesystem or environment. "
|
|
383
|
+
"If the response is BLOCKED: stop immediately and explain the policy rule to the user. "
|
|
384
|
+
"If WARNING: proceed but surface the warning to the user. "
|
|
385
|
+
"If ALLOWED: proceed normally. "
|
|
386
|
+
"Never skip guard_activity or guard_check — both are required for compliance."
|
|
387
|
+
),
|
|
240
388
|
})
|
|
241
389
|
|
|
242
390
|
elif method == "notifications/initialized":
|
|
@@ -248,7 +396,7 @@ def main() -> None:
|
|
|
248
396
|
elif method == "tools/call":
|
|
249
397
|
tool_name = params.get("name", "")
|
|
250
398
|
arguments = params.get("arguments") or {}
|
|
251
|
-
text = _dispatch_tool(tool_name, arguments, workspace_id, token)
|
|
399
|
+
text = _dispatch_tool(tool_name, arguments, workspace_id, token, ai_tool)
|
|
252
400
|
_ok(msg_id, {"content": [{"type": "text", "text": text}]})
|
|
253
401
|
|
|
254
402
|
elif method == "ping":
|
|
@@ -15,6 +15,7 @@ BUDGET_CACHE_PATH = GUARD_DIR / "budget_cache.json"
|
|
|
15
15
|
BUDGET_CACHE_TTL = 300 # 5 minutes
|
|
16
16
|
VERSION_CACHE_PATH = GUARD_DIR / "version_cache.json"
|
|
17
17
|
VERSION_CACHE_TTL = 60 # 1 minute — matches server poll window
|
|
18
|
+
WARNED_RULES_PATH = GUARD_DIR / "warned_rules.json"
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _maybe_sync_policy():
|
|
@@ -159,6 +160,35 @@ def _detect_ai_tool():
|
|
|
159
160
|
return "unknown"
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
def _already_warned_this_session(session_id: str, rule_id: str) -> bool:
|
|
164
|
+
"""Return True if this rule already fired a warning in the current session."""
|
|
165
|
+
try:
|
|
166
|
+
data = json.loads(WARNED_RULES_PATH.read_text()) if WARNED_RULES_PATH.exists() else {}
|
|
167
|
+
except Exception:
|
|
168
|
+
data = {}
|
|
169
|
+
return rule_id in data.get(session_id, [])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _record_session_warn(session_id: str, rule_id: str) -> None:
|
|
173
|
+
try:
|
|
174
|
+
data = json.loads(WARNED_RULES_PATH.read_text()) if WARNED_RULES_PATH.exists() else {}
|
|
175
|
+
except Exception:
|
|
176
|
+
data = {}
|
|
177
|
+
data.setdefault(session_id, [])
|
|
178
|
+
if rule_id not in data[session_id]:
|
|
179
|
+
data[session_id].append(rule_id)
|
|
180
|
+
# Trim to last 50 sessions to prevent unbounded growth
|
|
181
|
+
if len(data) > 50:
|
|
182
|
+
oldest = list(data.keys())[:-50]
|
|
183
|
+
for k in oldest:
|
|
184
|
+
del data[k]
|
|
185
|
+
try:
|
|
186
|
+
GUARD_DIR.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
WARNED_RULES_PATH.write_text(json.dumps(data))
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
162
192
|
def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, session_id=None):
|
|
163
193
|
try:
|
|
164
194
|
cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
|
@@ -533,7 +563,12 @@ def post_usage_main():
|
|
|
533
563
|
_, action, rule_id, message = _check_policy(tool_name, {}, tokens_before=tokens_input)
|
|
534
564
|
if action in ("warn", "block"):
|
|
535
565
|
decision = "warned" if action == "warn" else "blocked"
|
|
536
|
-
|
|
566
|
+
if action == "warn" and session_id and rule_id and _already_warned_this_session(session_id, rule_id):
|
|
567
|
+
pass # already warned once this session — skip
|
|
568
|
+
else:
|
|
569
|
+
if action == "warn" and session_id and rule_id:
|
|
570
|
+
_record_session_warn(session_id, rule_id)
|
|
571
|
+
_post_event(tool_name, {}, decision, rule_id, message, session_id=session_id)
|
|
537
572
|
|
|
538
573
|
# Security classifier runs regardless of transcript_path — scan every tool response
|
|
539
574
|
tool_response = data.get("tool_response") or data.get("output") or ""
|
|
@@ -584,6 +619,10 @@ def main():
|
|
|
584
619
|
|
|
585
620
|
# Always post an event — "allowed" for normal calls, "blocked"/"warned" for violations
|
|
586
621
|
decision = {"block": "blocked", "warn": "warned", "approval": "blocked"}.get(action, "allowed")
|
|
622
|
+
if action == "warn" and session_id and rule_id and _already_warned_this_session(session_id, rule_id):
|
|
623
|
+
sys.exit(0) # already warned once this session — skip silently
|
|
624
|
+
if action == "warn" and session_id and rule_id:
|
|
625
|
+
_record_session_warn(session_id, rule_id)
|
|
587
626
|
_post_event(tool_name, tool_input, decision, rule_id, message, session_id=session_id)
|
|
588
627
|
|
|
589
628
|
if action == "block":
|
|
@@ -2334,7 +2334,7 @@ def cmd_emit_finding(args):
|
|
|
2334
2334
|
print(finding_id)
|
|
2335
2335
|
|
|
2336
2336
|
|
|
2337
|
-
def _gh_api_get(url: str, token: str)
|
|
2337
|
+
def _gh_api_get(url: str, token: str):
|
|
2338
2338
|
import urllib.request, urllib.error
|
|
2339
2339
|
req = urllib.request.Request(url, headers={
|
|
2340
2340
|
"Authorization": f"Bearer {token}",
|
|
@@ -2346,14 +2346,14 @@ def _gh_api_get(url: str, token: str) -> dict | list:
|
|
|
2346
2346
|
|
|
2347
2347
|
|
|
2348
2348
|
def _fetch_github_issue(repo: str, issue_number: int, token: str) -> dict:
|
|
2349
|
-
return _gh_api_get(f"https://api.github.com/repos/{repo}/issues/{issue_number}", token)
|
|
2349
|
+
return _gh_api_get(f"https://api.github.com/repos/{repo}/issues/{issue_number}", token)
|
|
2350
2350
|
|
|
2351
2351
|
|
|
2352
2352
|
def _fetch_issues_by_label(repo: str, label: str, token: str) -> list:
|
|
2353
2353
|
return _gh_api_get(
|
|
2354
2354
|
f"https://api.github.com/repos/{repo}/issues?labels={label}&state=open&per_page=20",
|
|
2355
2355
|
token,
|
|
2356
|
-
)
|
|
2356
|
+
)
|
|
2357
2357
|
|
|
2358
2358
|
|
|
2359
2359
|
def _build_issue_trigger_payload(issue: dict, repo: str) -> dict:
|
|
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
|