mtrx-cli 0.1.25 → 0.1.27
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 +1 -1
- package/src/matrx/__init__.py +1 -1
- package/src/matrx/cli/cursor_ca.py +257 -39
- package/src/matrx/cli/cursor_config.py +14 -1
- package/src/matrx/cli/cursor_daemon.py +4 -0
- package/src/matrx/cli/cursor_launcher.py +3 -1
- package/src/matrx/cli/cursor_proxy.py +412 -166
- package/src/matrx/cli/cursor_reroute.py +376 -17
- package/src/matrx/cli/launcher.py +47 -1
- package/src/matrx/cli/main.py +384 -59
- package/src/matrx/cli/state.py +21 -0
package/package.json
CHANGED
package/src/matrx/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.27"
|
|
@@ -12,14 +12,16 @@ import datetime
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import platform
|
|
15
|
+
import ssl
|
|
15
16
|
import subprocess
|
|
16
17
|
import threading
|
|
18
|
+
from dataclasses import dataclass
|
|
17
19
|
from pathlib import Path
|
|
18
20
|
|
|
19
21
|
from cryptography import x509
|
|
20
22
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
21
23
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
22
|
-
from cryptography.x509.oid import NameOID
|
|
24
|
+
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
|
23
25
|
|
|
24
26
|
from matrx.cli.state import config_dir
|
|
25
27
|
|
|
@@ -30,6 +32,24 @@ _CA_KEY_FILE = "mtrx-ca.key"
|
|
|
30
32
|
_CA_CERT_FILE = "mtrx-ca.pem"
|
|
31
33
|
_CERT_VALIDITY_YEARS = 5
|
|
32
34
|
_LEAF_VALIDITY_DAYS = 365
|
|
35
|
+
_CA_COMMON_NAME = "MTRX Cursor Proxy CA"
|
|
36
|
+
_CA_ORGANIZATION = "MTRX"
|
|
37
|
+
_WINDOWS_ROOT_USER = "CurrentUser\\Root"
|
|
38
|
+
_WINDOWS_ROOT_MACHINE = "LocalMachine\\Root"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class HostCertMaterial:
|
|
43
|
+
hostname: str
|
|
44
|
+
cert_pem: bytes
|
|
45
|
+
key_pem: bytes
|
|
46
|
+
cert_path: Path
|
|
47
|
+
key_path: Path
|
|
48
|
+
leaf_serial: str
|
|
49
|
+
leaf_sha256: str
|
|
50
|
+
ca_sha256: str
|
|
51
|
+
chain_length: int
|
|
52
|
+
ssl_context: ssl.SSLContext
|
|
33
53
|
|
|
34
54
|
|
|
35
55
|
def ca_dir() -> Path:
|
|
@@ -63,8 +83,8 @@ def generate_ca(*, force: bool = False) -> tuple[Path, Path]:
|
|
|
63
83
|
|
|
64
84
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
65
85
|
subject = issuer = x509.Name([
|
|
66
|
-
x509.NameAttribute(NameOID.ORGANIZATION_NAME,
|
|
67
|
-
x509.NameAttribute(NameOID.COMMON_NAME,
|
|
86
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _CA_ORGANIZATION),
|
|
87
|
+
x509.NameAttribute(NameOID.COMMON_NAME, _CA_COMMON_NAME),
|
|
68
88
|
])
|
|
69
89
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
70
90
|
cert = (
|
|
@@ -75,7 +95,11 @@ def generate_ca(*, force: bool = False) -> tuple[Path, Path]:
|
|
|
75
95
|
.serial_number(x509.random_serial_number())
|
|
76
96
|
.not_valid_before(now)
|
|
77
97
|
.not_valid_after(now + datetime.timedelta(days=_CERT_VALIDITY_YEARS * 365))
|
|
78
|
-
.add_extension(x509.BasicConstraints(ca=True, path_length=
|
|
98
|
+
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
|
99
|
+
.add_extension(
|
|
100
|
+
x509.SubjectKeyIdentifier.from_public_key(key.public_key()),
|
|
101
|
+
critical=False,
|
|
102
|
+
)
|
|
79
103
|
.add_extension(
|
|
80
104
|
x509.KeyUsage(
|
|
81
105
|
digital_signature=True,
|
|
@@ -127,48 +151,76 @@ class CertCache:
|
|
|
127
151
|
self._ca_key = ca_key
|
|
128
152
|
self._ca_cert = ca_cert
|
|
129
153
|
self._lock = threading.Lock()
|
|
130
|
-
self._cache: dict[str,
|
|
131
|
-
self._ctx_cache: dict[str, "ssl.SSLContext"] = {}
|
|
154
|
+
self._cache: dict[str, HostCertMaterial] = {}
|
|
132
155
|
self._certs_dir = ca_dir() / "certs"
|
|
133
156
|
self._certs_dir.mkdir(parents=True, exist_ok=True)
|
|
134
157
|
|
|
135
158
|
def get_cert_pair(self, hostname: str) -> tuple[bytes, bytes]:
|
|
136
159
|
"""Return (cert_pem, key_pem) for *hostname*, creating if needed."""
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
160
|
+
material = self.get_or_create_material(hostname)
|
|
161
|
+
return material.cert_pem, material.key_pem
|
|
146
162
|
|
|
147
163
|
def get_ssl_context(self, hostname: str) -> "ssl.SSLContext":
|
|
148
164
|
"""Return a server-side ``ssl.SSLContext`` for *hostname*."""
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
return self.get_or_create_material(hostname).ssl_context
|
|
166
|
+
|
|
167
|
+
def get_handshake_info(self, hostname: str) -> dict[str, str | int | bool]:
|
|
168
|
+
material = self.get_or_create_material(hostname)
|
|
169
|
+
return {
|
|
170
|
+
"hostname": hostname,
|
|
171
|
+
"leaf_serial": material.leaf_serial,
|
|
172
|
+
"leaf_sha256": material.leaf_sha256,
|
|
173
|
+
"ca_sha256": material.ca_sha256,
|
|
174
|
+
"chain_length": material.chain_length,
|
|
175
|
+
"cert_path": str(material.cert_path),
|
|
176
|
+
"key_path": str(material.key_path),
|
|
177
|
+
"cache_ready": True,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def prewarm(self, hostnames: list[str] | tuple[str, ...]) -> None:
|
|
181
|
+
for hostname in hostnames:
|
|
182
|
+
self.get_or_create_material(hostname)
|
|
183
|
+
|
|
184
|
+
def get_or_create_material(self, hostname: str) -> HostCertMaterial:
|
|
151
185
|
with self._lock:
|
|
152
|
-
|
|
153
|
-
|
|
186
|
+
material = self._cache.get(hostname)
|
|
187
|
+
if material is not None:
|
|
188
|
+
return material
|
|
154
189
|
|
|
155
|
-
|
|
190
|
+
material = self._build_material(hostname)
|
|
191
|
+
self._cache[hostname] = material
|
|
192
|
+
return material
|
|
156
193
|
|
|
194
|
+
def _build_material(self, hostname: str) -> HostCertMaterial:
|
|
195
|
+
cert_pem, key_pem, leaf_cert = self._sign_leaf(hostname)
|
|
157
196
|
cert_file = self._certs_dir / f"{hostname}.crt"
|
|
158
197
|
key_file = self._certs_dir / f"{hostname}.key"
|
|
159
198
|
cert_file.write_bytes(cert_pem)
|
|
160
199
|
key_file.write_bytes(key_pem)
|
|
161
200
|
os.chmod(key_file, 0o600)
|
|
162
201
|
|
|
163
|
-
|
|
202
|
+
chain = self._pem_certificates(cert_pem)
|
|
203
|
+
if len(chain) < 2:
|
|
204
|
+
raise ValueError(f"incomplete served certificate chain for {hostname}")
|
|
205
|
+
|
|
206
|
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
164
207
|
ctx.load_cert_chain(str(cert_file), str(key_file))
|
|
165
208
|
ctx.set_alpn_protocols(["http/1.1"])
|
|
166
209
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
return HostCertMaterial(
|
|
211
|
+
hostname=hostname,
|
|
212
|
+
cert_pem=cert_pem,
|
|
213
|
+
key_pem=key_pem,
|
|
214
|
+
cert_path=cert_file,
|
|
215
|
+
key_path=key_file,
|
|
216
|
+
leaf_serial=f"{leaf_cert.serial_number:X}",
|
|
217
|
+
leaf_sha256=leaf_cert.fingerprint(hashes.SHA256()).hex(),
|
|
218
|
+
ca_sha256=self._ca_cert.fingerprint(hashes.SHA256()).hex(),
|
|
219
|
+
chain_length=len(chain),
|
|
220
|
+
ssl_context=ctx,
|
|
221
|
+
)
|
|
170
222
|
|
|
171
|
-
def _sign_leaf(self, hostname: str) -> tuple[bytes, bytes]:
|
|
223
|
+
def _sign_leaf(self, hostname: str) -> tuple[bytes, bytes, x509.Certificate]:
|
|
172
224
|
leaf_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
173
225
|
now = datetime.datetime.now(datetime.timezone.utc)
|
|
174
226
|
subject = x509.Name([
|
|
@@ -182,7 +234,7 @@ class CertCache:
|
|
|
182
234
|
.public_key(leaf_key.public_key())
|
|
183
235
|
.serial_number(x509.random_serial_number())
|
|
184
236
|
.not_valid_before(now - datetime.timedelta(days=1))
|
|
185
|
-
.not_valid_after(now + datetime.timedelta(days=_LEAF_VALIDITY_DAYS))
|
|
237
|
+
.not_valid_after(now + datetime.timedelta(days=min(_LEAF_VALIDITY_DAYS, 397)))
|
|
186
238
|
.add_extension(
|
|
187
239
|
x509.SubjectAlternativeName([x509.DNSName(hostname)]),
|
|
188
240
|
critical=False,
|
|
@@ -191,16 +243,55 @@ class CertCache:
|
|
|
191
243
|
x509.BasicConstraints(ca=False, path_length=None),
|
|
192
244
|
critical=True,
|
|
193
245
|
)
|
|
246
|
+
.add_extension(
|
|
247
|
+
x509.KeyUsage(
|
|
248
|
+
digital_signature=True,
|
|
249
|
+
key_encipherment=True,
|
|
250
|
+
key_cert_sign=False,
|
|
251
|
+
crl_sign=False,
|
|
252
|
+
content_commitment=False,
|
|
253
|
+
data_encipherment=False,
|
|
254
|
+
key_agreement=False,
|
|
255
|
+
encipher_only=False,
|
|
256
|
+
decipher_only=False,
|
|
257
|
+
),
|
|
258
|
+
critical=True,
|
|
259
|
+
)
|
|
260
|
+
.add_extension(
|
|
261
|
+
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]),
|
|
262
|
+
critical=False,
|
|
263
|
+
)
|
|
264
|
+
.add_extension(
|
|
265
|
+
x509.SubjectKeyIdentifier.from_public_key(leaf_key.public_key()),
|
|
266
|
+
critical=False,
|
|
267
|
+
)
|
|
268
|
+
.add_extension(
|
|
269
|
+
x509.AuthorityKeyIdentifier.from_issuer_public_key(self._ca_key.public_key()),
|
|
270
|
+
critical=False,
|
|
271
|
+
)
|
|
194
272
|
)
|
|
195
273
|
leaf_cert = builder.sign(self._ca_key, hashes.SHA256())
|
|
196
274
|
|
|
197
|
-
cert_pem =
|
|
275
|
+
cert_pem = (
|
|
276
|
+
leaf_cert.public_bytes(serialization.Encoding.PEM)
|
|
277
|
+
+ self._ca_cert.public_bytes(serialization.Encoding.PEM)
|
|
278
|
+
)
|
|
198
279
|
key_pem = leaf_key.private_bytes(
|
|
199
280
|
encoding=serialization.Encoding.PEM,
|
|
200
281
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
201
282
|
encryption_algorithm=serialization.NoEncryption(),
|
|
202
283
|
)
|
|
203
|
-
return cert_pem, key_pem
|
|
284
|
+
return cert_pem, key_pem, leaf_cert
|
|
285
|
+
|
|
286
|
+
@staticmethod
|
|
287
|
+
def _pem_certificates(pem_bytes: bytes) -> list[bytes]:
|
|
288
|
+
marker = b"-----END CERTIFICATE-----"
|
|
289
|
+
certs: list[bytes] = []
|
|
290
|
+
for part in pem_bytes.split(marker):
|
|
291
|
+
if b"-----BEGIN CERTIFICATE-----" not in part:
|
|
292
|
+
continue
|
|
293
|
+
certs.append(part + marker + b"\n")
|
|
294
|
+
return certs
|
|
204
295
|
|
|
205
296
|
|
|
206
297
|
# ---------------------------------------------------------------------------
|
|
@@ -211,21 +302,89 @@ def trust_ca_system(cert_path: Path | None = None) -> bool:
|
|
|
211
302
|
"""Attempt to trust the CA certificate in the OS certificate store.
|
|
212
303
|
|
|
213
304
|
Returns True on success. May require elevated privileges.
|
|
305
|
+
Also persists NODE_EXTRA_CA_CERTS as a user environment variable so
|
|
306
|
+
Node.js processes trust the CA regardless of how they are launched.
|
|
214
307
|
"""
|
|
215
308
|
cert_path = cert_path or ca_cert_path()
|
|
216
309
|
system = platform.system()
|
|
310
|
+
ok = False
|
|
217
311
|
try:
|
|
218
312
|
if system == "Darwin":
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
313
|
+
ok = _trust_ca_macos(cert_path)
|
|
314
|
+
elif system == "Linux":
|
|
315
|
+
ok = _trust_ca_linux(cert_path)
|
|
316
|
+
elif system == "Windows":
|
|
317
|
+
ok = _trust_ca_windows(cert_path)
|
|
224
318
|
except Exception as exc:
|
|
225
319
|
logger.warning("CA trust failed: %s", exc)
|
|
320
|
+
if ok:
|
|
321
|
+
persist_node_extra_ca_certs(cert_path)
|
|
322
|
+
return ok
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def persist_node_extra_ca_certs(cert_path: Path | None = None) -> bool:
|
|
326
|
+
"""Persist NODE_EXTRA_CA_CERTS as a permanent user-level environment variable.
|
|
327
|
+
|
|
328
|
+
This ensures all Node.js processes — including Cursor sub-processes not
|
|
329
|
+
launched via the mtrx launcher — trust the MTRX CA certificate.
|
|
330
|
+
|
|
331
|
+
- Windows: ``setx NODE_EXTRA_CA_CERTS <path>`` writes to HKCU\\Environment;
|
|
332
|
+
all new user processes inherit it automatically.
|
|
333
|
+
- macOS: ``launchctl setenv`` for the current session; ``~/.zprofile`` for
|
|
334
|
+
persistence across reboots.
|
|
335
|
+
- Linux: ``~/.profile`` export line for login shells and display managers.
|
|
336
|
+
|
|
337
|
+
Returns True if the variable was persisted successfully.
|
|
338
|
+
"""
|
|
339
|
+
cert_path = cert_path or ca_cert_path()
|
|
340
|
+
value = str(cert_path)
|
|
341
|
+
system = platform.system()
|
|
342
|
+
try:
|
|
343
|
+
if system == "Windows":
|
|
344
|
+
result = subprocess.run(
|
|
345
|
+
["setx", "NODE_EXTRA_CA_CERTS", value],
|
|
346
|
+
capture_output=True,
|
|
347
|
+
timeout=15,
|
|
348
|
+
)
|
|
349
|
+
if result.returncode == 0:
|
|
350
|
+
logger.info("persist_node_extra_ca_certs: set via setx")
|
|
351
|
+
return True
|
|
352
|
+
logger.warning(
|
|
353
|
+
"persist_node_extra_ca_certs: setx failed: %s",
|
|
354
|
+
result.stderr.decode("utf-8", errors="replace").strip(),
|
|
355
|
+
)
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
if system == "Darwin":
|
|
359
|
+
subprocess.run(
|
|
360
|
+
["launchctl", "setenv", "NODE_EXTRA_CA_CERTS", value],
|
|
361
|
+
capture_output=True,
|
|
362
|
+
timeout=10,
|
|
363
|
+
)
|
|
364
|
+
zprofile = Path.home() / ".zprofile"
|
|
365
|
+
_append_env_export(zprofile, "NODE_EXTRA_CA_CERTS", value)
|
|
366
|
+
logger.info("persist_node_extra_ca_certs: set via launchctl + ~/.zprofile")
|
|
367
|
+
return True
|
|
368
|
+
|
|
369
|
+
if system == "Linux":
|
|
370
|
+
profile = Path.home() / ".profile"
|
|
371
|
+
_append_env_export(profile, "NODE_EXTRA_CA_CERTS", value)
|
|
372
|
+
logger.info("persist_node_extra_ca_certs: set via ~/.profile")
|
|
373
|
+
return True
|
|
374
|
+
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
logger.warning("persist_node_extra_ca_certs failed: %s", exc)
|
|
226
377
|
return False
|
|
227
378
|
|
|
228
379
|
|
|
380
|
+
def _append_env_export(path: Path, name: str, value: str) -> None:
|
|
381
|
+
"""Append ``export NAME="value"`` to a shell profile file if not already present."""
|
|
382
|
+
line = f'\nexport {name}="{value}" # added by mtrx\n'
|
|
383
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
384
|
+
if name not in text:
|
|
385
|
+
path.write_text(text + line, encoding="utf-8")
|
|
386
|
+
|
|
387
|
+
|
|
229
388
|
def _trust_ca_macos(cert_path: Path) -> bool:
|
|
230
389
|
result = subprocess.run(
|
|
231
390
|
[
|
|
@@ -249,14 +408,71 @@ def _trust_ca_linux(cert_path: Path) -> bool:
|
|
|
249
408
|
|
|
250
409
|
|
|
251
410
|
def _trust_ca_windows(cert_path: Path) -> bool:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
411
|
+
status = windows_trust_status(cert_path)
|
|
412
|
+
if status["fully_trusted"]:
|
|
413
|
+
return True
|
|
414
|
+
|
|
415
|
+
user_ok = status["user_root"] or _add_ca_to_windows_store(cert_path, user=True)
|
|
416
|
+
machine_ok = status["machine_root"]
|
|
417
|
+
if not machine_ok:
|
|
418
|
+
machine_ok = _add_ca_to_windows_store(cert_path, user=False)
|
|
419
|
+
return user_ok or machine_ok
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _cert_subject_and_fingerprint(cert_path: Path) -> tuple[str, str] | None:
|
|
423
|
+
try:
|
|
424
|
+
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
|
|
425
|
+
except (OSError, ValueError):
|
|
426
|
+
return None
|
|
427
|
+
return cert.subject.rfc4514_string(), cert.fingerprint(hashes.SHA1()).hex().upper()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _certutil_store_output(*, user: bool) -> str:
|
|
431
|
+
command = ["certutil"]
|
|
432
|
+
if user:
|
|
433
|
+
command.append("-user")
|
|
434
|
+
command.extend(["-store", "Root"])
|
|
435
|
+
try:
|
|
436
|
+
result = subprocess.run(command, capture_output=True, timeout=15)
|
|
437
|
+
except Exception:
|
|
438
|
+
return ""
|
|
439
|
+
if result.returncode != 0:
|
|
440
|
+
return ""
|
|
441
|
+
return result.stdout.decode("utf-8", errors="replace").upper()
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _is_ca_trusted_windows(cert_path: Path, *, user: bool) -> bool:
|
|
445
|
+
cert_info = _cert_subject_and_fingerprint(cert_path)
|
|
446
|
+
if cert_info is None:
|
|
447
|
+
return False
|
|
448
|
+
subject, fingerprint = cert_info
|
|
449
|
+
output = _certutil_store_output(user=user)
|
|
450
|
+
return fingerprint in output or subject.upper() in output
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _add_ca_to_windows_store(cert_path: Path, *, user: bool) -> bool:
|
|
454
|
+
command = ["certutil"]
|
|
455
|
+
if user:
|
|
456
|
+
command.append("-user")
|
|
457
|
+
command.extend(["-addstore", "Root", str(cert_path)])
|
|
458
|
+
try:
|
|
459
|
+
result = subprocess.run(command, capture_output=True, timeout=30)
|
|
460
|
+
except Exception:
|
|
461
|
+
return False
|
|
257
462
|
return result.returncode == 0
|
|
258
463
|
|
|
259
464
|
|
|
465
|
+
def windows_trust_status(cert_path: Path | None = None) -> dict[str, bool]:
|
|
466
|
+
cert_path = cert_path or ca_cert_path()
|
|
467
|
+
user_root = _is_ca_trusted_windows(cert_path, user=True)
|
|
468
|
+
machine_root = _is_ca_trusted_windows(cert_path, user=False)
|
|
469
|
+
return {
|
|
470
|
+
"user_root": user_root,
|
|
471
|
+
"machine_root": machine_root,
|
|
472
|
+
"fully_trusted": user_root and machine_root,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
|
|
260
476
|
def is_ca_trusted() -> bool:
|
|
261
477
|
"""Best-effort check whether the MTRX CA is trusted by the OS."""
|
|
262
478
|
if not ca_exists():
|
|
@@ -265,11 +481,13 @@ def is_ca_trusted() -> bool:
|
|
|
265
481
|
try:
|
|
266
482
|
if system == "Darwin":
|
|
267
483
|
result = subprocess.run(
|
|
268
|
-
["security", "find-certificate", "-c",
|
|
484
|
+
["security", "find-certificate", "-c", _CA_COMMON_NAME],
|
|
269
485
|
capture_output=True,
|
|
270
486
|
timeout=10,
|
|
271
487
|
)
|
|
272
488
|
return result.returncode == 0
|
|
489
|
+
if system == "Windows":
|
|
490
|
+
return windows_trust_status(ca_cert_path())["fully_trusted"]
|
|
273
491
|
except Exception:
|
|
274
492
|
pass
|
|
275
493
|
return False
|
|
@@ -464,6 +464,8 @@ def configure_cursor_proxy_settings(
|
|
|
464
464
|
previous = {
|
|
465
465
|
"http.proxy": settings.get("http.proxy"),
|
|
466
466
|
"http.proxyStrictSSL": settings.get("http.proxyStrictSSL"),
|
|
467
|
+
"http.proxySupport": settings.get("http.proxySupport"),
|
|
468
|
+
"http.systemCertificates": settings.get("http.systemCertificates"),
|
|
467
469
|
"terminal.integrated.env.osx": settings.get("terminal.integrated.env.osx"),
|
|
468
470
|
"terminal.integrated.env.linux": settings.get("terminal.integrated.env.linux"),
|
|
469
471
|
"terminal.integrated.env.windows": settings.get("terminal.integrated.env.windows"),
|
|
@@ -471,10 +473,14 @@ def configure_cursor_proxy_settings(
|
|
|
471
473
|
|
|
472
474
|
settings["http.proxy"] = proxy_url
|
|
473
475
|
settings["http.proxyStrictSSL"] = False
|
|
476
|
+
settings["http.proxySupport"] = "override" # force all extensions through proxy agent
|
|
477
|
+
settings["http.systemCertificates"] = True # use OS store where our CA is already trusted
|
|
474
478
|
|
|
475
479
|
# Inject NODE_EXTRA_CA_CERTS into integrated terminal env so Cursor's
|
|
476
480
|
# Node.js runtime trusts our CA. Cursor itself reads this from the
|
|
477
481
|
# process environment, but setting it here covers more cases.
|
|
482
|
+
# Explicitly unset HTTPS_PROXY/HTTP_PROXY in terminals so child processes
|
|
483
|
+
# (e.g. Claude Code) do NOT inherit the proxy from the Cursor process.
|
|
478
484
|
for env_key in (
|
|
479
485
|
"terminal.integrated.env.osx",
|
|
480
486
|
"terminal.integrated.env.linux",
|
|
@@ -484,6 +490,11 @@ def configure_cursor_proxy_settings(
|
|
|
484
490
|
if not isinstance(env_block, dict):
|
|
485
491
|
env_block = {}
|
|
486
492
|
env_block["NODE_EXTRA_CA_CERTS"] = ca_cert_path
|
|
493
|
+
# null unsets an inherited env var in VS Code integrated terminals
|
|
494
|
+
env_block["HTTPS_PROXY"] = None
|
|
495
|
+
env_block["HTTP_PROXY"] = None
|
|
496
|
+
env_block["https_proxy"] = None
|
|
497
|
+
env_block["http_proxy"] = None
|
|
487
498
|
settings[env_key] = env_block
|
|
488
499
|
|
|
489
500
|
_write_settings_json(settings)
|
|
@@ -494,7 +505,7 @@ def restore_cursor_proxy_settings(previous: dict) -> bool:
|
|
|
494
505
|
"""Restore Cursor's settings.json to pre-proxy values."""
|
|
495
506
|
settings = _read_settings_json()
|
|
496
507
|
|
|
497
|
-
for key in ("http.proxy", "http.proxyStrictSSL"):
|
|
508
|
+
for key in ("http.proxy", "http.proxyStrictSSL", "http.proxySupport", "http.systemCertificates"):
|
|
498
509
|
old = previous.get(key)
|
|
499
510
|
if old is None:
|
|
500
511
|
settings.pop(key, None)
|
|
@@ -511,6 +522,8 @@ def restore_cursor_proxy_settings(previous: dict) -> bool:
|
|
|
511
522
|
env_block = settings.get(env_key)
|
|
512
523
|
if isinstance(env_block, dict):
|
|
513
524
|
env_block.pop("NODE_EXTRA_CA_CERTS", None)
|
|
525
|
+
for _k in ("HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"):
|
|
526
|
+
env_block.pop(_k, None)
|
|
514
527
|
if not env_block:
|
|
515
528
|
settings.pop(env_key, None)
|
|
516
529
|
else:
|
|
@@ -41,6 +41,8 @@ def main() -> int:
|
|
|
41
41
|
matrx_base_url = config.get("matrx_base_url", "")
|
|
42
42
|
host = config.get("host", "127.0.0.1")
|
|
43
43
|
port = config.get("port", 8842)
|
|
44
|
+
agent_id = config.get("agent_id") or None
|
|
45
|
+
group_id = config.get("group_id") or None
|
|
44
46
|
|
|
45
47
|
if not matrx_key or not matrx_base_url:
|
|
46
48
|
print("Invalid proxy config: matrx_key and matrx_base_url required", file=sys.stderr)
|
|
@@ -56,6 +58,8 @@ def main() -> int:
|
|
|
56
58
|
host=host,
|
|
57
59
|
port=port,
|
|
58
60
|
pid_file=pid_file,
|
|
61
|
+
agent_id=agent_id,
|
|
62
|
+
group_id=group_id,
|
|
59
63
|
)
|
|
60
64
|
return 0
|
|
61
65
|
|
|
@@ -85,14 +85,16 @@ def launch_cursor_with_proxy(
|
|
|
85
85
|
env["HTTP_PROXY"] = proxy_url
|
|
86
86
|
env["HTTPS_PROXY"] = proxy_url
|
|
87
87
|
env["NODE_EXTRA_CA_CERTS"] = str(ca_cert_path)
|
|
88
|
+
launch_args = [exe]
|
|
88
89
|
|
|
89
90
|
kwargs: dict = {}
|
|
90
91
|
if platform.system() == "Windows":
|
|
91
92
|
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
|
93
|
+
launch_args.append("--disable-quic")
|
|
92
94
|
|
|
93
95
|
try:
|
|
94
96
|
subprocess.Popen(
|
|
95
|
-
|
|
97
|
+
launch_args,
|
|
96
98
|
env=env,
|
|
97
99
|
stdin=subprocess.DEVNULL,
|
|
98
100
|
stdout=subprocess.DEVNULL,
|