mtrx-cli 0.1.10 → 0.1.13
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.
- package/package.json +6 -2
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/cursor_ca.py +275 -0
- package/src/matrx/cli/cursor_config.py +262 -74
- package/src/matrx/cli/cursor_daemon.py +64 -0
- package/src/matrx/cli/cursor_proxy.py +459 -261
- package/src/matrx/cli/cursor_service.py +343 -0
- package/src/matrx/cli/launcher.py +47 -27
- package/src/matrx/cli/main.py +150 -69
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-platform background service management for the MTRX Cursor proxy.
|
|
3
|
+
|
|
4
|
+
Handles installing, starting, stopping, and checking the proxy daemon on
|
|
5
|
+
macOS (Launch Agent), Linux (systemd user unit), and Windows (detached
|
|
6
|
+
subprocess with PID tracking).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import platform
|
|
15
|
+
import shutil
|
|
16
|
+
import signal
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import textwrap
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from matrx.cli.cursor_proxy import DEFAULT_PORT, HEALTH_PATH, PROXY_HOST
|
|
26
|
+
from matrx.cli.state import config_dir
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
_LAUNCH_AGENT_LABEL = "so.mtrx.cursor-proxy"
|
|
31
|
+
_SYSTEMD_UNIT_NAME = "mtrx-cursor-proxy.service"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pid_file_path() -> Path:
|
|
35
|
+
return config_dir() / "cursor-proxy.pid"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def proxy_config_path() -> Path:
|
|
39
|
+
return config_dir() / "cursor-proxy.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _find_python() -> str:
|
|
43
|
+
"""Return the path to the current Python interpreter."""
|
|
44
|
+
return sys.executable
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _launch_agent_plist_path() -> Path:
|
|
48
|
+
return Path.home() / "Library" / "LaunchAgents" / f"{_LAUNCH_AGENT_LABEL}.plist"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _systemd_unit_path() -> Path:
|
|
52
|
+
return Path.home() / ".config" / "systemd" / "user" / _SYSTEMD_UNIT_NAME
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Proxy config persistence (key + base_url needed to start the daemon)
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def save_proxy_config(
|
|
60
|
+
*,
|
|
61
|
+
matrx_key: str,
|
|
62
|
+
matrx_base_url: str,
|
|
63
|
+
host: str = PROXY_HOST,
|
|
64
|
+
port: int = DEFAULT_PORT,
|
|
65
|
+
) -> Path:
|
|
66
|
+
conf = proxy_config_path()
|
|
67
|
+
conf.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
conf.write_text(
|
|
69
|
+
json.dumps({
|
|
70
|
+
"matrx_key": matrx_key,
|
|
71
|
+
"matrx_base_url": matrx_base_url,
|
|
72
|
+
"host": host,
|
|
73
|
+
"port": port,
|
|
74
|
+
}),
|
|
75
|
+
encoding="utf-8",
|
|
76
|
+
)
|
|
77
|
+
os.chmod(conf, 0o600)
|
|
78
|
+
return conf
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_proxy_config() -> dict | None:
|
|
82
|
+
conf = proxy_config_path()
|
|
83
|
+
if not conf.exists():
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return json.loads(conf.read_text(encoding="utf-8"))
|
|
87
|
+
except Exception:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Service lifecycle
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def install_service(
|
|
96
|
+
*,
|
|
97
|
+
matrx_key: str,
|
|
98
|
+
matrx_base_url: str,
|
|
99
|
+
host: str = PROXY_HOST,
|
|
100
|
+
port: int = DEFAULT_PORT,
|
|
101
|
+
) -> bool:
|
|
102
|
+
"""Install and start the proxy as a background service."""
|
|
103
|
+
save_proxy_config(
|
|
104
|
+
matrx_key=matrx_key,
|
|
105
|
+
matrx_base_url=matrx_base_url,
|
|
106
|
+
host=host,
|
|
107
|
+
port=port,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
system = platform.system()
|
|
111
|
+
if system == "Darwin":
|
|
112
|
+
return _install_macos(host, port)
|
|
113
|
+
if system == "Linux":
|
|
114
|
+
return _install_linux(host, port)
|
|
115
|
+
return _install_generic(host, port)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def uninstall_service() -> bool:
|
|
119
|
+
"""Stop and remove the background service."""
|
|
120
|
+
system = platform.system()
|
|
121
|
+
ok = True
|
|
122
|
+
if system == "Darwin":
|
|
123
|
+
ok = _uninstall_macos()
|
|
124
|
+
elif system == "Linux":
|
|
125
|
+
ok = _uninstall_linux()
|
|
126
|
+
else:
|
|
127
|
+
ok = _stop_by_pid()
|
|
128
|
+
|
|
129
|
+
proxy_config_path().unlink(missing_ok=True)
|
|
130
|
+
pid_file_path().unlink(missing_ok=True)
|
|
131
|
+
return ok
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_proxy_running(host: str = PROXY_HOST, port: int = DEFAULT_PORT) -> bool:
|
|
135
|
+
"""Check if the proxy is running and healthy."""
|
|
136
|
+
try:
|
|
137
|
+
resp = httpx.get(
|
|
138
|
+
f"http://{host}:{port}{HEALTH_PATH}",
|
|
139
|
+
timeout=3,
|
|
140
|
+
)
|
|
141
|
+
return resp.status_code == 200
|
|
142
|
+
except Exception:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_proxy_status(host: str = PROXY_HOST, port: int = DEFAULT_PORT) -> dict | None:
|
|
147
|
+
"""Get proxy status info, or None if not reachable."""
|
|
148
|
+
try:
|
|
149
|
+
resp = httpx.get(
|
|
150
|
+
f"http://{host}:{port}{HEALTH_PATH}",
|
|
151
|
+
timeout=3,
|
|
152
|
+
)
|
|
153
|
+
if resp.status_code == 200:
|
|
154
|
+
return resp.json()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# macOS: Launch Agent
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _install_macos(host: str, port: int) -> bool:
|
|
165
|
+
_unload_launchd()
|
|
166
|
+
|
|
167
|
+
python = _find_python()
|
|
168
|
+
conf = str(proxy_config_path())
|
|
169
|
+
pid = str(pid_file_path())
|
|
170
|
+
log_dir = config_dir() / "logs"
|
|
171
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
|
|
173
|
+
plist_content = textwrap.dedent(f"""\
|
|
174
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
175
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
176
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
177
|
+
<plist version="1.0">
|
|
178
|
+
<dict>
|
|
179
|
+
<key>Label</key>
|
|
180
|
+
<string>{_LAUNCH_AGENT_LABEL}</string>
|
|
181
|
+
<key>ProgramArguments</key>
|
|
182
|
+
<array>
|
|
183
|
+
<string>{python}</string>
|
|
184
|
+
<string>-m</string>
|
|
185
|
+
<string>matrx.cli.cursor_daemon</string>
|
|
186
|
+
<string>--config</string>
|
|
187
|
+
<string>{conf}</string>
|
|
188
|
+
<string>--pid-file</string>
|
|
189
|
+
<string>{pid}</string>
|
|
190
|
+
</array>
|
|
191
|
+
<key>RunAtLoad</key>
|
|
192
|
+
<true/>
|
|
193
|
+
<key>KeepAlive</key>
|
|
194
|
+
<true/>
|
|
195
|
+
<key>StandardOutPath</key>
|
|
196
|
+
<string>{log_dir / "cursor-proxy.out.log"}</string>
|
|
197
|
+
<key>StandardErrorPath</key>
|
|
198
|
+
<string>{log_dir / "cursor-proxy.err.log"}</string>
|
|
199
|
+
</dict>
|
|
200
|
+
</plist>
|
|
201
|
+
""")
|
|
202
|
+
|
|
203
|
+
plist_path = _launch_agent_plist_path()
|
|
204
|
+
plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
plist_path.write_text(plist_content, encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
result = subprocess.run(
|
|
208
|
+
["launchctl", "load", "-w", str(plist_path)],
|
|
209
|
+
capture_output=True,
|
|
210
|
+
timeout=10,
|
|
211
|
+
)
|
|
212
|
+
if result.returncode != 0:
|
|
213
|
+
logger.warning("launchctl load failed: %s", result.stderr.decode(errors="replace"))
|
|
214
|
+
return _install_generic(host, port)
|
|
215
|
+
|
|
216
|
+
time.sleep(1)
|
|
217
|
+
return is_proxy_running(host, port)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _unload_launchd() -> None:
|
|
221
|
+
plist_path = _launch_agent_plist_path()
|
|
222
|
+
if plist_path.exists():
|
|
223
|
+
subprocess.run(
|
|
224
|
+
["launchctl", "unload", str(plist_path)],
|
|
225
|
+
capture_output=True,
|
|
226
|
+
timeout=10,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _uninstall_macos() -> bool:
|
|
231
|
+
_unload_launchd()
|
|
232
|
+
plist_path = _launch_agent_plist_path()
|
|
233
|
+
plist_path.unlink(missing_ok=True)
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Linux: systemd user unit
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def _install_linux(host: str, port: int) -> bool:
|
|
242
|
+
python = _find_python()
|
|
243
|
+
conf = str(proxy_config_path())
|
|
244
|
+
pid = str(pid_file_path())
|
|
245
|
+
|
|
246
|
+
unit_content = textwrap.dedent(f"""\
|
|
247
|
+
[Unit]
|
|
248
|
+
Description=MTRX Cursor MITM Proxy
|
|
249
|
+
After=network.target
|
|
250
|
+
|
|
251
|
+
[Service]
|
|
252
|
+
Type=simple
|
|
253
|
+
ExecStart={python} -m matrx.cli.cursor_daemon --config {conf} --pid-file {pid}
|
|
254
|
+
Restart=on-failure
|
|
255
|
+
RestartSec=5
|
|
256
|
+
|
|
257
|
+
[Install]
|
|
258
|
+
WantedBy=default.target
|
|
259
|
+
""")
|
|
260
|
+
|
|
261
|
+
unit_path = _systemd_unit_path()
|
|
262
|
+
unit_path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
unit_path.write_text(unit_content, encoding="utf-8")
|
|
264
|
+
|
|
265
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True, timeout=10)
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
["systemctl", "--user", "enable", "--now", _SYSTEMD_UNIT_NAME],
|
|
268
|
+
capture_output=True,
|
|
269
|
+
timeout=10,
|
|
270
|
+
)
|
|
271
|
+
if result.returncode != 0:
|
|
272
|
+
logger.warning("systemctl enable failed: %s", result.stderr.decode(errors="replace"))
|
|
273
|
+
return _install_generic(host, port)
|
|
274
|
+
|
|
275
|
+
time.sleep(1)
|
|
276
|
+
return is_proxy_running(host, port)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _uninstall_linux() -> bool:
|
|
280
|
+
subprocess.run(
|
|
281
|
+
["systemctl", "--user", "disable", "--now", _SYSTEMD_UNIT_NAME],
|
|
282
|
+
capture_output=True,
|
|
283
|
+
timeout=10,
|
|
284
|
+
)
|
|
285
|
+
unit_path = _systemd_unit_path()
|
|
286
|
+
unit_path.unlink(missing_ok=True)
|
|
287
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True, timeout=10)
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Generic: detached subprocess with PID file
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def _install_generic(host: str, port: int) -> bool:
|
|
296
|
+
"""Start the proxy as a detached subprocess (works everywhere)."""
|
|
297
|
+
_stop_by_pid()
|
|
298
|
+
|
|
299
|
+
python = _find_python()
|
|
300
|
+
conf = str(proxy_config_path())
|
|
301
|
+
pid = str(pid_file_path())
|
|
302
|
+
|
|
303
|
+
kwargs: dict = {
|
|
304
|
+
"start_new_session": True,
|
|
305
|
+
}
|
|
306
|
+
if sys.platform == "win32":
|
|
307
|
+
kwargs = {
|
|
308
|
+
"creationflags": subprocess.CREATE_NO_WINDOW | subprocess.DETACHED_PROCESS,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
log_dir = config_dir() / "logs"
|
|
312
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
stdout_log = open(log_dir / "cursor-proxy.out.log", "a")
|
|
314
|
+
stderr_log = open(log_dir / "cursor-proxy.err.log", "a")
|
|
315
|
+
|
|
316
|
+
subprocess.Popen(
|
|
317
|
+
[python, "-m", "matrx.cli.cursor_daemon", "--config", conf, "--pid-file", pid],
|
|
318
|
+
stdout=stdout_log,
|
|
319
|
+
stderr=stderr_log,
|
|
320
|
+
stdin=subprocess.DEVNULL,
|
|
321
|
+
**kwargs,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Wait a moment for startup
|
|
325
|
+
for _ in range(10):
|
|
326
|
+
time.sleep(0.5)
|
|
327
|
+
if is_proxy_running(host, port):
|
|
328
|
+
return True
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _stop_by_pid() -> bool:
|
|
333
|
+
pf = pid_file_path()
|
|
334
|
+
if not pf.exists():
|
|
335
|
+
return True
|
|
336
|
+
try:
|
|
337
|
+
pid = int(pf.read_text(encoding="utf-8").strip())
|
|
338
|
+
os.kill(pid, signal.SIGTERM)
|
|
339
|
+
time.sleep(0.5)
|
|
340
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
341
|
+
pass
|
|
342
|
+
pf.unlink(missing_ok=True)
|
|
343
|
+
return True
|
|
@@ -80,6 +80,15 @@ class LaunchPlan:
|
|
|
80
80
|
orchestration: dict | None = None
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def _runtime_agent_basename(tool: str) -> tuple[str, str, list[str], str]:
|
|
84
|
+
if tool == "codex":
|
|
85
|
+
return "codex-cli", "Codex CLI", ["cli", "codex"], "codex"
|
|
86
|
+
if tool == "claude":
|
|
87
|
+
return "claude-cli", "Claude CLI", ["claude", "cli"], "claude_code"
|
|
88
|
+
normalized = f"{tool}-cli"
|
|
89
|
+
return normalized, f"{tool.capitalize()} CLI", ["cli", tool], tool
|
|
90
|
+
|
|
91
|
+
|
|
83
92
|
def configured_route(state: dict, tool: str) -> str | None:
|
|
84
93
|
route = state.get("defaults", {}).get(tool)
|
|
85
94
|
if route in VALID_ROUTES:
|
|
@@ -132,6 +141,12 @@ def build_launch_plan(
|
|
|
132
141
|
env = dict(source_env)
|
|
133
142
|
auth_source = ""
|
|
134
143
|
launch_args = list(passthrough_args or [])
|
|
144
|
+
orchestration = _build_orchestration_metadata(
|
|
145
|
+
state=state,
|
|
146
|
+
tool=tool,
|
|
147
|
+
route=route,
|
|
148
|
+
env=source_env,
|
|
149
|
+
)
|
|
135
150
|
|
|
136
151
|
if tool == "codex":
|
|
137
152
|
env, auth_source, launch_args = _build_codex_env(
|
|
@@ -139,9 +154,15 @@ def build_launch_plan(
|
|
|
139
154
|
route,
|
|
140
155
|
env,
|
|
141
156
|
launch_args,
|
|
157
|
+
orchestration=orchestration,
|
|
142
158
|
)
|
|
143
159
|
elif tool == "claude":
|
|
144
|
-
env, auth_source = _build_claude_env(
|
|
160
|
+
env, auth_source = _build_claude_env(
|
|
161
|
+
state,
|
|
162
|
+
route,
|
|
163
|
+
env,
|
|
164
|
+
orchestration=orchestration,
|
|
165
|
+
)
|
|
145
166
|
else:
|
|
146
167
|
raise ValueError(f"Unsupported tool: {tool}")
|
|
147
168
|
|
|
@@ -152,12 +173,7 @@ def build_launch_plan(
|
|
|
152
173
|
args=launch_args,
|
|
153
174
|
env=env,
|
|
154
175
|
auth_source=auth_source,
|
|
155
|
-
orchestration=
|
|
156
|
-
state=state,
|
|
157
|
-
tool=tool,
|
|
158
|
-
route=route,
|
|
159
|
-
env=source_env,
|
|
160
|
-
),
|
|
176
|
+
orchestration=orchestration,
|
|
161
177
|
)
|
|
162
178
|
|
|
163
179
|
|
|
@@ -438,6 +454,8 @@ def _build_codex_env(
|
|
|
438
454
|
route: str,
|
|
439
455
|
env: dict[str, str],
|
|
440
456
|
passthrough_args: list[str],
|
|
457
|
+
*,
|
|
458
|
+
orchestration: dict | None = None,
|
|
441
459
|
) -> tuple[dict[str, str], str, list[str]]:
|
|
442
460
|
matrx = state["auth"]["matrx"]
|
|
443
461
|
openai = state["auth"]["openai"]
|
|
@@ -457,10 +475,14 @@ def _build_codex_env(
|
|
|
457
475
|
env_b64 = base64.b64encode(json.dumps(env_snap).encode()).decode() if env_snap else ""
|
|
458
476
|
session_id = str(uuid.uuid4())
|
|
459
477
|
group_id, project_id = _resolve_matrx_context_overrides(state, env)
|
|
478
|
+
runtime_agent_id = (
|
|
479
|
+
(orchestration or {}).get("agent_id")
|
|
480
|
+
or _runtime_agent_basename("codex")[0]
|
|
481
|
+
)
|
|
460
482
|
header_parts = [
|
|
461
483
|
f'"Authorization" = "Bearer {access_token}"',
|
|
462
484
|
f'"X-Matrx-Key" = "{mx_key}"',
|
|
463
|
-
'"X-Matrx-Agent-Id" = "
|
|
485
|
+
f'"X-Matrx-Agent-Id" = "{runtime_agent_id}"',
|
|
464
486
|
'"X-Matrx-Provider" = "codex"',
|
|
465
487
|
f'"X-Matrx-Session-Id" = "{session_id}"',
|
|
466
488
|
]
|
|
@@ -501,6 +523,8 @@ def _build_claude_env(
|
|
|
501
523
|
state: dict,
|
|
502
524
|
route: str,
|
|
503
525
|
env: dict[str, str],
|
|
526
|
+
*,
|
|
527
|
+
orchestration: dict | None = None,
|
|
504
528
|
) -> tuple[dict[str, str], str]:
|
|
505
529
|
matrx = state["auth"]["matrx"]
|
|
506
530
|
anthropic = state["auth"]["anthropic"]
|
|
@@ -520,13 +544,17 @@ def _build_claude_env(
|
|
|
520
544
|
env.pop("ANTHROPIC_API_KEY", None)
|
|
521
545
|
group_id, project_id = _resolve_matrx_context_overrides(state, env)
|
|
522
546
|
session_id = str(uuid.uuid4())
|
|
547
|
+
runtime_agent_id = (
|
|
548
|
+
(orchestration or {}).get("agent_id")
|
|
549
|
+
or _runtime_agent_basename("claude")[0]
|
|
550
|
+
)
|
|
523
551
|
# Evolutionary scaffolding: env snapshot for AI context injection
|
|
524
552
|
env_snap = _capture_env_snapshot()
|
|
525
553
|
env_b64 = base64.b64encode(json.dumps(env_snap).encode()).decode() if env_snap else ""
|
|
526
554
|
custom_headers = "\n".join(
|
|
527
555
|
[
|
|
528
556
|
f"x-matrx-key: {mx_key}",
|
|
529
|
-
"x-matrx-agent-id:
|
|
557
|
+
f"x-matrx-agent-id: {runtime_agent_id}",
|
|
530
558
|
"x-matrx-provider: claude_code",
|
|
531
559
|
f"x-matrx-session-id: {session_id}",
|
|
532
560
|
]
|
|
@@ -592,27 +620,18 @@ def _build_orchestration_metadata(
|
|
|
592
620
|
if not mx_key:
|
|
593
621
|
return None
|
|
594
622
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
provider = "codex"
|
|
598
|
-
capabilities = ["cli", "codex"]
|
|
599
|
-
name = "Codex CLI"
|
|
600
|
-
elif tool == "claude":
|
|
601
|
-
agent_id = "claude-cli"
|
|
602
|
-
provider = "claude_code"
|
|
603
|
-
capabilities = ["claude", "cli"]
|
|
604
|
-
name = "Claude CLI"
|
|
605
|
-
else:
|
|
606
|
-
agent_id = f"{tool}-cli"
|
|
607
|
-
provider = tool
|
|
608
|
-
capabilities = ["cli", tool]
|
|
609
|
-
name = f"{tool.capitalize()} CLI"
|
|
623
|
+
agent_kind, name, capabilities, provider = _runtime_agent_basename(tool)
|
|
624
|
+
agent_id = f"{agent_kind}-{uuid.uuid4().hex[:8]}"
|
|
610
625
|
|
|
611
626
|
_, project_id = _resolve_matrx_context_overrides(state, env)
|
|
627
|
+
base_url = ensure_root_url(
|
|
628
|
+
env.get("MATRX_BASE_URL") or state.get("auth", {}).get("matrx", {}).get("base_url")
|
|
629
|
+
)
|
|
612
630
|
return {
|
|
613
|
-
"base_url":
|
|
631
|
+
"base_url": base_url,
|
|
614
632
|
"matrx_key": mx_key,
|
|
615
633
|
"agent_id": agent_id,
|
|
634
|
+
"agent_kind": agent_kind,
|
|
616
635
|
"provider": provider,
|
|
617
636
|
"name": name,
|
|
618
637
|
"capabilities": capabilities,
|
|
@@ -633,6 +652,7 @@ def _best_effort_register_cli_agent(orchestration: dict) -> None:
|
|
|
633
652
|
payload = {
|
|
634
653
|
"agent_id": agent_id,
|
|
635
654
|
"name": orchestration.get("name") or agent_id,
|
|
655
|
+
"agent_kind": orchestration.get("agent_kind"),
|
|
636
656
|
"capabilities": list(orchestration.get("capabilities") or []),
|
|
637
657
|
"max_concurrent_tasks": 1,
|
|
638
658
|
}
|
|
@@ -736,8 +756,8 @@ def _validate_claude_launch_plan(plan: LaunchPlan, state: dict) -> None:
|
|
|
736
756
|
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Session-Id")
|
|
737
757
|
if "x-matrx-provider: claude_code" not in lowered_headers:
|
|
738
758
|
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Provider=claude_code")
|
|
739
|
-
if "x-matrx-agent-id:
|
|
740
|
-
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Agent-Id
|
|
759
|
+
if "x-matrx-agent-id:" not in lowered_headers:
|
|
760
|
+
raise ValueError("Claude Matrx route is missing ANTHROPIC_CUSTOM_HEADERS with X-Matrx-Agent-Id")
|
|
741
761
|
|
|
742
762
|
if plan.env.get("ANTHROPIC_AUTH_TOKEN"):
|
|
743
763
|
raise ValueError("Claude Matrx route should not set ANTHROPIC_AUTH_TOKEN")
|