@melaya/runner 1.0.47 → 1.0.49

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.
@@ -229,11 +229,24 @@ export async function connect(opts) {
229
229
  if (k.startsWith("MEL_MODEL_"))
230
230
  delete inherited[k];
231
231
  }
232
+ // certifi CA-bundle path resolved at venv setup. Set as both
233
+ // SSL_CERT_FILE (urllib / openssl) and REQUESTS_CA_BUNDLE (requests
234
+ // library) so EVERY HTTPS-using tool — gmail_send, slack_post_text,
235
+ // anthropic SDK, openai SDK, web_search, etc. — verifies against
236
+ // certifi's bundle rather than the platform default. Without this,
237
+ // macOS Python crashes with `[SSL: CERTIFICATE_VERIFY_FAILED]` on
238
+ // any cloud call (run eb1b8f67df56491b hit exactly this for
239
+ // gmail_send → api.googleapis.com).
240
+ const certBundle = (await import("./pythonEnv.js")).getCertBundlePath();
241
+ const sslEnv = certBundle
242
+ ? { SSL_CERT_FILE: certBundle, REQUESTS_CA_BUNDLE: certBundle }
243
+ : {};
232
244
  const env = {
233
245
  ...inherited,
234
246
  PYTHONPATH: sharedDir,
235
247
  PYTHONIOENCODING: "utf-8",
236
248
  PYTHONUTF8: "1",
249
+ ...sslEnv,
237
250
  MEL_RUN_ID: payload.runId,
238
251
  // Point agentscope hooks at the REAL server (not the browser's Vite URL).
239
252
  // Convert wss://api.melaya.org → https://api.melaya.org so pushMessage,
@@ -542,6 +555,13 @@ export async function connect(opts) {
542
555
  ];
543
556
  const { existsSync: _exists2 } = await import("fs");
544
557
  const bundledNltkData = candidateNltkPaths.find((p) => _exists2(p)) || "";
558
+ // Resolve certifi bundle for HTTPS verification (same rationale as
559
+ // the runner:run spawn — embedder providers like OpenAI hit
560
+ // api.openai.com over TLS and crash on macOS without certifi).
561
+ const ragCertBundle = (await import("./pythonEnv.js")).getCertBundlePath();
562
+ const ragSslEnv = ragCertBundle
563
+ ? { SSL_CERT_FILE: ragCertBundle, REQUESTS_CA_BUNDLE: ragCertBundle }
564
+ : {};
545
565
  const proc = spawn(env.pythonPath, [
546
566
  "-u", stagedScript,
547
567
  "--pipeline-name", payload.pipelineName,
@@ -552,6 +572,7 @@ export async function connect(opts) {
552
572
  env: {
553
573
  ...process.env,
554
574
  PYTHONPATH: sharedDir, // so `from agentscope.rag import …` resolves
575
+ ...ragSslEnv,
555
576
  ...(bundledNltkData ? { NLTK_DATA: bundledNltkData } : {}),
556
577
  },
557
578
  cwd: workDir,
@@ -68,7 +68,17 @@ const FAMILY_OVERRIDES = [
68
68
  { pattern: /qwen3.*vl.*\b8b\b/i, family: "qwen3", tier: "agentic-capable", thinking: "on", reason: "qwen3-vl-8b: agentic-capable, full thinking" },
69
69
  // ── Qwen3 — capable (≥8B) BEFORE the smaller non-vl 4b pattern, so
70
70
  // 14b / 32b don't get caught by the 4b regex's stem match. ──
71
- { pattern: /qwen3.*\b(8b|14b|32b)\b/i, family: "qwen3", tier: "agentic-capable", thinking: "on", reason: "qwen3 ≥8b: agentic-capable" },
71
+ // Mainline qwen3 releases ship 8B, 14B, 32B. Custom/finetuned
72
+ // 27B/30B/72B variants exist (e.g. qwen3.6-27b on LM Studio) — we
73
+ // enumerate them so `family="qwen3"` propagates and model.py can
74
+ // apply chat_template_kwargs.enable_thinking=false when needed.
75
+ // Without the explicit 27b entry, qwen3.6-27b falls through to
76
+ // the param-count path with `family=null`, the disable_thinking
77
+ // branch in model.py is skipped, and the model produces
78
+ // thinking-only responses that the OpenAI formatter drops (run
79
+ // 1ecad7cb hit this — Copywriter ended in 11.5KB thinking block,
80
+ // ReportWriter emailed [CONTENT NOT AVAILABLE]).
81
+ { pattern: /qwen3.*\b(8b|14b|27b|30b|32b|72b)\b/i, family: "qwen3", tier: "agentic-capable", thinking: "off", reason: "qwen3 ≥8b: agentic-capable, thinking disabled (model is large enough to reason without explicit <think> blocks; thinking-only responses get stripped by the OpenAI formatter and lose data to next agent)" },
72
82
  // ── Qwen3 — small variants ──
73
83
  { pattern: /qwen3.*\b1\.7b\b/i, family: "qwen3", tier: "text-only", thinking: "off", reason: "qwen3-1.7b: param count below the agentic 7B floor; thinking disabled to prevent <think> loops" },
74
84
  { pattern: /qwen3.*\b0\.5b\b/i, family: "qwen3", tier: "text-only", thinking: "off", reason: "qwen3-0.5b: too small for agentic work" },
@@ -214,7 +224,7 @@ export function classifyModel(id, lmstudioMeta) {
214
224
  // Bump whenever FAMILY_OVERRIDES, tier thresholds, or any classifier
215
225
  // logic changes in a way that could re-tier an existing cached model.
216
226
  // Cached profiles with a mismatched version are ignored and re-classified.
217
- const PROFILE_SCHEMA_VERSION = 2;
227
+ const PROFILE_SCHEMA_VERSION = 3;
218
228
  const LMSTUDIO_BASE = process.env.LMSTUDIO_BASE_URL || "http://127.0.0.1:1234";
219
229
  const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434";
220
230
  // JIT-load timeout. 8B-class quantised models on M-series Macs typically
@@ -10,6 +10,7 @@
10
10
  * shared-bundle version. Re-bootstraps when the bundle version changes.
11
11
  */
12
12
  export declare function venvPython(): string;
13
+ export declare function getCertBundlePath(): string;
13
14
  export declare function ensurePythonEnv(systemPython: string, expectedVersion: string, onProgress?: (msg: string) => void): Promise<{
14
15
  ok: boolean;
15
16
  pythonPath: string;
package/dist/pythonEnv.js CHANGED
@@ -119,12 +119,69 @@ const PIP_DEPS = [
119
119
  // "pandas is not installed". Numpy is already in PIP_DEPS (line 57) so
120
120
  // the wheel install is fast.
121
121
  "pandas",
122
+ // certifi — provides the CA root bundle that tools like gmail_send /
123
+ // any other HTTPS API rely on for cert verification. On macOS Python
124
+ // 3.x doesn't read the system keychain by default; without certifi +
125
+ // SSL_CERT_FILE pointed at its bundle, every Gmail / Slack / Anthropic
126
+ // / etc. API call raises SSL: CERTIFICATE_VERIFY_FAILED (run
127
+ // eb1b8f67df56491b hit exactly this — gmail_send dispatched but
128
+ // urllib couldn't verify api.googleapis.com). The runner exports
129
+ // SSL_CERT_FILE + REQUESTS_CA_BUNDLE pointing here when spawning
130
+ // pipeline Python — see getCertBundlePath() + spawn env merging in
131
+ // connection.ts.
132
+ "certifi",
122
133
  ];
123
134
  export function venvPython() {
124
135
  return platform() === "win32"
125
136
  ? join(VENV_DIR, "Scripts", "python.exe")
126
137
  : join(VENV_DIR, "bin", "python");
127
138
  }
139
+ // ── certifi CA-bundle path resolution + cache ────────────────────────────
140
+ // Set as SSL_CERT_FILE + REQUESTS_CA_BUNDLE when spawning pipeline Python
141
+ // so that ANY HTTPS-using tool (gmail_send, slack_post_text, anthropic /
142
+ // openai SDKs, requests, urllib, etc.) verifies certs against certifi's
143
+ // bundle rather than the platform default. On macOS specifically, the
144
+ // system Python doesn't read the keychain — without this, every API call
145
+ // crashes with `[SSL: CERTIFICATE_VERIFY_FAILED]`.
146
+ //
147
+ // We cache the path in ~/.melaya-runner/cert-bundle-path so we don't pay
148
+ // a Python startup roundtrip on every spawn. The cache is invalidated
149
+ // when the venv is rebuilt (the file lives outside VENV_DIR but is
150
+ // re-written every ensurePythonEnv).
151
+ const CERT_PATH_CACHE = join(CACHE_DIR, "cert-bundle-path");
152
+ export function getCertBundlePath() {
153
+ try {
154
+ if (existsSync(CERT_PATH_CACHE)) {
155
+ const cached = readFileSync(CERT_PATH_CACHE, "utf-8").trim();
156
+ if (cached && existsSync(cached))
157
+ return cached;
158
+ }
159
+ }
160
+ catch { /* fall through to resolution */ }
161
+ return "";
162
+ }
163
+ async function _resolveAndCacheCertBundle(onProgress) {
164
+ if (!existsSync(venvPython()))
165
+ return;
166
+ // Resolve certifi.where() — fast subprocess (<300 ms typical).
167
+ let resolved = "";
168
+ await runProc(venvPython(), ["-c", "import certifi, sys; sys.stdout.write(certifi.where())"], (line) => {
169
+ if (line && existsSync(line.trim()))
170
+ resolved = line.trim();
171
+ });
172
+ if (resolved) {
173
+ try {
174
+ writeFileSync(CERT_PATH_CACHE, resolved + "\n", "utf-8");
175
+ onProgress(`cert bundle: ${resolved}`);
176
+ }
177
+ catch (e) {
178
+ onProgress(`(warn: failed to cache cert bundle path: ${e?.message})`);
179
+ }
180
+ }
181
+ else {
182
+ onProgress("(warn: certifi.where() did not resolve a valid path — HTTPS calls may fail with cert verify errors)");
183
+ }
184
+ }
128
185
  // The marker is keyed on sharedVersion + a hash of PIP_DEPS so changes
129
186
  // to either invalidate the venv. Without the PIP_DEPS half, a runner
130
187
  // update that adds a dep (e.g. scrapling[fetchers] in 1.0.32) would
@@ -226,6 +283,7 @@ export async function ensurePythonEnv(systemPython, expectedVersion, onProgress
226
283
  // says we're up to date. This is what previously broke for boxes
227
284
  // that upgraded the runner without invalidating the marker.
228
285
  await ensureNltkData(onProgress);
286
+ await _resolveAndCacheCertBundle(onProgress);
229
287
  return { ok: true, pythonPath: venvPython() };
230
288
  }
231
289
  if (!existsSync(AGENTSCOPE)) {
@@ -298,6 +356,7 @@ export async function ensurePythonEnv(systemPython, expectedVersion, onProgress
298
356
  // Centralised in `ensureNltkData()` so the success/failure surface is
299
357
  // identical whether the venv is fresh or already valid.
300
358
  await ensureNltkData(onProgress);
359
+ await _resolveAndCacheCertBundle(onProgress);
301
360
  writeFileSync(VENV_MARK, venvMarkerValue(expectedVersion), "utf-8");
302
361
  // sanity: confirm shortuuid + agentscope (via PYTHONPATH) resolve now.
303
362
  // Probe must mirror the spawn env so PYTHONPATH=CACHE_DIR points at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.47",
3
+ "version": "1.0.49",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,