conduct-cli 0.4.26__tar.gz → 0.4.28__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.26 → conduct_cli-0.4.28}/PKG-INFO +1 -1
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/pyproject.toml +4 -3
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli/guard.py +4 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli/main.py +92 -0
- conduct_cli-0.4.28/src/conduct_cli/mcp_server.py +334 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli.egg-info/SOURCES.txt +1 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli.egg-info/entry_points.txt +1 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/README.md +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/setup.cfg +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/setup.py +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.26 → conduct_cli-0.4.28}/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.28"
|
|
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]
|
|
@@ -374,6 +374,10 @@ def main():
|
|
|
374
374
|
msg = f"[ConductGuard] {reason or 'Budget hard cap reached. Contact your manager.'}"
|
|
375
375
|
print(msg)
|
|
376
376
|
print(msg, file=sys.stderr)
|
|
377
|
+
tool_name = (data.get("tool_name") or "").lower()
|
|
378
|
+
tool_input = data.get("tool_input") or {}
|
|
379
|
+
session_id = data.get("session_id")
|
|
380
|
+
_post_event(tool_name, tool_input, "blocked", "budget-hard-cap", reason or "Monthly budget hard cap reached.", session_id=session_id)
|
|
377
381
|
sys.exit(2)
|
|
378
382
|
|
|
379
383
|
session_id = data.get("session_id")
|
|
@@ -192,6 +192,81 @@ 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 # already registered
|
|
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 cmd_mcp_install(args):
|
|
229
|
+
"""Register conduct-mcp in Claude Code and Codex CLI."""
|
|
230
|
+
import shutil
|
|
231
|
+
import subprocess
|
|
232
|
+
|
|
233
|
+
registered = []
|
|
234
|
+
|
|
235
|
+
# --- Claude Code ---
|
|
236
|
+
if shutil.which("claude"):
|
|
237
|
+
try:
|
|
238
|
+
result = subprocess.run(
|
|
239
|
+
["claude", "mcp", "add", "conduct", "conduct-mcp"],
|
|
240
|
+
capture_output=True, text=True, timeout=15,
|
|
241
|
+
)
|
|
242
|
+
if result.returncode == 0:
|
|
243
|
+
registered.append("Claude Code")
|
|
244
|
+
else:
|
|
245
|
+
# claude mcp add is idempotent; also try writing settings.json directly as fallback
|
|
246
|
+
_write_claude_mcp_settings()
|
|
247
|
+
registered.append("Claude Code (settings.json)")
|
|
248
|
+
except Exception:
|
|
249
|
+
_write_claude_mcp_settings()
|
|
250
|
+
registered.append("Claude Code (settings.json)")
|
|
251
|
+
else:
|
|
252
|
+
# claude CLI not found but .claude/ might exist (IDE extension)
|
|
253
|
+
written = _write_claude_mcp_settings()
|
|
254
|
+
if written:
|
|
255
|
+
registered.append("Claude Code (settings.json)")
|
|
256
|
+
|
|
257
|
+
# --- Codex CLI ---
|
|
258
|
+
written = _write_codex_mcp_config()
|
|
259
|
+
if written:
|
|
260
|
+
registered.append("Codex")
|
|
261
|
+
|
|
262
|
+
if registered:
|
|
263
|
+
print(f"{GREEN}✓ conduct-mcp registered in: {', '.join(registered)}{RESET}")
|
|
264
|
+
print(f"{GRAY} Restart Claude Code / Codex to pick up the new MCP server.{RESET}")
|
|
265
|
+
else:
|
|
266
|
+
print(f"{YELLOW}⚠ No supported AI tools detected. Install Claude Code or Codex first.{RESET}")
|
|
267
|
+
print(f"{GRAY} Then re-run: conduct mcp install{RESET}")
|
|
268
|
+
|
|
269
|
+
|
|
195
270
|
def cmd_login(args):
|
|
196
271
|
server = args.server
|
|
197
272
|
api_key = args.api_key
|
|
@@ -254,6 +329,13 @@ def cmd_login(args):
|
|
|
254
329
|
except Exception:
|
|
255
330
|
pass # Never block login on Guard errors
|
|
256
331
|
|
|
332
|
+
# Auto-register MCP servers in Claude Code / Codex
|
|
333
|
+
try:
|
|
334
|
+
import types
|
|
335
|
+
cmd_mcp_install(types.SimpleNamespace())
|
|
336
|
+
except Exception:
|
|
337
|
+
pass # Never block login on MCP registration errors
|
|
338
|
+
|
|
257
339
|
|
|
258
340
|
def cmd_agents(args):
|
|
259
341
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
@@ -1179,6 +1261,11 @@ def main():
|
|
|
1179
1261
|
# conduct guard
|
|
1180
1262
|
guard_p, _guard_sub = _guard.register_guard_parser(sub)
|
|
1181
1263
|
|
|
1264
|
+
# conduct mcp
|
|
1265
|
+
mcp_p = sub.add_parser("mcp", help="Manage the Conduct MCP server")
|
|
1266
|
+
mcp_sub = mcp_p.add_subparsers(dest="mcp_command")
|
|
1267
|
+
mcp_sub.add_parser("install", help="Register conduct-mcp in Claude Code and Codex")
|
|
1268
|
+
|
|
1182
1269
|
args = parser.parse_args()
|
|
1183
1270
|
|
|
1184
1271
|
if args.command == "login":
|
|
@@ -1223,6 +1310,11 @@ def main():
|
|
|
1223
1310
|
cmd_run(args)
|
|
1224
1311
|
elif args.command == "guard":
|
|
1225
1312
|
_guard.dispatch_guard(args, guard_p)
|
|
1313
|
+
elif args.command == "mcp":
|
|
1314
|
+
if getattr(args, "mcp_command", None) == "install":
|
|
1315
|
+
cmd_mcp_install(args)
|
|
1316
|
+
else:
|
|
1317
|
+
mcp_p.print_help()
|
|
1226
1318
|
else:
|
|
1227
1319
|
parser.print_help()
|
|
1228
1320
|
|
|
@@ -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
|