conduct-cli 0.4.27__tar.gz → 0.4.29__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.27 → conduct_cli-0.4.29}/PKG-INFO +1 -1
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/pyproject.toml +4 -3
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli/main.py +159 -0
- conduct_cli-0.4.29/src/conduct_cli/mcp_server.py +334 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli.egg-info/entry_points.txt +1 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/README.md +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/setup.cfg +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/setup.py +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.27 → conduct_cli-0.4.29}/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.29"
|
|
8
8
|
description = "CLI for Conduct AI — install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -31,8 +31,9 @@ Repository = "https://github.com/sseshachala/conductai"
|
|
|
31
31
|
"Bug Tracker" = "https://github.com/sseshachala/conductai/issues"
|
|
32
32
|
|
|
33
33
|
[project.scripts]
|
|
34
|
-
conduct
|
|
35
|
-
|
|
34
|
+
conduct = "conduct_cli.main:main"
|
|
35
|
+
conduct-mcp = "conduct_cli.mcp_server:main"
|
|
36
|
+
conductguard-mcp = "conduct_cli.guardmcp:main"
|
|
36
37
|
conductguard-post = "conduct_cli.guard:post_usage_main"
|
|
37
38
|
|
|
38
39
|
[tool.setuptools.packages.find]
|
|
@@ -192,6 +192,148 @@ def _poll_run(server: str, workflow_id: str, run_id: str, hdrs: dict) -> bool:
|
|
|
192
192
|
|
|
193
193
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
|
194
194
|
|
|
195
|
+
def _write_claude_mcp_settings() -> bool:
|
|
196
|
+
"""Write conduct-mcp into ~/.claude/settings.json. Returns True if written."""
|
|
197
|
+
settings_path = Path.home() / ".claude" / "settings.json"
|
|
198
|
+
try:
|
|
199
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
200
|
+
servers = existing.setdefault("mcpServers", {})
|
|
201
|
+
if "conduct" in servers:
|
|
202
|
+
return True # already registered
|
|
203
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
204
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
206
|
+
return True
|
|
207
|
+
except Exception:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _write_codex_mcp_config() -> bool:
|
|
212
|
+
"""Write conduct-mcp into ~/.codex/config.toml. Returns True if written."""
|
|
213
|
+
codex_dir = Path.home() / ".codex"
|
|
214
|
+
if not codex_dir.exists():
|
|
215
|
+
return False
|
|
216
|
+
config_path = codex_dir / "config.toml"
|
|
217
|
+
try:
|
|
218
|
+
content = config_path.read_text() if config_path.exists() else ""
|
|
219
|
+
if "conduct-mcp" in content:
|
|
220
|
+
return True
|
|
221
|
+
mcp_block = '\n[[mcp_servers]]\nname = "conduct"\ncommand = "conduct-mcp"\nargs = []\n'
|
|
222
|
+
config_path.write_text(content + mcp_block)
|
|
223
|
+
return True
|
|
224
|
+
except Exception:
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _write_cursor_mcp_config() -> bool:
|
|
229
|
+
"""Write conduct-mcp into ~/.cursor/mcp.json. Returns True if written."""
|
|
230
|
+
cursor_dir = Path.home() / ".cursor"
|
|
231
|
+
if not cursor_dir.exists():
|
|
232
|
+
return False
|
|
233
|
+
config_path = cursor_dir / "mcp.json"
|
|
234
|
+
try:
|
|
235
|
+
existing = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
236
|
+
servers = existing.setdefault("mcpServers", {})
|
|
237
|
+
if "conduct" in servers:
|
|
238
|
+
return True
|
|
239
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
240
|
+
config_path.write_text(json.dumps(existing, indent=2))
|
|
241
|
+
return True
|
|
242
|
+
except Exception:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_windsurf_mcp_config() -> bool:
|
|
247
|
+
"""Write conduct-mcp into ~/.codeium/windsurf/mcp_config.json. Returns True if written."""
|
|
248
|
+
config_path = Path.home() / ".codeium" / "windsurf" / "mcp_config.json"
|
|
249
|
+
if not config_path.parent.exists():
|
|
250
|
+
return False
|
|
251
|
+
try:
|
|
252
|
+
existing = json.loads(config_path.read_text()) if config_path.exists() else {}
|
|
253
|
+
servers = existing.setdefault("mcpServers", {})
|
|
254
|
+
if "conduct" in servers:
|
|
255
|
+
return True
|
|
256
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
257
|
+
config_path.write_text(json.dumps(existing, indent=2))
|
|
258
|
+
return True
|
|
259
|
+
except Exception:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _write_vscode_mcp_config() -> bool:
|
|
264
|
+
"""Write conduct-mcp into VS Code settings.json (mcp.servers). Returns True if written."""
|
|
265
|
+
# Check both standard locations
|
|
266
|
+
candidates = [
|
|
267
|
+
Path.home() / ".vscode" / "settings.json",
|
|
268
|
+
Path.home() / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
269
|
+
Path.home() / ".config" / "Code" / "User" / "settings.json",
|
|
270
|
+
]
|
|
271
|
+
settings_path = next((p for p in candidates if p.exists()), None)
|
|
272
|
+
if not settings_path:
|
|
273
|
+
return False
|
|
274
|
+
try:
|
|
275
|
+
existing = json.loads(settings_path.read_text()) if settings_path.exists() else {}
|
|
276
|
+
servers = existing.setdefault("mcp", {}).setdefault("servers", {})
|
|
277
|
+
if "conduct" in servers:
|
|
278
|
+
return True
|
|
279
|
+
servers["conduct"] = {"command": "conduct-mcp", "args": []}
|
|
280
|
+
settings_path.write_text(json.dumps(existing, indent=2))
|
|
281
|
+
return True
|
|
282
|
+
except Exception:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cmd_mcp_install(args):
|
|
287
|
+
"""Register conduct-mcp in Claude Code, Codex, Cursor, Windsurf, and VS Code."""
|
|
288
|
+
import shutil
|
|
289
|
+
import subprocess
|
|
290
|
+
|
|
291
|
+
registered = []
|
|
292
|
+
|
|
293
|
+
# --- Claude Code ---
|
|
294
|
+
if shutil.which("claude"):
|
|
295
|
+
try:
|
|
296
|
+
result = subprocess.run(
|
|
297
|
+
["claude", "mcp", "add", "conduct", "conduct-mcp"],
|
|
298
|
+
capture_output=True, text=True, timeout=15,
|
|
299
|
+
)
|
|
300
|
+
if result.returncode == 0:
|
|
301
|
+
registered.append("Claude Code")
|
|
302
|
+
else:
|
|
303
|
+
_write_claude_mcp_settings()
|
|
304
|
+
registered.append("Claude Code (settings.json)")
|
|
305
|
+
except Exception:
|
|
306
|
+
_write_claude_mcp_settings()
|
|
307
|
+
registered.append("Claude Code (settings.json)")
|
|
308
|
+
else:
|
|
309
|
+
if _write_claude_mcp_settings():
|
|
310
|
+
registered.append("Claude Code (settings.json)")
|
|
311
|
+
|
|
312
|
+
# --- Codex CLI ---
|
|
313
|
+
if _write_codex_mcp_config():
|
|
314
|
+
registered.append("Codex")
|
|
315
|
+
|
|
316
|
+
# --- Cursor ---
|
|
317
|
+
if _write_cursor_mcp_config():
|
|
318
|
+
registered.append("Cursor")
|
|
319
|
+
|
|
320
|
+
# --- Windsurf ---
|
|
321
|
+
if _write_windsurf_mcp_config():
|
|
322
|
+
registered.append("Windsurf")
|
|
323
|
+
|
|
324
|
+
# --- VS Code (Copilot) ---
|
|
325
|
+
if _write_vscode_mcp_config():
|
|
326
|
+
registered.append("VS Code (Copilot)")
|
|
327
|
+
|
|
328
|
+
if registered:
|
|
329
|
+
print(f"{GREEN}✓ conduct-mcp registered in: {', '.join(registered)}{RESET}")
|
|
330
|
+
print(f"{GRAY} Restart your AI tools to pick up the new MCP server.{RESET}")
|
|
331
|
+
else:
|
|
332
|
+
print(f"{YELLOW}⚠ No supported AI tools detected on this machine.{RESET}")
|
|
333
|
+
print(f"{GRAY} Supported: Claude Code, Codex, Cursor, Windsurf, VS Code{RESET}")
|
|
334
|
+
print(f"{GRAY} After installing any of these, re-run: conduct mcp install{RESET}")
|
|
335
|
+
|
|
336
|
+
|
|
195
337
|
def cmd_login(args):
|
|
196
338
|
server = args.server
|
|
197
339
|
api_key = args.api_key
|
|
@@ -254,6 +396,13 @@ def cmd_login(args):
|
|
|
254
396
|
except Exception:
|
|
255
397
|
pass # Never block login on Guard errors
|
|
256
398
|
|
|
399
|
+
# Auto-register MCP servers in Claude Code / Codex
|
|
400
|
+
try:
|
|
401
|
+
import types
|
|
402
|
+
cmd_mcp_install(types.SimpleNamespace())
|
|
403
|
+
except Exception:
|
|
404
|
+
pass # Never block login on MCP registration errors
|
|
405
|
+
|
|
257
406
|
|
|
258
407
|
def cmd_agents(args):
|
|
259
408
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
@@ -1179,6 +1328,11 @@ def main():
|
|
|
1179
1328
|
# conduct guard
|
|
1180
1329
|
guard_p, _guard_sub = _guard.register_guard_parser(sub)
|
|
1181
1330
|
|
|
1331
|
+
# conduct mcp
|
|
1332
|
+
mcp_p = sub.add_parser("mcp", help="Manage the Conduct MCP server")
|
|
1333
|
+
mcp_sub = mcp_p.add_subparsers(dest="mcp_command")
|
|
1334
|
+
mcp_sub.add_parser("install", help="Register conduct-mcp in Claude Code and Codex")
|
|
1335
|
+
|
|
1182
1336
|
args = parser.parse_args()
|
|
1183
1337
|
|
|
1184
1338
|
if args.command == "login":
|
|
@@ -1223,6 +1377,11 @@ def main():
|
|
|
1223
1377
|
cmd_run(args)
|
|
1224
1378
|
elif args.command == "guard":
|
|
1225
1379
|
_guard.dispatch_guard(args, guard_p)
|
|
1380
|
+
elif args.command == "mcp":
|
|
1381
|
+
if getattr(args, "mcp_command", None) == "install":
|
|
1382
|
+
cmd_mcp_install(args)
|
|
1383
|
+
else:
|
|
1384
|
+
mcp_p.print_help()
|
|
1226
1385
|
else:
|
|
1227
1386
|
parser.print_help()
|
|
1228
1387
|
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
conduct-mcp — Conduct AI MCP server.
|
|
4
|
+
|
|
5
|
+
Runs as a subprocess started by Claude Code / Cursor / Windsurf via the
|
|
6
|
+
mcpServers config written by `conduct mcp install`. Communicates over
|
|
7
|
+
stdin/stdout using JSON-RPC 2.0 (MCP stdio transport).
|
|
8
|
+
|
|
9
|
+
Exposes six tools:
|
|
10
|
+
conduct_list_agents — list agents in the workspace
|
|
11
|
+
conduct_list_projects — list projects in the workspace
|
|
12
|
+
conduct_list_playbooks — list available playbook templates
|
|
13
|
+
conduct_run_workflow — trigger a workflow run
|
|
14
|
+
conduct_get_run — get run status / result
|
|
15
|
+
conduct_guard_status — ConductGuard policy status
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
CONDUCT_CONFIG_PATH = Path.home() / ".conduct" / "config.json"
|
|
26
|
+
GUARD_DIR = Path.home() / ".conductguard"
|
|
27
|
+
GUARD_POLICY_PATH = GUARD_DIR / "policy.json"
|
|
28
|
+
GUARD_CONFIG_PATH = GUARD_DIR / "config.json"
|
|
29
|
+
|
|
30
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
31
|
+
|
|
32
|
+
_TOOLS = [
|
|
33
|
+
{
|
|
34
|
+
"name": "conduct_list_agents",
|
|
35
|
+
"description": "List all installed agents in your Conduct workspace.",
|
|
36
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "conduct_list_projects",
|
|
40
|
+
"description": "List all projects in your Conduct workspace.",
|
|
41
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "conduct_list_playbooks",
|
|
45
|
+
"description": "List available Conduct playbooks (workflow templates).",
|
|
46
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "conduct_run_workflow",
|
|
50
|
+
"description": "Trigger a workflow run in Conduct. Returns the run ID.",
|
|
51
|
+
"inputSchema": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"workflow_id": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "The workflow UUID to run",
|
|
57
|
+
},
|
|
58
|
+
"payload": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"description": "Optional trigger payload (key-value pairs)",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"required": ["workflow_id"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "conduct_get_run",
|
|
68
|
+
"description": "Get the status and result of a workflow run.",
|
|
69
|
+
"inputSchema": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {
|
|
72
|
+
"workflow_id": {"type": "string"},
|
|
73
|
+
"run_id": {"type": "string"},
|
|
74
|
+
},
|
|
75
|
+
"required": ["workflow_id", "run_id"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "conduct_guard_status",
|
|
80
|
+
"description": (
|
|
81
|
+
"Show current ConductGuard policy status — active rules, "
|
|
82
|
+
"today's spend, and your team info."
|
|
83
|
+
),
|
|
84
|
+
"inputSchema": {"type": "object", "properties": {}, "required": []},
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Config helpers ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def _load_conduct_config() -> dict:
|
|
92
|
+
if CONDUCT_CONFIG_PATH.exists():
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(CONDUCT_CONFIG_PATH.read_text())
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_guard_policy() -> dict:
|
|
101
|
+
if GUARD_POLICY_PATH.exists():
|
|
102
|
+
try:
|
|
103
|
+
return json.loads(GUARD_POLICY_PATH.read_text())
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return {"rules": []}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _load_guard_config() -> dict:
|
|
110
|
+
if GUARD_CONFIG_PATH.exists():
|
|
111
|
+
try:
|
|
112
|
+
return json.loads(GUARD_CONFIG_PATH.read_text())
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ── HTTP helpers ───────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def _api_get(url: str, token: str | None, api_key: str | None) -> dict | list:
|
|
121
|
+
headers = {"Content-Type": "application/json"}
|
|
122
|
+
if token:
|
|
123
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
124
|
+
elif api_key:
|
|
125
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
126
|
+
req = urllib.request.Request(url, headers=headers)
|
|
127
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
128
|
+
return json.loads(resp.read())
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _api_post(url: str, body: dict, token: str | None, api_key: str | None) -> dict | list:
|
|
132
|
+
headers = {"Content-Type": "application/json"}
|
|
133
|
+
if token:
|
|
134
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
135
|
+
elif api_key:
|
|
136
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
137
|
+
data = json.dumps(body).encode()
|
|
138
|
+
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
|
|
139
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
140
|
+
return json.loads(resp.read())
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── Tool handlers ──────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def _handle_list_agents(server: str, workspace_id: str, token: str | None, api_key: str | None) -> str:
|
|
146
|
+
try:
|
|
147
|
+
url = f"{server}/agents?workspace_id={workspace_id}"
|
|
148
|
+
result = _api_get(url, token, api_key)
|
|
149
|
+
agents = [
|
|
150
|
+
{"id": a.get("id"), "name": a.get("name"), "status": a.get("status")}
|
|
151
|
+
for a in (result if isinstance(result, list) else result.get("items", []))
|
|
152
|
+
]
|
|
153
|
+
return json.dumps(agents, indent=2)
|
|
154
|
+
except urllib.error.HTTPError as e:
|
|
155
|
+
return f"Error — HTTP {e.code}: {e.read().decode()[:200]}"
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return f"Error — {e}"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _handle_list_projects(server: str, workspace_id: str, token: str | None, api_key: str | None) -> str:
|
|
161
|
+
try:
|
|
162
|
+
url = f"{server}/projects?workspace_id={workspace_id}"
|
|
163
|
+
result = _api_get(url, token, api_key)
|
|
164
|
+
return json.dumps(result, indent=2)
|
|
165
|
+
except urllib.error.HTTPError as e:
|
|
166
|
+
return f"Error — HTTP {e.code}: {e.read().decode()[:200]}"
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return f"Error — {e}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _handle_list_playbooks(server: str, workspace_id: str, token: str | None, api_key: str | None) -> str:
|
|
172
|
+
try:
|
|
173
|
+
url = f"{server}/playbooks?workspace_id={workspace_id}"
|
|
174
|
+
result = _api_get(url, token, api_key)
|
|
175
|
+
return json.dumps(result, indent=2)
|
|
176
|
+
except urllib.error.HTTPError as e:
|
|
177
|
+
return f"Error — HTTP {e.code}: {e.read().decode()[:200]}"
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return f"Error — {e}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _handle_run_workflow(
|
|
183
|
+
arguments: dict,
|
|
184
|
+
server: str,
|
|
185
|
+
workspace_id: str,
|
|
186
|
+
token: str | None,
|
|
187
|
+
api_key: str | None,
|
|
188
|
+
) -> str:
|
|
189
|
+
workflow_id = arguments.get("workflow_id", "")
|
|
190
|
+
payload = arguments.get("payload") or {}
|
|
191
|
+
if not workflow_id:
|
|
192
|
+
return "Error — workflow_id is required."
|
|
193
|
+
try:
|
|
194
|
+
url = f"{server}/workflows/{workflow_id}/runs"
|
|
195
|
+
body = {"workspace_id": workspace_id, "payload": payload}
|
|
196
|
+
run = _api_post(url, body, token, api_key)
|
|
197
|
+
return json.dumps({"run_id": run.get("id") or run.get("run_id"), "status": run.get("status")}, indent=2)
|
|
198
|
+
except urllib.error.HTTPError as e:
|
|
199
|
+
return f"Error — HTTP {e.code}: {e.read().decode()[:200]}"
|
|
200
|
+
except Exception as e:
|
|
201
|
+
return f"Error — {e}"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _handle_get_run(
|
|
205
|
+
arguments: dict,
|
|
206
|
+
server: str,
|
|
207
|
+
token: str | None,
|
|
208
|
+
api_key: str | None,
|
|
209
|
+
) -> str:
|
|
210
|
+
workflow_id = arguments.get("workflow_id", "")
|
|
211
|
+
run_id = arguments.get("run_id", "")
|
|
212
|
+
if not workflow_id or not run_id:
|
|
213
|
+
return "Error — workflow_id and run_id are both required."
|
|
214
|
+
try:
|
|
215
|
+
url = f"{server}/workflows/{workflow_id}/runs/{run_id}"
|
|
216
|
+
run = _api_get(url, token, api_key)
|
|
217
|
+
return json.dumps(
|
|
218
|
+
{
|
|
219
|
+
"status": run.get("status"),
|
|
220
|
+
"outcome": run.get("outcome"),
|
|
221
|
+
"started_at": run.get("started_at"),
|
|
222
|
+
"completed_at": run.get("completed_at"),
|
|
223
|
+
},
|
|
224
|
+
indent=2,
|
|
225
|
+
)
|
|
226
|
+
except urllib.error.HTTPError as e:
|
|
227
|
+
return f"Error — HTTP {e.code}: {e.read().decode()[:200]}"
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return f"Error — {e}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _handle_guard_status(workspace_id: str) -> str:
|
|
233
|
+
cfg = _load_guard_config()
|
|
234
|
+
policy = _load_guard_policy()
|
|
235
|
+
return json.dumps(
|
|
236
|
+
{
|
|
237
|
+
"workspace_id": workspace_id,
|
|
238
|
+
"email": cfg.get("user_email", ""),
|
|
239
|
+
"rules_active": len(policy.get("rules", [])),
|
|
240
|
+
"policy_version": policy.get("version", ""),
|
|
241
|
+
},
|
|
242
|
+
indent=2,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _dispatch_tool(
|
|
247
|
+
name: str,
|
|
248
|
+
arguments: dict,
|
|
249
|
+
server: str,
|
|
250
|
+
workspace_id: str,
|
|
251
|
+
token: str | None,
|
|
252
|
+
api_key: str | None,
|
|
253
|
+
) -> str:
|
|
254
|
+
if name == "conduct_list_agents":
|
|
255
|
+
return _handle_list_agents(server, workspace_id, token, api_key)
|
|
256
|
+
if name == "conduct_list_projects":
|
|
257
|
+
return _handle_list_projects(server, workspace_id, token, api_key)
|
|
258
|
+
if name == "conduct_list_playbooks":
|
|
259
|
+
return _handle_list_playbooks(server, workspace_id, token, api_key)
|
|
260
|
+
if name == "conduct_run_workflow":
|
|
261
|
+
return _handle_run_workflow(arguments, server, workspace_id, token, api_key)
|
|
262
|
+
if name == "conduct_get_run":
|
|
263
|
+
return _handle_get_run(arguments, server, token, api_key)
|
|
264
|
+
if name == "conduct_guard_status":
|
|
265
|
+
return _handle_guard_status(workspace_id)
|
|
266
|
+
return f"Unknown tool: {name}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ── JSON-RPC helpers ───────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
def _send(obj: dict) -> None:
|
|
272
|
+
print(json.dumps(obj), flush=True)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _ok(msg_id, result: dict) -> None:
|
|
276
|
+
_send({"jsonrpc": "2.0", "id": msg_id, "result": result})
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _err(msg_id, code: int, message: str) -> None:
|
|
280
|
+
_send({"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}})
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ── Main loop ──────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
def main() -> None:
|
|
286
|
+
cfg = _load_conduct_config()
|
|
287
|
+
server = cfg.get("server", "").rstrip("/")
|
|
288
|
+
workspace_id = cfg.get("workspace", "")
|
|
289
|
+
api_key = cfg.get("api_key")
|
|
290
|
+
token = cfg.get("token")
|
|
291
|
+
|
|
292
|
+
for raw in sys.stdin:
|
|
293
|
+
raw = raw.strip()
|
|
294
|
+
if not raw:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
msg = json.loads(raw)
|
|
299
|
+
except json.JSONDecodeError:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
msg_id = msg.get("id") # None for notifications
|
|
303
|
+
method = msg.get("method", "")
|
|
304
|
+
params = msg.get("params") or {}
|
|
305
|
+
|
|
306
|
+
if method == "initialize":
|
|
307
|
+
_ok(msg_id, {
|
|
308
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
309
|
+
"capabilities": {"tools": {}},
|
|
310
|
+
"serverInfo": {"name": "conduct", "version": "1.0.0"},
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
elif method == "notifications/initialized":
|
|
314
|
+
pass # notification — no response
|
|
315
|
+
|
|
316
|
+
elif method == "tools/list":
|
|
317
|
+
_ok(msg_id, {"tools": _TOOLS})
|
|
318
|
+
|
|
319
|
+
elif method == "tools/call":
|
|
320
|
+
tool_name = params.get("name", "")
|
|
321
|
+
arguments = params.get("arguments") or {}
|
|
322
|
+
text = _dispatch_tool(tool_name, arguments, server, workspace_id, token, api_key)
|
|
323
|
+
_ok(msg_id, {"content": [{"type": "text", "text": text}]})
|
|
324
|
+
|
|
325
|
+
elif method == "ping":
|
|
326
|
+
_ok(msg_id, {})
|
|
327
|
+
|
|
328
|
+
else:
|
|
329
|
+
if msg_id is not None:
|
|
330
|
+
_err(msg_id, -32601, f"Method not found: {method}")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
main()
|
|
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
|