mtrx-cli 0.1.11 → 0.1.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mtrx-cli",
3
- "version": "0.1.11",
4
- "description": "MATRX CLI for routing Codex and Claude through Matrx",
3
+ "version": "0.1.14",
4
+ "description": "MATRX CLI for routing Codex, Claude, and Cursor through Matrx",
5
5
  "homepage": "https://mtrx.so",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,6 +15,7 @@
15
15
  "cli",
16
16
  "codex",
17
17
  "claude",
18
+ "cursor",
18
19
  "ai"
19
20
  ],
20
21
  "bin": {
@@ -24,8 +25,12 @@
24
25
  "bin/mtrx.js",
25
26
  "src/matrx/__init__.py",
26
27
  "src/matrx/cli/__init__.py",
28
+ "src/matrx/cli/cursor_ca.py",
27
29
  "src/matrx/cli/cursor_config.py",
30
+ "src/matrx/cli/cursor_daemon.py",
31
+ "src/matrx/cli/cursor_launcher.py",
28
32
  "src/matrx/cli/cursor_proxy.py",
33
+ "src/matrx/cli/cursor_service.py",
29
34
  "src/matrx/cli/launcher.py",
30
35
  "src/matrx/cli/main.py",
31
36
  "src/matrx/cli/state.py"
@@ -1 +1 @@
1
- __version__ = "0.1.11"
1
+ __version__ = "0.1.14"
@@ -0,0 +1,275 @@
1
+ """
2
+ CA certificate management for the MTRX Cursor MITM proxy.
3
+
4
+ Generates a root CA key + certificate, dynamically signs per-host leaf
5
+ certificates, and provides platform-specific helpers for trusting the CA
6
+ in the system/Cursor environment.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import datetime
12
+ import logging
13
+ import os
14
+ import platform
15
+ import subprocess
16
+ import threading
17
+ from pathlib import Path
18
+
19
+ from cryptography import x509
20
+ from cryptography.hazmat.primitives import hashes, serialization
21
+ from cryptography.hazmat.primitives.asymmetric import rsa
22
+ from cryptography.x509.oid import NameOID
23
+
24
+ from matrx.cli.state import config_dir
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _CA_DIR_NAME = "ca"
29
+ _CA_KEY_FILE = "mtrx-ca.key"
30
+ _CA_CERT_FILE = "mtrx-ca.pem"
31
+ _CERT_VALIDITY_YEARS = 5
32
+ _LEAF_VALIDITY_DAYS = 365
33
+
34
+
35
+ def ca_dir() -> Path:
36
+ return config_dir() / _CA_DIR_NAME
37
+
38
+
39
+ def ca_key_path() -> Path:
40
+ return ca_dir() / _CA_KEY_FILE
41
+
42
+
43
+ def ca_cert_path() -> Path:
44
+ return ca_dir() / _CA_CERT_FILE
45
+
46
+
47
+ def ca_exists() -> bool:
48
+ return ca_key_path().exists() and ca_cert_path().exists()
49
+
50
+
51
+ def generate_ca(*, force: bool = False) -> tuple[Path, Path]:
52
+ """Generate a root CA key and self-signed certificate.
53
+
54
+ Returns (key_path, cert_path). Skips generation if files already exist
55
+ unless *force* is True.
56
+ """
57
+ key_path = ca_key_path()
58
+ cert_path = ca_cert_path()
59
+ if not force and key_path.exists() and cert_path.exists():
60
+ return key_path, cert_path
61
+
62
+ ca_dir().mkdir(parents=True, exist_ok=True)
63
+
64
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
65
+ subject = issuer = x509.Name([
66
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "MTRX"),
67
+ x509.NameAttribute(NameOID.COMMON_NAME, "MTRX Cursor Proxy CA"),
68
+ ])
69
+ now = datetime.datetime.now(datetime.timezone.utc)
70
+ cert = (
71
+ x509.CertificateBuilder()
72
+ .subject_name(subject)
73
+ .issuer_name(issuer)
74
+ .public_key(key.public_key())
75
+ .serial_number(x509.random_serial_number())
76
+ .not_valid_before(now)
77
+ .not_valid_after(now + datetime.timedelta(days=_CERT_VALIDITY_YEARS * 365))
78
+ .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
79
+ .add_extension(
80
+ x509.KeyUsage(
81
+ digital_signature=True,
82
+ key_cert_sign=True,
83
+ crl_sign=True,
84
+ content_commitment=False,
85
+ key_encipherment=False,
86
+ data_encipherment=False,
87
+ key_agreement=False,
88
+ encipher_only=False,
89
+ decipher_only=False,
90
+ ),
91
+ critical=True,
92
+ )
93
+ .sign(key, hashes.SHA256())
94
+ )
95
+
96
+ key_path.write_bytes(
97
+ key.private_bytes(
98
+ encoding=serialization.Encoding.PEM,
99
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
100
+ encryption_algorithm=serialization.NoEncryption(),
101
+ )
102
+ )
103
+ os.chmod(key_path, 0o600)
104
+ cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
105
+
106
+ return key_path, cert_path
107
+
108
+
109
+ def load_ca() -> tuple[rsa.RSAPrivateKey, x509.Certificate]:
110
+ """Load the CA key and certificate from disk."""
111
+ key_pem = ca_key_path().read_bytes()
112
+ cert_pem = ca_cert_path().read_bytes()
113
+ key = serialization.load_pem_private_key(key_pem, password=None)
114
+ cert = x509.load_pem_x509_certificate(cert_pem)
115
+ return key, cert # type: ignore[return-value]
116
+
117
+
118
+ class CertCache:
119
+ """Thread-safe cache for dynamically-signed leaf certificates.
120
+
121
+ Caches both the raw PEM bytes and ready-to-use ``ssl.SSLContext``
122
+ objects so that repeated connections to the same host avoid temp-file
123
+ I/O and key-generation overhead.
124
+ """
125
+
126
+ def __init__(self, ca_key: rsa.RSAPrivateKey, ca_cert: x509.Certificate):
127
+ self._ca_key = ca_key
128
+ self._ca_cert = ca_cert
129
+ self._lock = threading.Lock()
130
+ self._cache: dict[str, tuple[bytes, bytes]] = {}
131
+ self._ctx_cache: dict[str, "ssl.SSLContext"] = {}
132
+ self._certs_dir = ca_dir() / "certs"
133
+ self._certs_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ def get_cert_pair(self, hostname: str) -> tuple[bytes, bytes]:
136
+ """Return (cert_pem, key_pem) for *hostname*, creating if needed."""
137
+ with self._lock:
138
+ if hostname in self._cache:
139
+ return self._cache[hostname]
140
+
141
+ cert_pem, key_pem = self._sign_leaf(hostname)
142
+
143
+ with self._lock:
144
+ self._cache[hostname] = (cert_pem, key_pem)
145
+ return cert_pem, key_pem
146
+
147
+ def get_ssl_context(self, hostname: str) -> "ssl.SSLContext":
148
+ """Return a server-side ``ssl.SSLContext`` for *hostname*."""
149
+ import ssl as _ssl
150
+
151
+ with self._lock:
152
+ if hostname in self._ctx_cache:
153
+ return self._ctx_cache[hostname]
154
+
155
+ cert_pem, key_pem = self.get_cert_pair(hostname)
156
+
157
+ cert_file = self._certs_dir / f"{hostname}.crt"
158
+ key_file = self._certs_dir / f"{hostname}.key"
159
+ cert_file.write_bytes(cert_pem)
160
+ key_file.write_bytes(key_pem)
161
+ os.chmod(key_file, 0o600)
162
+
163
+ ctx = _ssl.SSLContext(_ssl.PROTOCOL_TLS_SERVER)
164
+ ctx.load_cert_chain(str(cert_file), str(key_file))
165
+ ctx.set_alpn_protocols(["http/1.1"])
166
+
167
+ with self._lock:
168
+ self._ctx_cache[hostname] = ctx
169
+ return ctx
170
+
171
+ def _sign_leaf(self, hostname: str) -> tuple[bytes, bytes]:
172
+ leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
173
+ now = datetime.datetime.now(datetime.timezone.utc)
174
+ subject = x509.Name([
175
+ x509.NameAttribute(NameOID.COMMON_NAME, hostname),
176
+ ])
177
+
178
+ builder = (
179
+ x509.CertificateBuilder()
180
+ .subject_name(subject)
181
+ .issuer_name(self._ca_cert.subject)
182
+ .public_key(leaf_key.public_key())
183
+ .serial_number(x509.random_serial_number())
184
+ .not_valid_before(now - datetime.timedelta(days=1))
185
+ .not_valid_after(now + datetime.timedelta(days=_LEAF_VALIDITY_DAYS))
186
+ .add_extension(
187
+ x509.SubjectAlternativeName([x509.DNSName(hostname)]),
188
+ critical=False,
189
+ )
190
+ .add_extension(
191
+ x509.BasicConstraints(ca=False, path_length=None),
192
+ critical=True,
193
+ )
194
+ )
195
+ leaf_cert = builder.sign(self._ca_key, hashes.SHA256())
196
+
197
+ cert_pem = leaf_cert.public_bytes(serialization.Encoding.PEM)
198
+ key_pem = leaf_key.private_bytes(
199
+ encoding=serialization.Encoding.PEM,
200
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
201
+ encryption_algorithm=serialization.NoEncryption(),
202
+ )
203
+ return cert_pem, key_pem
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # Platform-specific CA trust helpers
208
+ # ---------------------------------------------------------------------------
209
+
210
+ def trust_ca_system(cert_path: Path | None = None) -> bool:
211
+ """Attempt to trust the CA certificate in the OS certificate store.
212
+
213
+ Returns True on success. May require elevated privileges.
214
+ """
215
+ cert_path = cert_path or ca_cert_path()
216
+ system = platform.system()
217
+ try:
218
+ if system == "Darwin":
219
+ return _trust_ca_macos(cert_path)
220
+ if system == "Linux":
221
+ return _trust_ca_linux(cert_path)
222
+ if system == "Windows":
223
+ return _trust_ca_windows(cert_path)
224
+ except Exception as exc:
225
+ logger.warning("CA trust failed: %s", exc)
226
+ return False
227
+
228
+
229
+ def _trust_ca_macos(cert_path: Path) -> bool:
230
+ result = subprocess.run(
231
+ [
232
+ "security", "add-trusted-cert",
233
+ "-d", # user trust domain
234
+ "-r", "trustRoot",
235
+ "-k", str(Path.home() / "Library" / "Keychains" / "login.keychain-db"),
236
+ str(cert_path),
237
+ ],
238
+ capture_output=True,
239
+ timeout=30,
240
+ )
241
+ return result.returncode == 0
242
+
243
+
244
+ def _trust_ca_linux(cert_path: Path) -> bool:
245
+ dest = Path("/usr/local/share/ca-certificates/mtrx-cursor-proxy.crt")
246
+ subprocess.run(["sudo", "cp", str(cert_path), str(dest)], check=True, timeout=30)
247
+ result = subprocess.run(["sudo", "update-ca-certificates"], capture_output=True, timeout=30)
248
+ return result.returncode == 0
249
+
250
+
251
+ def _trust_ca_windows(cert_path: Path) -> bool:
252
+ result = subprocess.run(
253
+ ["certutil", "-addstore", "Root", str(cert_path)],
254
+ capture_output=True,
255
+ timeout=30,
256
+ )
257
+ return result.returncode == 0
258
+
259
+
260
+ def is_ca_trusted() -> bool:
261
+ """Best-effort check whether the MTRX CA is trusted by the OS."""
262
+ if not ca_exists():
263
+ return False
264
+ system = platform.system()
265
+ try:
266
+ if system == "Darwin":
267
+ result = subprocess.run(
268
+ ["security", "find-certificate", "-c", "MTRX Cursor Proxy CA"],
269
+ capture_output=True,
270
+ timeout=10,
271
+ )
272
+ return result.returncode == 0
273
+ except Exception:
274
+ pass
275
+ return False