pinokiod 8.2.3 → 8.2.4

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/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # Production image for pinokiod with required system tooling
2
- FROM node:20 AS build
2
+ FROM node:24.22.1 AS build
3
3
  WORKDIR /app
4
4
 
5
5
  RUN apt-get update \
@@ -9,12 +9,13 @@ RUN apt-get update \
9
9
  COPY package*.json ./
10
10
  RUN npm ci --omit=dev && npm cache clean --force
11
11
 
12
- FROM node:20 AS runtime
12
+ FROM node:24.22.1 AS runtime
13
13
  ENV NODE_ENV=production
14
14
  WORKDIR /app
15
15
 
16
16
  RUN apt-get update \
17
17
  && apt-get install -y --no-install-recommends \
18
+ ca-certificates \
18
19
  bluez \
19
20
  curl \
20
21
  git \
@@ -187,8 +187,11 @@ class Anywhere {
187
187
  async autostart() {
188
188
  // a pid file left behind by a crash or reboot must never point at an unrelated process
189
189
  if (!(await this.daemonAlive())) fs.rmSync(this.pidPath(), { force: true })
190
+ if (!(await this.installed())) {
191
+ await this.stop()
192
+ return false
193
+ }
190
194
  if (!(await this.enabled())) return false
191
- if (!(await this.installed())) return false
192
195
  await this.start()
193
196
  return true
194
197
  }
@@ -199,6 +202,11 @@ class Anywhere {
199
202
  }
200
203
  async _start() {
201
204
  this.error = null
205
+ if (!(await this.installed())) {
206
+ await this.stop()
207
+ this.error = "Update the tailscaled tool before starting Pinokio Anywhere."
208
+ throw new Error(this.error)
209
+ }
202
210
  await this.loadConfig()
203
211
  await fs.promises.mkdir(this.stateDir(), { recursive: true })
204
212
  const bin = this.binPath()
package/kernel/bin/bun.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const semver = require("semver")
2
2
  const Util = require("../util")
3
3
 
4
- const BUN_VERSION = "1.3.11"
4
+ const BUN_VERSION = "1.4.4"
5
5
 
6
6
  class Bun {
7
7
  description = "Installs Bun, a fast JavaScript runtime and package manager."
@@ -2,6 +2,8 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const { execFile } = require('child_process')
4
4
  const { managedPythonEnv } = require('../python_env')
5
+ const semver = require('semver')
6
+ const { PIP_MIN_VERSION } = require('./conda-pins')
5
7
 
6
8
  async function buildCondaListFromMeta(minicondaPath, useCondaList) {
7
9
  if (!useCondaList) {
@@ -108,7 +110,20 @@ function resolveCondaBinary(minicondaPath, platform = process.platform, allowFal
108
110
  return allowFallback ? 'conda' : null
109
111
  }
110
112
 
113
+ async function hasSecureManagedPip(prefix, platform = process.platform) {
114
+ const python = path.join(prefix, platform === 'win32' ? 'python.exe' : 'bin/python')
115
+ return await new Promise(resolve => {
116
+ execFile(python, ['-I', '-c', 'from importlib.metadata import version; print(version("pip"))'], {
117
+ env: managedPythonEnv(), windowsHide: true, timeout: 15000,
118
+ }, (error, stdout) => {
119
+ const version = !error && semver.valid(String(stdout).trim())
120
+ resolve(!!version && semver.gte(version, PIP_MIN_VERSION))
121
+ })
122
+ })
123
+ }
124
+
111
125
  module.exports = {
112
126
  buildCondaListFromMeta,
113
127
  managedCondaRuns,
128
+ hasSecureManagedPip,
114
129
  }
@@ -1,6 +1,22 @@
1
1
  const semver = require('semver')
2
2
 
3
3
  const CONDA_PIN_VERSION = "26.5.3"
4
+ // Conda 26.9.4 has no Python 3.10 builds. Apply its CA-bundle requirements
5
+ // explicitly while retaining the managed Python version required by apps.
6
+ const TLS_PACKAGE_MINIMUMS = {
7
+ 'ca-certificates': '2026.10.19',
8
+ certifi: '2026.10.19',
9
+ }
10
+ // conda-forge's patched pip requires Python 3.11; this official wheel supports 3.10.
11
+ // Bootstrap URL and hash: https://discuss.python.org/t/109576
12
+ const PIP_MIN_VERSION = '26.2.2'
13
+ const PIP_INSTALL_SPEC = 'pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70'
14
+ const PIP_INSTALL_COMMAND = `python -I -m pip --isolated --disable-pip-version-check install --no-index --no-deps "${PIP_INSTALL_SPEC}"`
15
+ const TLS_PACKAGE_SPECS = Object.entries(TLS_PACKAGE_MINIMUMS).map(([name, version]) => `${name}>=${version}`)
16
+ const hasSecureTlsPackages = (versions = {}) => Object.entries(TLS_PACKAGE_MINIMUMS).every(([name, minimum]) => {
17
+ const version = semver.coerce(versions[name])
18
+ return version && semver.gte(version, minimum)
19
+ })
4
20
  const PYTHON_PIN_VERSION = "3.10.20"
5
21
  const PYTHON_INSTALL_SPEC = `python=${PYTHON_PIN_VERSION}`
6
22
  const WINDOWS_PYTHON_SSL_FIX_SPEC = `${PYTHON_INSTALL_SPEC}=*_1_cpython`
@@ -30,6 +46,12 @@ const isExpectedPythonPinned = (platform, version, build) => {
30
46
 
31
47
  module.exports = {
32
48
  CONDA_PIN_VERSION,
49
+ TLS_PACKAGE_MINIMUMS,
50
+ PIP_MIN_VERSION,
51
+ PIP_INSTALL_SPEC,
52
+ PIP_INSTALL_COMMAND,
53
+ TLS_PACKAGE_SPECS,
54
+ hasSecureTlsPackages,
33
55
  PYTHON_INSTALL_SPEC,
34
56
  WINDOWS_PYTHON_SSL_FIX_SPEC,
35
57
  isExpectedCondaPinned,
@@ -1,18 +1,37 @@
1
1
  const fs = require('fs')
2
2
  const { glob } = require('glob')
3
3
  const semver = require('semver')
4
+ const { createHash } = require('crypto')
5
+ const { execFile } = require('child_process')
6
+ const { managedPythonEnv } = require('../python_env')
4
7
  const { isPathRemovalBlockedError, removePath } = require('../path_removal')
5
- const { buildCondaListFromMeta, managedCondaRuns } = require('./conda-meta')
8
+ const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
6
9
  const {
7
10
  CONDA_PIN_VERSION,
11
+ PIP_INSTALL_COMMAND,
12
+ TLS_PACKAGE_MINIMUMS,
13
+ TLS_PACKAGE_SPECS,
14
+ hasSecureTlsPackages,
8
15
  PYTHON_INSTALL_SPEC,
9
16
  WINDOWS_PYTHON_SSL_FIX_SPEC,
10
17
  isExpectedCondaPinned,
11
18
  isExpectedPythonPinned,
12
19
  } = require('./conda-pins')
13
20
 
14
- const MINIFORGE_RELEASE = "26.3.2-3"
21
+ const MINIFORGE_RELEASE = "26.9.4-0"
15
22
  const MINIFORGE_BASE_URL = `https://github.com/conda-forge/miniforge/releases/download/${MINIFORGE_RELEASE}`
23
+ // GitHub's published SHA-256 asset digests for Miniforge 26.9.4-0.
24
+ const MINIFORGE_SHA256 = {
25
+ darwin: {
26
+ x64: '4bf8d162f90a9ae79fbe1c3b152a9957b6cf4e7385758fa21e0340229d444df1',
27
+ arm64: '85512f5495465644e09655d84bc1df79d978353abd9ef0d0a8f3df75da44c760',
28
+ },
29
+ linux: {
30
+ x64: '621428f40496a90702753b09237794518a081d9979d9f845a525de761b6056f7',
31
+ arm64: 'd265b10be348403a3bb8d475ff989beba48db9baa5fe7ee28eb38213538056bb',
32
+ },
33
+ win32: { x64: '43638698f5ee811debae27b0dc3ab5c0300129bd400a847376c4bd2f6be15ceb' },
34
+ }
16
35
  const CONDA_ROOT_DIR = "miniforge"
17
36
  const LEGACY_CONDA_ROOT_DIR = "miniconda"
18
37
 
@@ -44,11 +63,22 @@ class Conda {
44
63
  pinnedPackages() {
45
64
  return [
46
65
  `conda ==${CONDA_PIN_VERSION}`,
66
+ ...TLS_PACKAGE_SPECS,
47
67
  ].join("\n")
48
68
  }
49
69
  async hasCondaMeta() {
50
70
  return await this.kernel.exists(`bin/${CONDA_ROOT_DIR}/conda-meta`)
51
71
  }
72
+ async verifyInstaller(installer) {
73
+ const expected = MINIFORGE_SHA256[this.kernel.platform]?.[this.kernel.arch]
74
+ const hash = createHash('sha256')
75
+ for await (const chunk of fs.createReadStream(this.kernel.bin.path(installer))) {
76
+ hash.update(chunk)
77
+ }
78
+ if (!expected || hash.digest('hex') !== expected) {
79
+ throw new Error('Miniforge installer SHA-256 verification failed. The installer was not run and existing environments were preserved.')
80
+ }
81
+ }
52
82
  env() {
53
83
  let base = {
54
84
  CONDA_PREFIX: this.kernel.bin.path(CONDA_ROOT_DIR),
@@ -78,6 +108,7 @@ class Conda {
78
108
  channel_priority: flexible
79
109
  create_default_packages:
80
110
  - ${PYTHON_INSTALL_SPEC}
111
+ ${TLS_PACKAGE_SPECS.map(spec => ` - ${spec}`).join("\n")}
81
112
  envs_dirs:
82
113
  - ${this.kernel.bin.path(`${CONDA_ROOT_DIR}/envs`)}
83
114
  pkgs_dirs:
@@ -138,12 +169,37 @@ report_errors: false`)
138
169
  this.kernel.bin.installed.conda = conda
139
170
  this.kernel.bin.installed.conda_versions = conda_versions
140
171
  this.kernel.bin.installed.conda_builds = conda_builds
141
- if (!(conda_check.conda && conda_check.mamba && conda_check.python)) {
172
+ if (!(conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions))) {
142
173
  return false
143
174
  }
144
- return await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)
175
+ return await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform) && await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)
145
176
  }
146
177
  async install(req, ondata) {
178
+ await this.check()
179
+ const { conda_versions: versions, conda_builds: builds } = this.kernel.bin.installed
180
+ if (isExpectedCondaPinned(versions.conda) &&
181
+ isExpectedPythonPinned(this.kernel.platform, versions.python, builds.python) &&
182
+ await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)) {
183
+ // Refresh a working runtime in place, preserving named app environments.
184
+ await this.init()
185
+ const bootstrap = await this.kernel.bin.exec({
186
+ message: [PIP_INSTALL_COMMAND, `python -I -m pip --isolated install --no-deps --only-binary=:all: --index-url https://pypi.org/simple "certifi>=${TLS_PACKAGE_MINIMUMS.certifi}"`],
187
+ }, ondata)
188
+ if (bootstrap?.error?.length) throw new Error("Conda certificate bootstrap failed; existing environments were preserved.")
189
+ const python = this.kernel.bin.path(CONDA_ROOT_DIR, this.kernel.platform === 'win32' ? 'python.exe' : 'bin/python')
190
+ const output = await new Promise((resolve, reject) => execFile(python,
191
+ ['-I', '-c', 'import certifi; from importlib.metadata import version; print(certifi.where()); print(version("certifi"))'],
192
+ { env: managedPythonEnv(), windowsHide: true, timeout: 15000 },
193
+ (error, stdout) => error ? reject(error) : resolve(stdout.trim())))
194
+ const [caFile, certifiVersion] = output.split(/[\r\n]+/)
195
+ if (!semver.valid(certifiVersion) || !semver.gte(certifiVersion, TLS_PACKAGE_MINIMUMS.certifi)) throw new Error("The certificate bootstrap did not complete.")
196
+ const result = await this.kernel.bin.exec({
197
+ message: [`conda install -y --override-channels -c conda-forge "${this.kernel.platform === "win32" ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC}" "conda-libmamba-solver>=25.4.0" pip ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(" ")}`, PIP_INSTALL_COMMAND],
198
+ env: { CONDA_SSL_VERIFY: caFile },
199
+ }, ondata)
200
+ if (result?.error?.length || !(await this.check())) throw new Error("Conda certificate update failed; existing environments were preserved.")
201
+ return
202
+ }
147
203
  for(let i=0; i<5; i++) {
148
204
  await this._install(req, ondata)
149
205
  let installed = await this.check()
@@ -170,6 +226,7 @@ report_errors: false`)
170
226
 
171
227
  ondata({ raw: `downloading installer: ${installer_url}...\r\n` })
172
228
  await this.kernel.bin.download(installer_url, installer, ondata)
229
+ await this.verifyInstaller(installer)
173
230
 
174
231
  const caddy = this.kernel.bin.mod && this.kernel.bin.mod.caddy
175
232
  if (caddy && typeof caddy.running === "function" && await caddy.running()) {
@@ -227,13 +284,16 @@ report_errors: false`)
227
284
  `"conda=${CONDA_PIN_VERSION}"`,
228
285
  this.kernel.platform === "win32" ? `"${WINDOWS_PYTHON_SSL_FIX_SPEC}"` : `"${PYTHON_INSTALL_SPEC}"`,
229
286
  `"conda-libmamba-solver>=25.4.0"`,
287
+ "pip",
288
+ ...TLS_PACKAGE_SPECS.map(spec => `"${spec}"`),
230
289
  ]
231
290
 
232
291
  let cmds = [
233
292
  "conda clean -y --all",
234
293
  `conda install -y --override-channels -c conda-forge ${condaPackages.join(" ")} ${mods}`.trim(),
294
+ PIP_INSTALL_COMMAND,
235
295
  ]
236
- await this.kernel.bin.exec({
296
+ const result = await this.kernel.bin.exec({
237
297
  message: cmds,
238
298
  env: {
239
299
  PIP_REQUIRE_VIRTUALENV: "false"
@@ -241,6 +301,7 @@ report_errors: false`)
241
301
  }, (stream) => {
242
302
  ondata(stream)
243
303
  })
304
+ if (result?.error?.length) throw new Error("Conda package or pip installation failed.")
244
305
  if (this.kernel.platform === "win32") {
245
306
  // copy python.exe to python3.exe so you can run with both python3 and python
246
307
  await fs.promises.copyFile(
@@ -21,10 +21,11 @@ const LLVM = require('./llvm')
21
21
  const VS = require("./vs")
22
22
  const Cuda = require("./cuda")
23
23
  const Torch = require("./torch")
24
- const { buildCondaListFromMeta, managedCondaRuns } = require('./conda-meta')
24
+ const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
25
25
  const {
26
26
  isExpectedCondaPinned,
27
27
  isExpectedPythonPinned,
28
+ hasSecureTlsPackages,
28
29
  } = require('./conda-pins')
29
30
  const { glob } = require('glob')
30
31
  const fakeUa = require('fake-useragent');
@@ -412,8 +413,8 @@ class Bin {
412
413
  }
413
414
  }
414
415
 
415
- if (conda_check.conda && conda_check.mamba && conda_check.python) {
416
- this.correct_conda = await managedCondaRuns(this.kernel.bin.path("miniforge"), this.platform)
416
+ if (conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions)) {
417
+ this.correct_conda = await hasSecureManagedPip(this.kernel.bin.path("miniforge"), this.platform) && await managedCondaRuns(this.kernel.bin.path("miniforge"), this.platform)
417
418
  }
418
419
  }
419
420
  this.installed.conda = conda
@@ -2,7 +2,7 @@ const fs = require('fs')
2
2
  const fetch = require('cross-fetch')
3
3
  const { rimraf } = require('rimraf')
4
4
  const decompress = require('decompress');
5
- const NODE_VERSION = "24.18.0"
5
+ const NODE_VERSION = "24.22.1"
6
6
  const PNPM_VERSION = "11.9.0"
7
7
 
8
8
  const normalizeVersion = (version) => {
@@ -3,8 +3,8 @@ const semver = require('semver')
3
3
 
4
4
  // conda-forge ships only the daemon binary (no `tailscale` CLI); Pinokio
5
5
  // drives it over its LocalAPI (see kernel/anywhere).
6
- const TAILSCALED_SPEC = "tailscaled=1.104.1"
7
- const TAILSCALED_REQUIREMENT = ">=1.104.0" // 1.104 added --windows-mode=dev
6
+ const TAILSCALED_SPEC = "tailscaled=1.104.2"
7
+ const TAILSCALED_REQUIREMENT = ">=1.104.2" // Blocks the compromised eMudhra intermediate keys.
8
8
 
9
9
  class Tailscaled {
10
10
  description = "Installs the Tailscale daemon for Pinokio Anywhere: reach this Pinokio and its apps from your phone or another computer over a private network."
@@ -18,6 +18,7 @@ class Tailscaled {
18
18
  return this.kernel.bin.path("miniforge", "bin", "tailscaled")
19
19
  }
20
20
  async install(req, ondata) {
21
+ if (this.kernel.anywhere) await this.kernel.anywhere.stop()
21
22
  await this.kernel.bin.exec({
22
23
  message: [
23
24
  // a stale libmamba index cache reports the package as missing even
package/kernel/bin/uv.js CHANGED
@@ -1,4 +1,4 @@
1
- const UV_VERSION = "0.11.23"
1
+ const UV_VERSION = "0.13.4"
2
2
 
3
3
  const normalizeVersion = (version) => {
4
4
  const match = String(version || "").match(/^\d+\.\d+\.\d+/)
package/kernel/shell.js CHANGED
@@ -21,7 +21,7 @@ const ShellParser = require('./shell_parser')
21
21
  const AnsiStreamTracker = require('./ansi_stream_tracker')
22
22
  const ShellStateSync = require('./shell_state_sync')
23
23
  const ShellRunTemplate = require('./api/shell_run_template')
24
- const { PYTHON_INSTALL_SPEC } = require('./bin/conda-pins')
24
+ const { PYTHON_INSTALL_SPEC, PIP_INSTALL_COMMAND, PIP_MIN_VERSION } = require('./bin/conda-pins')
25
25
  const CondaRuntimeGuard = require('./shell_conda_runtime_guard')
26
26
  const home = os.homedir()
27
27
  const WINDOWS_CMD_CONDA_COMMAND_PATTERN = /(?:^|[&|()]\s*)@?\s*(?:call\s+)?(?:"[^"]*[\\/]conda(?:\.bat|\.exe)?"|(?:[a-z]:)?[^&|()\s"]*[\\/]conda(?:\.bat|\.exe)?|conda(?:\.bat|\.exe)?)\s+/i
@@ -1399,10 +1399,17 @@ class Shell {
1399
1399
  deactivate_path,
1400
1400
  // timeout,
1401
1401
  activate_command,
1402
+ this.isPowerShell()
1403
+ ? `${PIP_INSTALL_COMMAND}; if ($LASTEXITCODE -ne 0) { exit 1 }`
1404
+ : `${PIP_INSTALL_COMMAND} || exit 1`,
1402
1405
  // timeout,
1403
1406
  ]
1404
1407
  }
1405
1408
  }
1409
+ if (!use_uv) {
1410
+ const checkPip = `python -I -c "from importlib.metadata import distributions; assert all(tuple(map(int, d.version.split('.')[:3])) >= (${PIP_MIN_VERSION.split(".").join(", ")}) for d in distributions(name='pip')), 'Update pip in this app environment before running it.'"`
1411
+ venv_activation.push(this.isPowerShell() ? `${checkPip}; if ($LASTEXITCODE -ne 0) { exit 1 }` : `${checkPip} || exit 1`)
1412
+ }
1406
1413
  } else {
1407
1414
  venv_activation = []
1408
1415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.3",
3
+ "version": "8.2.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -59,17 +59,20 @@ test('anywhere preset requires tailscaled but not caddy', () => {
59
59
  assert.ok(!preset.conda_requirements.includes('caddy'))
60
60
  })
61
61
 
62
- test('tailscaled bin module pins 1.104.1 and checks the installed version', async () => {
62
+ test('tailscaled bin module pins 1.104.2 and checks the installed version', async () => {
63
63
  const mod = new Tailscaled()
64
- assert.equal(mod.cmd(), 'tailscaled=1.104.1')
64
+ assert.equal(mod.cmd(), 'tailscaled=1.104.2')
65
65
  mod.kernel = {
66
66
  platform: 'darwin',
67
67
  bin: {
68
- installed: { conda: new Set(['tailscaled']), conda_versions: { tailscaled: '1.104.1' } },
68
+ installed: { conda: new Set(['tailscaled']), conda_versions: { tailscaled: '1.104.2' } },
69
69
  path: (...segments) => segments.join('/'),
70
70
  },
71
71
  }
72
72
  assert.equal(await mod.installed(), true)
73
+ mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.1'
74
+ assert.equal(await mod.installed(), false)
75
+ mod.kernel.bin.installed.conda_versions.tailscaled = '1.104.2'
73
76
  assert.equal(mod.binPath(), 'miniforge/bin/tailscaled')
74
77
  mod.kernel.bin.installed.conda_versions.tailscaled = '1.98.5'
75
78
  assert.equal(await mod.installed(), false, 'older than 1.104 (no --windows-mode=dev) is not accepted')
@@ -441,3 +444,18 @@ test('sign-out resets the login state, and a link the server rejected is replace
441
444
  fs.rmSync(tmp, { recursive: true, force: true })
442
445
  }
443
446
  })
447
+
448
+
449
+ test('old Tailscale daemons are stopped and cannot be adopted through direct start', async t => {
450
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-anywhere-version-'))
451
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }))
452
+ const anywhere = new Anywhere(fakeKernel(root, { installed: false }))
453
+ let stopped = 0
454
+ anywhere.daemonAlive = async () => true
455
+ anywhere.enabled = async () => true
456
+ anywhere.stop = async () => { stopped++ }
457
+ assert.equal(await anywhere.autostart(), false)
458
+ assert.equal(stopped, 1)
459
+ await assert.rejects(anywhere.start(), /Update the tailscaled tool/)
460
+ assert.equal(stopped, 2)
461
+ })
@@ -6,16 +6,40 @@ const os = require('node:os')
6
6
  const path = require('node:path')
7
7
  const test = require('node:test')
8
8
 
9
- const Bin = require('../kernel/bin')
10
- const Conda = require('../kernel/bin/conda')
9
+ let Bin
10
+ let Conda
11
11
  const {
12
12
  CONDA_PIN_VERSION,
13
13
  PYTHON_INSTALL_SPEC,
14
14
  WINDOWS_PYTHON_SSL_FIX_SPEC,
15
15
  isExpectedCondaPinned,
16
16
  isExpectedPythonPinned,
17
+ TLS_PACKAGE_MINIMUMS,
18
+ TLS_PACKAGE_SPECS,
19
+ PIP_INSTALL_SPEC,
20
+ PIP_MIN_VERSION,
17
21
  } = require('../kernel/bin/conda-pins')
18
22
 
23
+ test.beforeEach(t => {
24
+ const realExecFile = childProcess.execFile.bind(childProcess)
25
+ t.mock.method(childProcess, 'execFile', (binary, args, options, done) => {
26
+ if (args[0] !== '-I' || args[1] !== '-c') return realExecFile(binary, args, options, done)
27
+ const prefix = path.basename(binary) === 'python.exe' ? path.dirname(binary) : path.dirname(path.dirname(binary))
28
+ if (args[2].includes('certifi.where()')) {
29
+ done(null, path.join(prefix, 'certificates.pem') + '\n2026.10.19\n')
30
+ return
31
+ }
32
+ fs.readFile(path.join(prefix, 'test-pip-version'), 'utf8').then(
33
+ version => done(null, version), error => done(error, '')
34
+ )
35
+ })
36
+ for (const file of ['../kernel/bin/conda-meta', '../kernel/bin/conda', '../kernel/bin']) {
37
+ delete require.cache[require.resolve(file)]
38
+ }
39
+ Bin = require('../kernel/bin')
40
+ Conda = require('../kernel/bin/conda')
41
+ })
42
+
19
43
  async function pathExists(target) {
20
44
  return fs.access(target).then(() => true).catch(() => false)
21
45
  }
@@ -49,6 +73,10 @@ async function writeHealthyCondaMeta(root, platform = 'win32', condaVersion = CO
49
73
  await writeCondaMeta(root, 'conda', condaVersion, 'py310')
50
74
  await writeCondaMeta(root, 'conda-libmamba-solver', '25.4.0', 'pyhd3eb1b0_0')
51
75
  await writeCondaMeta(root, 'python', '3.10.20', platform === 'win32' ? 'h4de0772_1_cpython' : 'cpython')
76
+ for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
77
+ await writeCondaMeta(root, name, version)
78
+ }
79
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
52
80
  }
53
81
 
54
82
  async function writeFakeManagedConda(root, platform = 'win32') {
@@ -87,6 +115,7 @@ function restoreEnv(name, value) {
87
115
  function createConda(kernel) {
88
116
  const conda = new Conda()
89
117
  conda.kernel = kernel
118
+ conda.verifyInstaller = async () => {} // Installer lifecycle tests mock the download.
90
119
  return conda
91
120
  }
92
121
 
@@ -161,7 +190,7 @@ test('Conda uses Miniforge assets and writes conda-forge-only config', async ()
161
190
 
162
191
  assert.equal(
163
192
  conda.urls.win32.x64,
164
- 'https://github.com/conda-forge/miniforge/releases/download/26.3.2-3/Miniforge3-Windows-x86_64.exe'
193
+ 'https://github.com/conda-forge/miniforge/releases/download/26.9.4-0/Miniforge3-Windows-x86_64.exe'
165
194
  )
166
195
 
167
196
  await conda.init()
@@ -201,7 +230,7 @@ test('Conda init writes the current Conda pin into an existing runtime', async (
201
230
 
202
231
  assert.equal(
203
232
  await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
204
- 'conda ==26.5.3'
233
+ ['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
205
234
  )
206
235
  })
207
236
 
@@ -253,13 +282,14 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
253
282
  assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
254
283
  const condaInstall = calls.find((call) => Array.isArray(call.message))
255
284
  assert.ok(condaInstall)
285
+ assert.equal(condaInstall.message[2], `python -I -m pip --isolated --disable-pip-version-check install --no-index --no-deps "${PIP_INSTALL_SPEC}"`)
256
286
  assert.equal(
257
287
  condaInstall.message[1],
258
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0"'
288
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
259
289
  )
260
290
  assert.equal(
261
291
  await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
262
- 'conda ==26.5.3'
292
+ ['conda ==26.5.3', ...TLS_PACKAGE_SPECS].join('\n')
263
293
  )
264
294
  })
265
295
 
@@ -287,7 +317,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
287
317
  assert.ok(condaInstall)
288
318
  assert.equal(
289
319
  condaInstall.message[1],
290
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0"'
320
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
291
321
  )
292
322
  })
293
323
 
@@ -319,7 +349,7 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
319
349
  assert.ok(condaInstall)
320
350
  assert.equal(
321
351
  condaInstall.message[1],
322
- 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" uv=0.11.23 nodejs=22.21.1 pnpm'
352
+ 'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" pip "ca-certificates>=2026.10.19" "certifi>=2026.10.19" uv=0.11.23 nodejs=22.21.1 pnpm'
323
353
  )
324
354
  })
325
355
 
@@ -583,3 +613,91 @@ test('Conda install reports recovery details when the old runtime cannot be remo
583
613
  fsModule.promises.rm = originalRm
584
614
  }
585
615
  })
616
+
617
+
618
+ test('stale certificate packages trigger the existing Conda setup checks', async t => {
619
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-certs-'))
620
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
621
+ await writeHealthyCondaMeta(root, 'darwin')
622
+ await writeFakeManagedConda(root, 'darwin')
623
+ for (const [name, version] of Object.entries(TLS_PACKAGE_MINIMUMS)) {
624
+ await fs.rm(path.join(root, 'bin/miniforge/conda-meta', `${name}-${version}.json`))
625
+ await writeCondaMeta(root, name, '2026.4.22')
626
+ }
627
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
628
+ const bin = createBin(root, 'darwin')
629
+ await bin.tryList()
630
+ assert.equal(bin.correct_conda, false)
631
+ })
632
+
633
+ test('tampered Miniforge installers leave the existing runtime untouched', async t => {
634
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-hash-'))
635
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
636
+ await createMiniforge(root)
637
+ const kernel = createKernel(root, 'darwin')
638
+ kernel.bin.download = async () => fs.writeFile(path.join(root, 'bin/installer.sh'), 'tampered')
639
+ const conda = new Conda()
640
+ conda.kernel = kernel
641
+ await assert.rejects(conda._install({}, () => {}), /SHA-256 verification failed/)
642
+ assert.equal(await pathExists(path.join(root, 'bin/miniforge/Scripts/conda.exe')), true)
643
+ })
644
+
645
+
646
+ test('old pip cannot pass Conda readiness even with current CA packages', async t => {
647
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-ready-'))
648
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
649
+ await writeHealthyCondaMeta(root, 'darwin')
650
+ await writeFakeManagedConda(root, 'darwin')
651
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), '26.2.1')
652
+ assert.equal(await createConda(createKernel(root, 'darwin')).check(), false)
653
+ const bin = createBin(root, 'darwin')
654
+ await bin.tryList()
655
+ assert.equal(bin.correct_conda, false)
656
+ })
657
+
658
+ test('certificate refresh preserves named environments and uses the new CA bundle', async t => {
659
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-refresh-'))
660
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
661
+ await writeHealthyCondaMeta(root, 'darwin')
662
+ await writeFakeManagedConda(root, 'darwin')
663
+ const marker = path.join(root, 'bin/miniforge/envs/app/keep')
664
+ await fs.mkdir(path.dirname(marker), { recursive: true })
665
+ await fs.writeFile(marker, 'existing app')
666
+ const kernel = createKernel(root, 'darwin')
667
+ const calls = []
668
+ kernel.bin.download = async () => assert.fail('must not replace a working runtime')
669
+ kernel.bin.exec = async payload => { calls.push(payload); return {} }
670
+ await createConda(kernel).install({}, () => {})
671
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
672
+ assert.equal(calls.length, 2)
673
+ assert.equal(calls[1].env.CONDA_SSL_VERIFY, path.join(root, 'bin/miniforge/certificates.pem'))
674
+ })
675
+
676
+ test('failed pip bootstrap stops certificate refresh without replacing environments', async t => {
677
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-failed-'))
678
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
679
+ await writeHealthyCondaMeta(root, 'darwin')
680
+ await writeFakeManagedConda(root, 'darwin')
681
+ const kernel = createKernel(root, 'darwin')
682
+ kernel.bin.exec = async () => ({ error: ['ERROR: hash verification failed'] })
683
+ kernel.bin.download = async () => assert.fail('must preserve the runtime')
684
+ await assert.rejects(createConda(kernel).install({}, () => {}), /bootstrap failed/)
685
+ assert.equal(await pathExists(path.join(root, 'bin/miniforge/conda-meta')), true)
686
+ })
687
+
688
+ test('fresh installation does not report a failed pip bootstrap as finished', async t => {
689
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-install-failed-'))
690
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
691
+ await fs.mkdir(path.join(root, 'bin'), { recursive: true })
692
+ const kernel = createKernel(root, 'darwin')
693
+ kernel.bin.mods = []
694
+ kernel.bin.download = async () => {}
695
+ kernel.bin.exec = async payload => {
696
+ if (payload.conda?.skip) {
697
+ await fs.mkdir(path.join(root, 'bin/miniforge/conda-meta'), { recursive: true })
698
+ return {}
699
+ }
700
+ return { error: ['ERROR: pip download failed'] }
701
+ }
702
+ await assert.rejects(createConda(kernel)._install({}, () => {}), /pip installation failed/)
703
+ })
@@ -3,7 +3,7 @@ const test = require('node:test')
3
3
 
4
4
  const Node = require('../kernel/bin/node')
5
5
 
6
- function createNode({ nodeVersion = '24.18.0', pnpmVersion = '11.9.0', hasNode = true, hasPnpm = true } = {}) {
6
+ function createNode({ nodeVersion = '24.22.1', pnpmVersion = '11.9.0', hasNode = true, hasPnpm = true } = {}) {
7
7
  const node = new Node()
8
8
  const conda = new Set()
9
9
  if (hasNode) conda.add('nodejs')
@@ -23,12 +23,12 @@ function createNode({ nodeVersion = '24.18.0', pnpmVersion = '11.9.0', hasNode =
23
23
  }
24
24
 
25
25
  test('Node bin pins Node.js 24 LTS and matching pnpm', () => {
26
- assert.equal(createNode().cmd(), 'nodejs=24.18.0 pnpm=11.9.0')
26
+ assert.equal(createNode().cmd(), 'nodejs=24.22.1 pnpm=11.9.0')
27
27
  })
28
28
 
29
29
  test('Node.installed accepts exactly the pinned Node.js and pnpm versions', async () => {
30
30
  assert.equal(await createNode().installed(), true)
31
- assert.equal(await createNode({ nodeVersion: '24.18.0-h654b19f_0' }).installed(), true)
31
+ assert.equal(await createNode({ nodeVersion: '24.22.1-h654b19f_0' }).installed(), true)
32
32
  assert.equal(await createNode({ pnpmVersion: '11.9.0-h7c87c79_0' }).installed(), true)
33
33
  assert.equal(await createNode({ nodeVersion: '22.21.1' }).installed(), false)
34
34
  assert.equal(await createNode({ nodeVersion: '26.4.0' }).installed(), false)
@@ -377,3 +377,49 @@ test('redactEnvArgs summarizes protected argv env values', () => {
377
377
  truncated: false
378
378
  })
379
379
  })
380
+
381
+
382
+ test('new venv bootstrap failure prevents app commands even with chain:*', { skip: process.platform === 'win32' }, async t => {
383
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-bootstrap-'))
384
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
385
+ const kernel = createKernel(root)
386
+ kernel.bin = { path: (...parts) => path.join(root, 'bin', ...parts) }
387
+ const shell = createShell(kernel)
388
+ shell.shell = 'bash'
389
+ shell.env = {}
390
+ shell.conda_hook = () => ':'
391
+ const fakeBin = path.join(root, 'fake-bin')
392
+ await fs.mkdir(fakeBin)
393
+ await fs.writeFile(path.join(fakeBin, 'python'), '#!/bin/sh\nif [ "$1" = "-m" ]; then mkdir -p "$3/bin"; touch "$3/bin/activate"; exit 0; fi\nexit 19\n', { mode: 0o755 })
394
+ const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', chain: '*', message: `touch '${root}/app-ran'` })
395
+ const result = require('node:child_process').spawnSync('bash', ['--noprofile', '--norc', '-c', shell.build(params)], { env: { ...process.env, PATH: fakeBin + ':' + process.env.PATH }, encoding: 'utf8' })
396
+ assert.notEqual(result.status, 0, result.stderr)
397
+ assert.equal(await fs.access(path.join(root, 'app-ran')).then(() => true, () => false), false)
398
+ })
399
+
400
+ test('existing app environments do not run the creation-only pip bootstrap', async t => {
401
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-existing-env-'))
402
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
403
+ await fs.mkdir(path.join(root, 'env'))
404
+ const shell = createShell(createKernel(root))
405
+ shell.shell = 'bash'
406
+ const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', message: 'echo app' })
407
+ assert.doesNotMatch(shell.build(params), /pip.*install/)
408
+ })
409
+
410
+
411
+ test('existing venv with outdated pip cannot bypass the guard on a retry', { skip: process.platform === 'win32' }, async t => {
412
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-retry-'))
413
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
414
+ await fs.mkdir(path.join(root, 'env/bin'), { recursive: true })
415
+ await fs.writeFile(path.join(root, 'env/bin/activate'), '')
416
+ const fakeBin = path.join(root, 'fake-bin')
417
+ await fs.mkdir(fakeBin)
418
+ await fs.writeFile(path.join(fakeBin, 'python'), '#!/bin/sh\nexit 19\n', { mode: 0o755 })
419
+ const shell = createShell(createKernel(root))
420
+ shell.shell = 'bash'
421
+ const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', chain: '*', message: `touch '${root}/app-ran'` })
422
+ const result = require('node:child_process').spawnSync('bash', ['--noprofile', '--norc', '-c', shell.build(params)], { env: { ...process.env, PATH: fakeBin + ':' + process.env.PATH }, encoding: 'utf8' })
423
+ assert.notEqual(result.status, 0, result.stderr)
424
+ assert.equal(await fs.access(path.join(root, 'app-ran')).then(() => true, () => false), false)
425
+ })
@@ -16,13 +16,13 @@ function createUV(version) {
16
16
  return uv
17
17
  }
18
18
 
19
- test('UV bin pins uv to 0.11.23', () => {
20
- assert.equal(createUV('0.11.23').cmd(), 'uv=0.11.23')
19
+ test('UV bin pins uv to 0.13.4', () => {
20
+ assert.equal(createUV('0.13.4').cmd(), 'uv=0.13.4')
21
21
  })
22
22
 
23
- test('UV.installed accepts exactly uv 0.11.23', async () => {
24
- assert.equal(await createUV('0.11.23').installed(), true)
25
- assert.equal(await createUV('0.11.23-h1234567_0').installed(), true)
23
+ test('UV.installed accepts exactly uv 0.13.4', async () => {
24
+ assert.equal(await createUV('0.13.4').installed(), true)
25
+ assert.equal(await createUV('0.13.4-h1234567_0').installed(), true)
26
26
  assert.equal(await createUV('0.11.22').installed(), false)
27
27
  assert.equal(await createUV('0.12.0').installed(), false)
28
28
  assert.equal(await createUV(null).installed(), false)