@melaya/runner 1.1.41 → 1.1.44

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.
@@ -1,252 +1,295 @@
1
- #!/usr/bin/env python3
2
- """
3
- localDbProbe.py one-shot, read-only database reachability probe run BY THE
4
- RUNNER on behalf of the Melaya server.
5
-
6
- Why it exists: some databases sit behind an IP allow-list or inside a VPC that
7
- the Melaya cloud can never reach, but the user's own runner (on their LAN /
8
- bastion) can. The server hands us the resolved credentials over the already-
9
- authenticated Socket.IO channel; we open a single real connection from THIS
10
- machine's network vantage point, run a trivial read-only probe, and print the
11
- outcome. We never persist the credentials and never write to the database.
12
-
13
- Contract:
14
- stdin : one JSON object {"service": "...", "creds": { ... }}
15
- stdout : one JSON object {"ok": bool, "message"|"error": "..."}
16
- Exit code is always 0 (the result is the JSON on stdout); any crash is caught
17
- and reported as {"ok": false, "error": ...}.
18
- """
19
- import sys
20
- import json
21
- import base64
22
- import hashlib
23
- import urllib.parse
24
-
25
-
26
- def _out(ok, text):
27
- key = "message" if ok else "error"
28
- sys.stdout.write(json.dumps({"ok": bool(ok), key: str(text)[:300]}))
29
- sys.stdout.flush()
30
- sys.exit(0)
31
-
32
-
33
- def _pick(creds, *keys):
34
- for k in keys:
35
- v = (creds.get(k) or "").strip()
36
- if v:
37
- return v
38
- return ""
39
-
40
-
41
- # ── PostgreSQL (asyncpg) ─────────────────────────────────────────────────────
42
- def probe_postgres(creds):
43
- dsn = _pick(creds, "dsn", "POSTGRES_DSN")
44
- if not dsn:
45
- _out(False, "Connection DSN required")
46
- try:
47
- import asyncio
48
- import asyncpg # type: ignore
49
- except ImportError:
50
- _out(False, "asyncpg not installed on this runner (pip install asyncpg)")
51
-
52
- async def _run():
53
- conn = await asyncpg.connect(dsn, timeout=10)
54
- try:
55
- await conn.fetchval("SELECT 1")
56
- ver = await conn.fetchval("SELECT version()")
57
- finally:
58
- await conn.close()
59
- return ver or ""
60
-
61
- try:
62
- ver = asyncio.run(asyncio.wait_for(_run(), timeout=15))
63
- short = str(ver).split(" on ")[0] if ver else ""
64
- _out(True, f"Connected to PostgreSQL{(' ' + short) if short else ''}")
65
- except Exception as e: # noqa: BLE001
66
- _out(False, f"PostgreSQL connection failed: {e}")
67
-
68
-
69
- # ── MySQL / MariaDB (PyMySQL pure-python, sync) ────────────────────────────
70
- def probe_mysql(creds):
71
- dsn = _pick(creds, "dsn", "MYSQL_DSN")
72
- if not dsn:
73
- _out(False, "Connection DSN required")
74
- try:
75
- import pymysql # type: ignore
76
- except ImportError:
77
- _out(False, "PyMySQL not installed on this runner (pip install PyMySQL)")
78
- try:
79
- p = urllib.parse.urlparse(dsn)
80
- conn = pymysql.connect(
81
- host=p.hostname,
82
- port=p.port or 3306,
83
- user=urllib.parse.unquote(p.username or ""),
84
- password=urllib.parse.unquote(p.password or ""),
85
- db=(p.path or "").lstrip("/") or None,
86
- connect_timeout=10,
87
- read_timeout=10,
88
- )
89
- try:
90
- with conn.cursor() as cur:
91
- cur.execute("SELECT VERSION()")
92
- row = cur.fetchone()
93
- ver = (row[0] if row else "") or ""
94
- finally:
95
- conn.close()
96
- _out(True, f"Connected to MySQL{(' — ' + str(ver)) if ver else ''}")
97
- except Exception as e: # noqa: BLE001
98
- _out(False, f"MySQL connection failed: {e}")
99
-
100
-
101
- # ── Snowflake (key-pair JWT, mirrors the server handler) ─────────────────────
102
- def probe_snowflake(creds):
103
- account = _pick(creds, "account", "SNOWFLAKE_ACCOUNT")
104
- user = _pick(creds, "user", "SNOWFLAKE_USER")
105
- pk = _pick(creds, "private_key", "SNOWFLAKE_PRIVATE_KEY")
106
- if not account or not user or not pk:
107
- _out(False, "Account + Username + Private Key required")
108
- try:
109
- from cryptography.hazmat.primitives import serialization # type: ignore
110
- import requests # type: ignore
111
- except ImportError:
112
- _out(False, "cryptography/requests not installed on this runner")
113
-
114
- def b64url(b):
115
- if isinstance(b, str):
116
- b = b.encode()
117
- return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
118
-
119
- try:
120
- import time
121
- key = serialization.load_pem_private_key(pk.encode(), password=None)
122
- der_spki = key.public_key().public_bytes(
123
- serialization.Encoding.DER,
124
- serialization.PublicFormat.SubjectPublicKeyInfo,
125
- )
126
- fp = "SHA256:" + base64.b64encode(hashlib.sha256(der_spki).digest()).decode()
127
- acct_upper = account.upper()
128
- if "." in acct_upper:
129
- acct_upper = acct_upper.split(".")[0]
130
- qual = f"{acct_upper}.{user.upper()}"
131
- now = int(time.time())
132
- header = b64url(json.dumps({"alg": "RS256", "typ": "JWT"}))
133
- payload = b64url(json.dumps(
134
- {"iss": f"{qual}.{fp}", "sub": qual, "iat": now, "exp": now + 3540}))
135
- signing_input = f"{header}.{payload}".encode()
136
- from cryptography.hazmat.primitives import hashes # type: ignore
137
- from cryptography.hazmat.primitives.asymmetric import padding # type: ignore
138
- sig = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
139
- jwt = f"{header}.{payload}.{b64url(sig)}"
140
- host = f"https://{account.replace('_', '-').lower()}.snowflakecomputing.com"
141
- r = requests.post(
142
- f"{host}/api/v2/statements",
143
- headers={
144
- "Authorization": f"Bearer {jwt}",
145
- "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT",
146
- "Content-Type": "application/json",
147
- "Accept": "application/json",
148
- },
149
- json={"statement": "SELECT CURRENT_VERSION()", "timeout": 20},
150
- timeout=15,
151
- )
152
- if r.ok:
153
- v = ""
154
- try:
155
- v = (r.json().get("data") or [[None]])[0][0] or ""
156
- except Exception: # noqa: BLE001
157
- pass
158
- _out(True, f"Connected to Snowflake{(' (v' + str(v) + ')') if v else ''}")
159
- _out(False, f"Snowflake auth/query failed: HTTP {r.status_code} {r.text[:160]}")
160
- except Exception as e: # noqa: BLE001
161
- _out(False, f"Snowflake connection failed: {e}")
162
-
163
-
164
- # ── Databricks (PAT or OAuth M2M, mirrors the server handler) ────────────────
165
- def probe_databricks(creds):
166
- host = _pick(creds, "workspace_url", "DATABRICKS_HOST").rstrip("/")
167
- pat = _pick(creds, "token", "DATABRICKS_TOKEN")
168
- cid = _pick(creds, "client_id", "DATABRICKS_CLIENT_ID")
169
- sec = _pick(creds, "client_secret", "DATABRICKS_CLIENT_SECRET")
170
- if not host:
171
- _out(False, "Workspace URL required")
172
- try:
173
- import requests # type: ignore
174
- except ImportError:
175
- _out(False, "requests not installed on this runner")
176
- try:
177
- bearer = pat
178
- if not bearer:
179
- if not cid or not sec:
180
- _out(False, "Provide a Personal Access Token, or Client ID + Client Secret")
181
- tr = requests.post(
182
- f"{host}/oidc/v1/token",
183
- auth=(cid, sec),
184
- data={"grant_type": "client_credentials", "scope": "all-apis"},
185
- timeout=12,
186
- )
187
- tj = {}
188
- try:
189
- tj = tr.json()
190
- except Exception: # noqa: BLE001
191
- pass
192
- if not tr.ok or not tj.get("access_token"):
193
- _out(False, f"Databricks OAuth failed: {tj.get('error_description') or tj.get('error') or ('HTTP ' + str(tr.status_code))}")
194
- bearer = tj["access_token"]
195
- r = requests.get(
196
- f"{host}/api/2.1/clusters/list?page_size=1",
197
- headers={"Authorization": f"Bearer {bearer}"},
198
- timeout=12,
199
- )
200
- if r.ok:
201
- _out(True, f"Connected to Databricks ({host.replace('https://', '').replace('http://', '')})")
202
- _out(False, f"Databricks unreachable: HTTP {r.status_code} {r.text[:120]}")
203
- except Exception as e: # noqa: BLE001
204
- _out(False, f"Databricks connection failed: {e}")
205
-
206
-
207
- # ── SQLite (local file on the runner) ────────────────────────────────────────
208
- def probe_sqlite(creds):
209
- import os
210
- import sqlite3
211
- path = _pick(creds, "dsn", "db_path", "path")
212
- if not path:
213
- _out(False, "Database file path required")
214
- if not os.path.exists(path):
215
- _out(False, f"No SQLite file at {path}")
216
- try:
217
- conn = sqlite3.connect(path, timeout=8)
218
- try:
219
- conn.execute("SELECT 1")
220
- finally:
221
- conn.close()
222
- _out(True, f"Opened SQLite database ({os.path.basename(path)})")
223
- except Exception as e: # noqa: BLE001
224
- _out(False, f"SQLite open failed: {e}")
225
-
226
-
227
- _PROBES = {
228
- "postgres": probe_postgres,
229
- "mysql": probe_mysql,
230
- "snowflake": probe_snowflake,
231
- "databricks": probe_databricks,
232
- "sqlite": probe_sqlite,
233
- }
234
-
235
-
236
- def main():
237
- try:
238
- job = json.loads(sys.stdin.read() or "{}")
239
- except Exception as e: # noqa: BLE001
240
- _out(False, f"bad probe job: {e}")
241
- return
242
- service = (job.get("service") or "").strip()
243
- creds = job.get("creds") or {}
244
- fn = _PROBES.get(service)
245
- if not fn:
246
- _out(False, f"unsupported service: {service}")
247
- return
248
- fn(creds)
249
-
250
-
251
- if __name__ == "__main__":
252
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ localDbProbe.py - one-shot, read-only database reachability probe run BY THE
4
+ RUNNER on behalf of the Melaya server.
5
+
6
+ Why it exists: some databases sit behind an IP allow-list or inside a VPC that
7
+ the Melaya cloud can never reach, but the user's own runner (on their LAN /
8
+ bastion) can. The server hands us the resolved credentials over the already-
9
+ authenticated Socket.IO channel; we open a single real connection from THIS
10
+ machine's network vantage point, run a trivial read-only probe, and print the
11
+ outcome. We never persist the credentials and never write to the database.
12
+
13
+ Contract:
14
+ stdin : one JSON object {"service": "...", "creds": { ... }}
15
+ stdout : one JSON object {"ok": bool, "message"|"error": "..."}
16
+ Exit code is always 0 (the result is the JSON on stdout); any crash is caught
17
+ and reported as {"ok": false, "error": ...}.
18
+ """
19
+ import sys
20
+ import json
21
+ import base64
22
+ import hashlib
23
+ import urllib.parse
24
+
25
+
26
+ def _out(ok, text):
27
+ key = "message" if ok else "error"
28
+ sys.stdout.write(json.dumps({"ok": bool(ok), key: str(text)[:300]}))
29
+ sys.stdout.flush()
30
+ sys.exit(0)
31
+
32
+
33
+ def _pick(creds, *keys):
34
+ for k in keys:
35
+ v = (creds.get(k) or "").strip()
36
+ if v:
37
+ return v
38
+ return ""
39
+
40
+
41
+ _PLACEHOLDER_HOSTS = {"host", "hostname", "your-host", "your_host", "<host>", "myhost", "dbhost", "example.com"}
42
+
43
+
44
+ def _friendly_conn_error(e, host, port, engine):
45
+ """Turn a raw driver/socket exception into an actionable message. The most
46
+ common failure is a copy-pasted EXAMPLE DSN (host == 'host') or a Docker/VPC
47
+ hostname that doesn't resolve from the runner's machine."""
48
+ import socket
49
+ msg = str(e) or e.__class__.__name__
50
+ low = msg.lower()
51
+ host = host or ""
52
+ is_dns = isinstance(e, socket.gaierror) or "nodename nor servname" in low \
53
+ or "name or service not known" in low or "getaddrinfo" in low \
54
+ or "name does not resolve" in low or "no address associated" in low
55
+ if is_dns:
56
+ if host.lower() in _PLACEHOLDER_HOSTS:
57
+ return (f"the host is literally \"{host}\" - that's the EXAMPLE DSN, not your database. "
58
+ f"Paste your real connection string with the actual host, e.g. an AWS RDS / Cloud SQL "
59
+ f"endpoint: {engine.lower()}://user:pass@your-db.abc123.us-east-1.rds.amazonaws.com:{port}/db.")
60
+ if host and "." not in host and host not in ("localhost",):
61
+ return (f"can't resolve host \"{host}\" from the runner's machine. A bare name like this is "
62
+ f"usually a Docker/compose service or a VPC-internal alias that only resolves elsewhere - "
63
+ f"use the host that's reachable from this machine (e.g. your RDS/Cloud SQL endpoint, or a VPN/bastion host).")
64
+ return (f"can't resolve host \"{host}\" from the runner's machine - check the host in your DSN, and that "
65
+ f"this machine can reach it (DNS + network route; for a private RDS/VPC DB the runner must sit in the VPC or on an allow-listed IP).")
66
+ if isinstance(e, ConnectionRefusedError) or "refused" in low:
67
+ return f"host \"{host}\" reachable but port {port} refused the connection - is {engine} listening there / bound to that interface?"
68
+ if isinstance(e, (TimeoutError,)) or "timed out" in low or "timeout" in low:
69
+ return f"timed out connecting to {host}:{port} - firewall, IP allow-list, or wrong host/port?"
70
+ if "password authentication failed" in low or "authentication failed" in low or "access denied" in low:
71
+ return "authentication failed - check the username/password in the DSN."
72
+ if 'database "' in low and "does not exist" in low or "unknown database" in low:
73
+ return f"connected, but the database name in the DSN doesn't exist: {msg}"
74
+ return msg
75
+
76
+
77
+ # ── PostgreSQL (asyncpg) ─────────────────────────────────────────────────────
78
+ def probe_postgres(creds):
79
+ dsn = _pick(creds, "dsn", "POSTGRES_DSN")
80
+ if not dsn:
81
+ _out(False, "Connection DSN required")
82
+ try:
83
+ import asyncio
84
+ import asyncpg # type: ignore
85
+ except ImportError:
86
+ _out(False, "asyncpg not installed on this runner (pip install asyncpg)")
87
+ p = urllib.parse.urlparse(dsn)
88
+ host, port = (p.hostname or ""), (p.port or 5432)
89
+ if not host:
90
+ _out(False, "PostgreSQL connection failed: no host in the DSN - expected postgresql://user:pass@HOST:5432/db")
91
+
92
+ async def _run():
93
+ conn = await asyncpg.connect(dsn, timeout=10)
94
+ try:
95
+ await conn.fetchval("SELECT 1")
96
+ ver = await conn.fetchval("SELECT version()")
97
+ finally:
98
+ await conn.close()
99
+ return ver or ""
100
+
101
+ try:
102
+ ver = asyncio.run(asyncio.wait_for(_run(), timeout=15))
103
+ short = str(ver).split(" on ")[0] if ver else ""
104
+ _out(True, f"Connected to PostgreSQL{(' - ' + short) if short else ''}")
105
+ except Exception as e: # noqa: BLE001
106
+ _out(False, f"PostgreSQL connection failed: {_friendly_conn_error(e, host, port, 'PostgreSQL')}")
107
+
108
+
109
+ # ── MySQL / MariaDB (PyMySQL - pure-python, sync) ────────────────────────────
110
+ def probe_mysql(creds):
111
+ dsn = _pick(creds, "dsn", "MYSQL_DSN")
112
+ if not dsn:
113
+ _out(False, "Connection DSN required")
114
+ try:
115
+ import pymysql # type: ignore
116
+ except ImportError:
117
+ _out(False, "PyMySQL not installed on this runner (pip install PyMySQL)")
118
+ p = urllib.parse.urlparse(dsn)
119
+ host, port = (p.hostname or ""), (p.port or 3306)
120
+ if not host:
121
+ _out(False, "MySQL connection failed: no host in the DSN - expected mysql://user:pass@HOST:3306/db")
122
+ try:
123
+ conn = pymysql.connect(
124
+ host=host,
125
+ port=port,
126
+ user=urllib.parse.unquote(p.username or ""),
127
+ password=urllib.parse.unquote(p.password or ""),
128
+ db=(p.path or "").lstrip("/") or None,
129
+ connect_timeout=10,
130
+ read_timeout=10,
131
+ )
132
+ try:
133
+ with conn.cursor() as cur:
134
+ cur.execute("SELECT VERSION()")
135
+ row = cur.fetchone()
136
+ ver = (row[0] if row else "") or ""
137
+ finally:
138
+ conn.close()
139
+ _out(True, f"Connected to MySQL{(' - ' + str(ver)) if ver else ''}")
140
+ except Exception as e: # noqa: BLE001
141
+ _out(False, f"MySQL connection failed: {_friendly_conn_error(e, host, port, 'MySQL')}")
142
+
143
+
144
+ # ── Snowflake (key-pair JWT, mirrors the server handler) ─────────────────────
145
+ def probe_snowflake(creds):
146
+ account = _pick(creds, "account", "SNOWFLAKE_ACCOUNT")
147
+ user = _pick(creds, "user", "SNOWFLAKE_USER")
148
+ pk = _pick(creds, "private_key", "SNOWFLAKE_PRIVATE_KEY")
149
+ if not account or not user or not pk:
150
+ _out(False, "Account + Username + Private Key required")
151
+ try:
152
+ from cryptography.hazmat.primitives import serialization # type: ignore
153
+ import requests # type: ignore
154
+ except ImportError:
155
+ _out(False, "cryptography/requests not installed on this runner")
156
+
157
+ def b64url(b):
158
+ if isinstance(b, str):
159
+ b = b.encode()
160
+ return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
161
+
162
+ try:
163
+ import time
164
+ key = serialization.load_pem_private_key(pk.encode(), password=None)
165
+ der_spki = key.public_key().public_bytes(
166
+ serialization.Encoding.DER,
167
+ serialization.PublicFormat.SubjectPublicKeyInfo,
168
+ )
169
+ fp = "SHA256:" + base64.b64encode(hashlib.sha256(der_spki).digest()).decode()
170
+ acct_upper = account.upper()
171
+ if "." in acct_upper:
172
+ acct_upper = acct_upper.split(".")[0]
173
+ qual = f"{acct_upper}.{user.upper()}"
174
+ now = int(time.time())
175
+ header = b64url(json.dumps({"alg": "RS256", "typ": "JWT"}))
176
+ payload = b64url(json.dumps(
177
+ {"iss": f"{qual}.{fp}", "sub": qual, "iat": now, "exp": now + 3540}))
178
+ signing_input = f"{header}.{payload}".encode()
179
+ from cryptography.hazmat.primitives import hashes # type: ignore
180
+ from cryptography.hazmat.primitives.asymmetric import padding # type: ignore
181
+ sig = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
182
+ jwt = f"{header}.{payload}.{b64url(sig)}"
183
+ host = f"https://{account.replace('_', '-').lower()}.snowflakecomputing.com"
184
+ r = requests.post(
185
+ f"{host}/api/v2/statements",
186
+ headers={
187
+ "Authorization": f"Bearer {jwt}",
188
+ "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT",
189
+ "Content-Type": "application/json",
190
+ "Accept": "application/json",
191
+ },
192
+ json={"statement": "SELECT CURRENT_VERSION()", "timeout": 20},
193
+ timeout=15,
194
+ )
195
+ if r.ok:
196
+ v = ""
197
+ try:
198
+ v = (r.json().get("data") or [[None]])[0][0] or ""
199
+ except Exception: # noqa: BLE001
200
+ pass
201
+ _out(True, f"Connected to Snowflake{(' (v' + str(v) + ')') if v else ''}")
202
+ _out(False, f"Snowflake auth/query failed: HTTP {r.status_code} {r.text[:160]}")
203
+ except Exception as e: # noqa: BLE001
204
+ _out(False, f"Snowflake connection failed: {e}")
205
+
206
+
207
+ # ── Databricks (PAT or OAuth M2M, mirrors the server handler) ────────────────
208
+ def probe_databricks(creds):
209
+ host = _pick(creds, "workspace_url", "DATABRICKS_HOST").rstrip("/")
210
+ pat = _pick(creds, "token", "DATABRICKS_TOKEN")
211
+ cid = _pick(creds, "client_id", "DATABRICKS_CLIENT_ID")
212
+ sec = _pick(creds, "client_secret", "DATABRICKS_CLIENT_SECRET")
213
+ if not host:
214
+ _out(False, "Workspace URL required")
215
+ try:
216
+ import requests # type: ignore
217
+ except ImportError:
218
+ _out(False, "requests not installed on this runner")
219
+ try:
220
+ bearer = pat
221
+ if not bearer:
222
+ if not cid or not sec:
223
+ _out(False, "Provide a Personal Access Token, or Client ID + Client Secret")
224
+ tr = requests.post(
225
+ f"{host}/oidc/v1/token",
226
+ auth=(cid, sec),
227
+ data={"grant_type": "client_credentials", "scope": "all-apis"},
228
+ timeout=12,
229
+ )
230
+ tj = {}
231
+ try:
232
+ tj = tr.json()
233
+ except Exception: # noqa: BLE001
234
+ pass
235
+ if not tr.ok or not tj.get("access_token"):
236
+ _out(False, f"Databricks OAuth failed: {tj.get('error_description') or tj.get('error') or ('HTTP ' + str(tr.status_code))}")
237
+ bearer = tj["access_token"]
238
+ r = requests.get(
239
+ f"{host}/api/2.1/clusters/list?page_size=1",
240
+ headers={"Authorization": f"Bearer {bearer}"},
241
+ timeout=12,
242
+ )
243
+ if r.ok:
244
+ _out(True, f"Connected to Databricks ({host.replace('https://', '').replace('http://', '')})")
245
+ _out(False, f"Databricks unreachable: HTTP {r.status_code} {r.text[:120]}")
246
+ except Exception as e: # noqa: BLE001
247
+ _out(False, f"Databricks connection failed: {e}")
248
+
249
+
250
+ # ── SQLite (local file on the runner) ────────────────────────────────────────
251
+ def probe_sqlite(creds):
252
+ import os
253
+ import sqlite3
254
+ path = _pick(creds, "dsn", "db_path", "path")
255
+ if not path:
256
+ _out(False, "Database file path required")
257
+ if not os.path.exists(path):
258
+ _out(False, f"No SQLite file at {path}")
259
+ try:
260
+ conn = sqlite3.connect(path, timeout=8)
261
+ try:
262
+ conn.execute("SELECT 1")
263
+ finally:
264
+ conn.close()
265
+ _out(True, f"Opened SQLite database ({os.path.basename(path)})")
266
+ except Exception as e: # noqa: BLE001
267
+ _out(False, f"SQLite open failed: {e}")
268
+
269
+
270
+ _PROBES = {
271
+ "postgres": probe_postgres,
272
+ "mysql": probe_mysql,
273
+ "snowflake": probe_snowflake,
274
+ "databricks": probe_databricks,
275
+ "sqlite": probe_sqlite,
276
+ }
277
+
278
+
279
+ def main():
280
+ try:
281
+ job = json.loads(sys.stdin.read() or "{}")
282
+ except Exception as e: # noqa: BLE001
283
+ _out(False, f"bad probe job: {e}")
284
+ return
285
+ service = (job.get("service") or "").strip()
286
+ creds = job.get("creds") or {}
287
+ fn = _PROBES.get(service)
288
+ if not fn:
289
+ _out(False, f"unsupported service: {service}")
290
+ return
291
+ fn(creds)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()
package/dist/pythonEnv.js CHANGED
@@ -204,6 +204,17 @@ const PIP_DEPS = [
204
204
  // via the anthropic/openai wheels; pinned explicitly so a future dep
205
205
  // shuffle in those SDKs can't silently break the crew risk watcher.
206
206
  "httpx",
207
+ // ── Database connectors + "Test via runner" (localDbProbe.py) ──────────
208
+ // asyncpg + PyMySQL power the PostgreSQL / MySQL tools (shared.tools.database)
209
+ // AND the runner-side connection probe. Without them the DB tools 401 at
210
+ // runtime and the "Test via runner" button returns "asyncpg/PyMySQL not
211
+ // installed on this runner". cryptography (also a common transitive dep) is
212
+ // pinned so the Snowflake key-pair JWT path in the probe always resolves.
213
+ // These are the runner twin of the requirements.lock entries — the runner
214
+ // venv installs from THIS list, not the lock, so they must be listed here too.
215
+ "asyncpg",
216
+ "PyMySQL",
217
+ "cryptography",
207
218
  ];
208
219
  export function venvPython() {
209
220
  return platform() === "win32"
package/localDbProbe.py CHANGED
@@ -1,252 +1,295 @@
1
- #!/usr/bin/env python3
2
- """
3
- localDbProbe.py one-shot, read-only database reachability probe run BY THE
4
- RUNNER on behalf of the Melaya server.
5
-
6
- Why it exists: some databases sit behind an IP allow-list or inside a VPC that
7
- the Melaya cloud can never reach, but the user's own runner (on their LAN /
8
- bastion) can. The server hands us the resolved credentials over the already-
9
- authenticated Socket.IO channel; we open a single real connection from THIS
10
- machine's network vantage point, run a trivial read-only probe, and print the
11
- outcome. We never persist the credentials and never write to the database.
12
-
13
- Contract:
14
- stdin : one JSON object {"service": "...", "creds": { ... }}
15
- stdout : one JSON object {"ok": bool, "message"|"error": "..."}
16
- Exit code is always 0 (the result is the JSON on stdout); any crash is caught
17
- and reported as {"ok": false, "error": ...}.
18
- """
19
- import sys
20
- import json
21
- import base64
22
- import hashlib
23
- import urllib.parse
24
-
25
-
26
- def _out(ok, text):
27
- key = "message" if ok else "error"
28
- sys.stdout.write(json.dumps({"ok": bool(ok), key: str(text)[:300]}))
29
- sys.stdout.flush()
30
- sys.exit(0)
31
-
32
-
33
- def _pick(creds, *keys):
34
- for k in keys:
35
- v = (creds.get(k) or "").strip()
36
- if v:
37
- return v
38
- return ""
39
-
40
-
41
- # ── PostgreSQL (asyncpg) ─────────────────────────────────────────────────────
42
- def probe_postgres(creds):
43
- dsn = _pick(creds, "dsn", "POSTGRES_DSN")
44
- if not dsn:
45
- _out(False, "Connection DSN required")
46
- try:
47
- import asyncio
48
- import asyncpg # type: ignore
49
- except ImportError:
50
- _out(False, "asyncpg not installed on this runner (pip install asyncpg)")
51
-
52
- async def _run():
53
- conn = await asyncpg.connect(dsn, timeout=10)
54
- try:
55
- await conn.fetchval("SELECT 1")
56
- ver = await conn.fetchval("SELECT version()")
57
- finally:
58
- await conn.close()
59
- return ver or ""
60
-
61
- try:
62
- ver = asyncio.run(asyncio.wait_for(_run(), timeout=15))
63
- short = str(ver).split(" on ")[0] if ver else ""
64
- _out(True, f"Connected to PostgreSQL{(' ' + short) if short else ''}")
65
- except Exception as e: # noqa: BLE001
66
- _out(False, f"PostgreSQL connection failed: {e}")
67
-
68
-
69
- # ── MySQL / MariaDB (PyMySQL pure-python, sync) ────────────────────────────
70
- def probe_mysql(creds):
71
- dsn = _pick(creds, "dsn", "MYSQL_DSN")
72
- if not dsn:
73
- _out(False, "Connection DSN required")
74
- try:
75
- import pymysql # type: ignore
76
- except ImportError:
77
- _out(False, "PyMySQL not installed on this runner (pip install PyMySQL)")
78
- try:
79
- p = urllib.parse.urlparse(dsn)
80
- conn = pymysql.connect(
81
- host=p.hostname,
82
- port=p.port or 3306,
83
- user=urllib.parse.unquote(p.username or ""),
84
- password=urllib.parse.unquote(p.password or ""),
85
- db=(p.path or "").lstrip("/") or None,
86
- connect_timeout=10,
87
- read_timeout=10,
88
- )
89
- try:
90
- with conn.cursor() as cur:
91
- cur.execute("SELECT VERSION()")
92
- row = cur.fetchone()
93
- ver = (row[0] if row else "") or ""
94
- finally:
95
- conn.close()
96
- _out(True, f"Connected to MySQL{(' — ' + str(ver)) if ver else ''}")
97
- except Exception as e: # noqa: BLE001
98
- _out(False, f"MySQL connection failed: {e}")
99
-
100
-
101
- # ── Snowflake (key-pair JWT, mirrors the server handler) ─────────────────────
102
- def probe_snowflake(creds):
103
- account = _pick(creds, "account", "SNOWFLAKE_ACCOUNT")
104
- user = _pick(creds, "user", "SNOWFLAKE_USER")
105
- pk = _pick(creds, "private_key", "SNOWFLAKE_PRIVATE_KEY")
106
- if not account or not user or not pk:
107
- _out(False, "Account + Username + Private Key required")
108
- try:
109
- from cryptography.hazmat.primitives import serialization # type: ignore
110
- import requests # type: ignore
111
- except ImportError:
112
- _out(False, "cryptography/requests not installed on this runner")
113
-
114
- def b64url(b):
115
- if isinstance(b, str):
116
- b = b.encode()
117
- return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
118
-
119
- try:
120
- import time
121
- key = serialization.load_pem_private_key(pk.encode(), password=None)
122
- der_spki = key.public_key().public_bytes(
123
- serialization.Encoding.DER,
124
- serialization.PublicFormat.SubjectPublicKeyInfo,
125
- )
126
- fp = "SHA256:" + base64.b64encode(hashlib.sha256(der_spki).digest()).decode()
127
- acct_upper = account.upper()
128
- if "." in acct_upper:
129
- acct_upper = acct_upper.split(".")[0]
130
- qual = f"{acct_upper}.{user.upper()}"
131
- now = int(time.time())
132
- header = b64url(json.dumps({"alg": "RS256", "typ": "JWT"}))
133
- payload = b64url(json.dumps(
134
- {"iss": f"{qual}.{fp}", "sub": qual, "iat": now, "exp": now + 3540}))
135
- signing_input = f"{header}.{payload}".encode()
136
- from cryptography.hazmat.primitives import hashes # type: ignore
137
- from cryptography.hazmat.primitives.asymmetric import padding # type: ignore
138
- sig = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
139
- jwt = f"{header}.{payload}.{b64url(sig)}"
140
- host = f"https://{account.replace('_', '-').lower()}.snowflakecomputing.com"
141
- r = requests.post(
142
- f"{host}/api/v2/statements",
143
- headers={
144
- "Authorization": f"Bearer {jwt}",
145
- "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT",
146
- "Content-Type": "application/json",
147
- "Accept": "application/json",
148
- },
149
- json={"statement": "SELECT CURRENT_VERSION()", "timeout": 20},
150
- timeout=15,
151
- )
152
- if r.ok:
153
- v = ""
154
- try:
155
- v = (r.json().get("data") or [[None]])[0][0] or ""
156
- except Exception: # noqa: BLE001
157
- pass
158
- _out(True, f"Connected to Snowflake{(' (v' + str(v) + ')') if v else ''}")
159
- _out(False, f"Snowflake auth/query failed: HTTP {r.status_code} {r.text[:160]}")
160
- except Exception as e: # noqa: BLE001
161
- _out(False, f"Snowflake connection failed: {e}")
162
-
163
-
164
- # ── Databricks (PAT or OAuth M2M, mirrors the server handler) ────────────────
165
- def probe_databricks(creds):
166
- host = _pick(creds, "workspace_url", "DATABRICKS_HOST").rstrip("/")
167
- pat = _pick(creds, "token", "DATABRICKS_TOKEN")
168
- cid = _pick(creds, "client_id", "DATABRICKS_CLIENT_ID")
169
- sec = _pick(creds, "client_secret", "DATABRICKS_CLIENT_SECRET")
170
- if not host:
171
- _out(False, "Workspace URL required")
172
- try:
173
- import requests # type: ignore
174
- except ImportError:
175
- _out(False, "requests not installed on this runner")
176
- try:
177
- bearer = pat
178
- if not bearer:
179
- if not cid or not sec:
180
- _out(False, "Provide a Personal Access Token, or Client ID + Client Secret")
181
- tr = requests.post(
182
- f"{host}/oidc/v1/token",
183
- auth=(cid, sec),
184
- data={"grant_type": "client_credentials", "scope": "all-apis"},
185
- timeout=12,
186
- )
187
- tj = {}
188
- try:
189
- tj = tr.json()
190
- except Exception: # noqa: BLE001
191
- pass
192
- if not tr.ok or not tj.get("access_token"):
193
- _out(False, f"Databricks OAuth failed: {tj.get('error_description') or tj.get('error') or ('HTTP ' + str(tr.status_code))}")
194
- bearer = tj["access_token"]
195
- r = requests.get(
196
- f"{host}/api/2.1/clusters/list?page_size=1",
197
- headers={"Authorization": f"Bearer {bearer}"},
198
- timeout=12,
199
- )
200
- if r.ok:
201
- _out(True, f"Connected to Databricks ({host.replace('https://', '').replace('http://', '')})")
202
- _out(False, f"Databricks unreachable: HTTP {r.status_code} {r.text[:120]}")
203
- except Exception as e: # noqa: BLE001
204
- _out(False, f"Databricks connection failed: {e}")
205
-
206
-
207
- # ── SQLite (local file on the runner) ────────────────────────────────────────
208
- def probe_sqlite(creds):
209
- import os
210
- import sqlite3
211
- path = _pick(creds, "dsn", "db_path", "path")
212
- if not path:
213
- _out(False, "Database file path required")
214
- if not os.path.exists(path):
215
- _out(False, f"No SQLite file at {path}")
216
- try:
217
- conn = sqlite3.connect(path, timeout=8)
218
- try:
219
- conn.execute("SELECT 1")
220
- finally:
221
- conn.close()
222
- _out(True, f"Opened SQLite database ({os.path.basename(path)})")
223
- except Exception as e: # noqa: BLE001
224
- _out(False, f"SQLite open failed: {e}")
225
-
226
-
227
- _PROBES = {
228
- "postgres": probe_postgres,
229
- "mysql": probe_mysql,
230
- "snowflake": probe_snowflake,
231
- "databricks": probe_databricks,
232
- "sqlite": probe_sqlite,
233
- }
234
-
235
-
236
- def main():
237
- try:
238
- job = json.loads(sys.stdin.read() or "{}")
239
- except Exception as e: # noqa: BLE001
240
- _out(False, f"bad probe job: {e}")
241
- return
242
- service = (job.get("service") or "").strip()
243
- creds = job.get("creds") or {}
244
- fn = _PROBES.get(service)
245
- if not fn:
246
- _out(False, f"unsupported service: {service}")
247
- return
248
- fn(creds)
249
-
250
-
251
- if __name__ == "__main__":
252
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ localDbProbe.py - one-shot, read-only database reachability probe run BY THE
4
+ RUNNER on behalf of the Melaya server.
5
+
6
+ Why it exists: some databases sit behind an IP allow-list or inside a VPC that
7
+ the Melaya cloud can never reach, but the user's own runner (on their LAN /
8
+ bastion) can. The server hands us the resolved credentials over the already-
9
+ authenticated Socket.IO channel; we open a single real connection from THIS
10
+ machine's network vantage point, run a trivial read-only probe, and print the
11
+ outcome. We never persist the credentials and never write to the database.
12
+
13
+ Contract:
14
+ stdin : one JSON object {"service": "...", "creds": { ... }}
15
+ stdout : one JSON object {"ok": bool, "message"|"error": "..."}
16
+ Exit code is always 0 (the result is the JSON on stdout); any crash is caught
17
+ and reported as {"ok": false, "error": ...}.
18
+ """
19
+ import sys
20
+ import json
21
+ import base64
22
+ import hashlib
23
+ import urllib.parse
24
+
25
+
26
+ def _out(ok, text):
27
+ key = "message" if ok else "error"
28
+ sys.stdout.write(json.dumps({"ok": bool(ok), key: str(text)[:300]}))
29
+ sys.stdout.flush()
30
+ sys.exit(0)
31
+
32
+
33
+ def _pick(creds, *keys):
34
+ for k in keys:
35
+ v = (creds.get(k) or "").strip()
36
+ if v:
37
+ return v
38
+ return ""
39
+
40
+
41
+ _PLACEHOLDER_HOSTS = {"host", "hostname", "your-host", "your_host", "<host>", "myhost", "dbhost", "example.com"}
42
+
43
+
44
+ def _friendly_conn_error(e, host, port, engine):
45
+ """Turn a raw driver/socket exception into an actionable message. The most
46
+ common failure is a copy-pasted EXAMPLE DSN (host == 'host') or a Docker/VPC
47
+ hostname that doesn't resolve from the runner's machine."""
48
+ import socket
49
+ msg = str(e) or e.__class__.__name__
50
+ low = msg.lower()
51
+ host = host or ""
52
+ is_dns = isinstance(e, socket.gaierror) or "nodename nor servname" in low \
53
+ or "name or service not known" in low or "getaddrinfo" in low \
54
+ or "name does not resolve" in low or "no address associated" in low
55
+ if is_dns:
56
+ if host.lower() in _PLACEHOLDER_HOSTS:
57
+ return (f"the host is literally \"{host}\" - that's the EXAMPLE DSN, not your database. "
58
+ f"Paste your real connection string with the actual host, e.g. an AWS RDS / Cloud SQL "
59
+ f"endpoint: {engine.lower()}://user:pass@your-db.abc123.us-east-1.rds.amazonaws.com:{port}/db.")
60
+ if host and "." not in host and host not in ("localhost",):
61
+ return (f"can't resolve host \"{host}\" from the runner's machine. A bare name like this is "
62
+ f"usually a Docker/compose service or a VPC-internal alias that only resolves elsewhere - "
63
+ f"use the host that's reachable from this machine (e.g. your RDS/Cloud SQL endpoint, or a VPN/bastion host).")
64
+ return (f"can't resolve host \"{host}\" from the runner's machine - check the host in your DSN, and that "
65
+ f"this machine can reach it (DNS + network route; for a private RDS/VPC DB the runner must sit in the VPC or on an allow-listed IP).")
66
+ if isinstance(e, ConnectionRefusedError) or "refused" in low:
67
+ return f"host \"{host}\" reachable but port {port} refused the connection - is {engine} listening there / bound to that interface?"
68
+ if isinstance(e, (TimeoutError,)) or "timed out" in low or "timeout" in low:
69
+ return f"timed out connecting to {host}:{port} - firewall, IP allow-list, or wrong host/port?"
70
+ if "password authentication failed" in low or "authentication failed" in low or "access denied" in low:
71
+ return "authentication failed - check the username/password in the DSN."
72
+ if 'database "' in low and "does not exist" in low or "unknown database" in low:
73
+ return f"connected, but the database name in the DSN doesn't exist: {msg}"
74
+ return msg
75
+
76
+
77
+ # ── PostgreSQL (asyncpg) ─────────────────────────────────────────────────────
78
+ def probe_postgres(creds):
79
+ dsn = _pick(creds, "dsn", "POSTGRES_DSN")
80
+ if not dsn:
81
+ _out(False, "Connection DSN required")
82
+ try:
83
+ import asyncio
84
+ import asyncpg # type: ignore
85
+ except ImportError:
86
+ _out(False, "asyncpg not installed on this runner (pip install asyncpg)")
87
+ p = urllib.parse.urlparse(dsn)
88
+ host, port = (p.hostname or ""), (p.port or 5432)
89
+ if not host:
90
+ _out(False, "PostgreSQL connection failed: no host in the DSN - expected postgresql://user:pass@HOST:5432/db")
91
+
92
+ async def _run():
93
+ conn = await asyncpg.connect(dsn, timeout=10)
94
+ try:
95
+ await conn.fetchval("SELECT 1")
96
+ ver = await conn.fetchval("SELECT version()")
97
+ finally:
98
+ await conn.close()
99
+ return ver or ""
100
+
101
+ try:
102
+ ver = asyncio.run(asyncio.wait_for(_run(), timeout=15))
103
+ short = str(ver).split(" on ")[0] if ver else ""
104
+ _out(True, f"Connected to PostgreSQL{(' - ' + short) if short else ''}")
105
+ except Exception as e: # noqa: BLE001
106
+ _out(False, f"PostgreSQL connection failed: {_friendly_conn_error(e, host, port, 'PostgreSQL')}")
107
+
108
+
109
+ # ── MySQL / MariaDB (PyMySQL - pure-python, sync) ────────────────────────────
110
+ def probe_mysql(creds):
111
+ dsn = _pick(creds, "dsn", "MYSQL_DSN")
112
+ if not dsn:
113
+ _out(False, "Connection DSN required")
114
+ try:
115
+ import pymysql # type: ignore
116
+ except ImportError:
117
+ _out(False, "PyMySQL not installed on this runner (pip install PyMySQL)")
118
+ p = urllib.parse.urlparse(dsn)
119
+ host, port = (p.hostname or ""), (p.port or 3306)
120
+ if not host:
121
+ _out(False, "MySQL connection failed: no host in the DSN - expected mysql://user:pass@HOST:3306/db")
122
+ try:
123
+ conn = pymysql.connect(
124
+ host=host,
125
+ port=port,
126
+ user=urllib.parse.unquote(p.username or ""),
127
+ password=urllib.parse.unquote(p.password or ""),
128
+ db=(p.path or "").lstrip("/") or None,
129
+ connect_timeout=10,
130
+ read_timeout=10,
131
+ )
132
+ try:
133
+ with conn.cursor() as cur:
134
+ cur.execute("SELECT VERSION()")
135
+ row = cur.fetchone()
136
+ ver = (row[0] if row else "") or ""
137
+ finally:
138
+ conn.close()
139
+ _out(True, f"Connected to MySQL{(' - ' + str(ver)) if ver else ''}")
140
+ except Exception as e: # noqa: BLE001
141
+ _out(False, f"MySQL connection failed: {_friendly_conn_error(e, host, port, 'MySQL')}")
142
+
143
+
144
+ # ── Snowflake (key-pair JWT, mirrors the server handler) ─────────────────────
145
+ def probe_snowflake(creds):
146
+ account = _pick(creds, "account", "SNOWFLAKE_ACCOUNT")
147
+ user = _pick(creds, "user", "SNOWFLAKE_USER")
148
+ pk = _pick(creds, "private_key", "SNOWFLAKE_PRIVATE_KEY")
149
+ if not account or not user or not pk:
150
+ _out(False, "Account + Username + Private Key required")
151
+ try:
152
+ from cryptography.hazmat.primitives import serialization # type: ignore
153
+ import requests # type: ignore
154
+ except ImportError:
155
+ _out(False, "cryptography/requests not installed on this runner")
156
+
157
+ def b64url(b):
158
+ if isinstance(b, str):
159
+ b = b.encode()
160
+ return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
161
+
162
+ try:
163
+ import time
164
+ key = serialization.load_pem_private_key(pk.encode(), password=None)
165
+ der_spki = key.public_key().public_bytes(
166
+ serialization.Encoding.DER,
167
+ serialization.PublicFormat.SubjectPublicKeyInfo,
168
+ )
169
+ fp = "SHA256:" + base64.b64encode(hashlib.sha256(der_spki).digest()).decode()
170
+ acct_upper = account.upper()
171
+ if "." in acct_upper:
172
+ acct_upper = acct_upper.split(".")[0]
173
+ qual = f"{acct_upper}.{user.upper()}"
174
+ now = int(time.time())
175
+ header = b64url(json.dumps({"alg": "RS256", "typ": "JWT"}))
176
+ payload = b64url(json.dumps(
177
+ {"iss": f"{qual}.{fp}", "sub": qual, "iat": now, "exp": now + 3540}))
178
+ signing_input = f"{header}.{payload}".encode()
179
+ from cryptography.hazmat.primitives import hashes # type: ignore
180
+ from cryptography.hazmat.primitives.asymmetric import padding # type: ignore
181
+ sig = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
182
+ jwt = f"{header}.{payload}.{b64url(sig)}"
183
+ host = f"https://{account.replace('_', '-').lower()}.snowflakecomputing.com"
184
+ r = requests.post(
185
+ f"{host}/api/v2/statements",
186
+ headers={
187
+ "Authorization": f"Bearer {jwt}",
188
+ "X-Snowflake-Authorization-Token-Type": "KEYPAIR_JWT",
189
+ "Content-Type": "application/json",
190
+ "Accept": "application/json",
191
+ },
192
+ json={"statement": "SELECT CURRENT_VERSION()", "timeout": 20},
193
+ timeout=15,
194
+ )
195
+ if r.ok:
196
+ v = ""
197
+ try:
198
+ v = (r.json().get("data") or [[None]])[0][0] or ""
199
+ except Exception: # noqa: BLE001
200
+ pass
201
+ _out(True, f"Connected to Snowflake{(' (v' + str(v) + ')') if v else ''}")
202
+ _out(False, f"Snowflake auth/query failed: HTTP {r.status_code} {r.text[:160]}")
203
+ except Exception as e: # noqa: BLE001
204
+ _out(False, f"Snowflake connection failed: {e}")
205
+
206
+
207
+ # ── Databricks (PAT or OAuth M2M, mirrors the server handler) ────────────────
208
+ def probe_databricks(creds):
209
+ host = _pick(creds, "workspace_url", "DATABRICKS_HOST").rstrip("/")
210
+ pat = _pick(creds, "token", "DATABRICKS_TOKEN")
211
+ cid = _pick(creds, "client_id", "DATABRICKS_CLIENT_ID")
212
+ sec = _pick(creds, "client_secret", "DATABRICKS_CLIENT_SECRET")
213
+ if not host:
214
+ _out(False, "Workspace URL required")
215
+ try:
216
+ import requests # type: ignore
217
+ except ImportError:
218
+ _out(False, "requests not installed on this runner")
219
+ try:
220
+ bearer = pat
221
+ if not bearer:
222
+ if not cid or not sec:
223
+ _out(False, "Provide a Personal Access Token, or Client ID + Client Secret")
224
+ tr = requests.post(
225
+ f"{host}/oidc/v1/token",
226
+ auth=(cid, sec),
227
+ data={"grant_type": "client_credentials", "scope": "all-apis"},
228
+ timeout=12,
229
+ )
230
+ tj = {}
231
+ try:
232
+ tj = tr.json()
233
+ except Exception: # noqa: BLE001
234
+ pass
235
+ if not tr.ok or not tj.get("access_token"):
236
+ _out(False, f"Databricks OAuth failed: {tj.get('error_description') or tj.get('error') or ('HTTP ' + str(tr.status_code))}")
237
+ bearer = tj["access_token"]
238
+ r = requests.get(
239
+ f"{host}/api/2.1/clusters/list?page_size=1",
240
+ headers={"Authorization": f"Bearer {bearer}"},
241
+ timeout=12,
242
+ )
243
+ if r.ok:
244
+ _out(True, f"Connected to Databricks ({host.replace('https://', '').replace('http://', '')})")
245
+ _out(False, f"Databricks unreachable: HTTP {r.status_code} {r.text[:120]}")
246
+ except Exception as e: # noqa: BLE001
247
+ _out(False, f"Databricks connection failed: {e}")
248
+
249
+
250
+ # ── SQLite (local file on the runner) ────────────────────────────────────────
251
+ def probe_sqlite(creds):
252
+ import os
253
+ import sqlite3
254
+ path = _pick(creds, "dsn", "db_path", "path")
255
+ if not path:
256
+ _out(False, "Database file path required")
257
+ if not os.path.exists(path):
258
+ _out(False, f"No SQLite file at {path}")
259
+ try:
260
+ conn = sqlite3.connect(path, timeout=8)
261
+ try:
262
+ conn.execute("SELECT 1")
263
+ finally:
264
+ conn.close()
265
+ _out(True, f"Opened SQLite database ({os.path.basename(path)})")
266
+ except Exception as e: # noqa: BLE001
267
+ _out(False, f"SQLite open failed: {e}")
268
+
269
+
270
+ _PROBES = {
271
+ "postgres": probe_postgres,
272
+ "mysql": probe_mysql,
273
+ "snowflake": probe_snowflake,
274
+ "databricks": probe_databricks,
275
+ "sqlite": probe_sqlite,
276
+ }
277
+
278
+
279
+ def main():
280
+ try:
281
+ job = json.loads(sys.stdin.read() or "{}")
282
+ except Exception as e: # noqa: BLE001
283
+ _out(False, f"bad probe job: {e}")
284
+ return
285
+ service = (job.get("service") or "").strip()
286
+ creds = job.get("creds") or {}
287
+ fn = _PROBES.get(service)
288
+ if not fn:
289
+ _out(False, f"unsupported service: {service}")
290
+ return
291
+ fn(creds)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.41",
3
+ "version": "1.1.44",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,