pinokiod 8.2.18 → 8.2.20
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/kernel/bin/conda-pins.js +1 -1
- package/package.json +1 -1
- package/server/index.js +30 -7
- package/test/anywhere-api.test.js +29 -3
- package/test/conda-bin.test.js +71 -7
package/kernel/bin/conda-pins.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path')
|
|
|
3
3
|
const { resolveAsarPath } = require('../util')
|
|
4
4
|
const PIP_WHEEL = require('../../system/pip_seed.json')
|
|
5
5
|
|
|
6
|
-
const CONDA_PIN_VERSION = "26.
|
|
6
|
+
const CONDA_PIN_VERSION = "26.7.0"
|
|
7
7
|
// Conda 26.9.4 has no Python 3.10 builds. Apply its CA-bundle requirements
|
|
8
8
|
// explicitly while retaining the managed Python version required by apps.
|
|
9
9
|
const TLS_PACKAGE_MINIMUMS = {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1037,7 +1037,7 @@ class Server {
|
|
|
1037
1037
|
const entries = await this.kernel.api.listApps()
|
|
1038
1038
|
return { apps: entries.map((entry) => ({ id: entry.id, name: entry.title || entry.name || entry.id, icon: entry.icon })) }
|
|
1039
1039
|
}
|
|
1040
|
-
// launch
|
|
1040
|
+
// launch and stop are side effects: only pinokio.co and this computer may call them
|
|
1041
1041
|
anywhereOriginAllowed(origin) {
|
|
1042
1042
|
if (!origin) return true
|
|
1043
1043
|
let parsed
|
|
@@ -1067,10 +1067,15 @@ class Server {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
return items
|
|
1069
1069
|
}
|
|
1070
|
+
// the folder of an installed app, or "" for anything that is not exactly one folder name under api/
|
|
1071
|
+
async anywhereAppDir(id) {
|
|
1072
|
+
const dir = typeof id === "string" ? this.kernel.path("api", id) : ""
|
|
1073
|
+
if (!dir || path.dirname(dir) !== this.kernel.path("api") || path.basename(dir) !== id) return ""
|
|
1074
|
+
return (await this.kernel.api.exists(dir)) ? dir : ""
|
|
1075
|
+
}
|
|
1070
1076
|
async composeAnywhereLaunch({ id, href } = {}) {
|
|
1071
1077
|
const api = this.kernel.api
|
|
1072
|
-
|
|
1073
|
-
if (!dir || path.dirname(dir) !== this.kernel.path("api") || path.basename(dir) !== id || !(await api.exists(dir))) {
|
|
1078
|
+
if (!(await this.anywhereAppDir(id))) {
|
|
1074
1079
|
return { status: 400, body: { error: `Unknown app: ${id}` } }
|
|
1075
1080
|
}
|
|
1076
1081
|
const menu = await this.anywhereMenu(api, await api.launcher_path(id))
|
|
@@ -1104,6 +1109,21 @@ class Server {
|
|
|
1104
1109
|
}
|
|
1105
1110
|
return { status: 200, body: { action: "started" } }
|
|
1106
1111
|
}
|
|
1112
|
+
// stops every run under the app's folder the way the desktop stops one (kernel.api.stop by its running key)
|
|
1113
|
+
async composeAnywhereStop({ id } = {}) {
|
|
1114
|
+
const api = this.kernel.api
|
|
1115
|
+
const dir = await this.anywhereAppDir(id)
|
|
1116
|
+
if (!dir) {
|
|
1117
|
+
return { status: 400, body: { error: `Unknown app: ${id}` } }
|
|
1118
|
+
}
|
|
1119
|
+
for (const [key, value] of Object.entries(api.running || {})) {
|
|
1120
|
+
const rel = path.relative(dir, key.split("?")[0])
|
|
1121
|
+
if (value && rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
1122
|
+
await api.stop({ params: { id: key } })
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return { status: 200, body: { action: "stopped" } }
|
|
1126
|
+
}
|
|
1107
1127
|
async composeAnywhereStatus({ light } = {}) {
|
|
1108
1128
|
const anywhere = this.kernel ? this.kernel.anywhere : null
|
|
1109
1129
|
if (!anywhere) {
|
|
@@ -14349,19 +14369,22 @@ class Server {
|
|
|
14349
14369
|
res.set("Cache-Control", "no-store")
|
|
14350
14370
|
res.json(await this.composeAnywhereApps())
|
|
14351
14371
|
}))
|
|
14352
|
-
this.
|
|
14372
|
+
// app actions change what runs on this computer: pinokio.co and this computer only
|
|
14373
|
+
const anywhereAppAction = (fn) => ex(async (req, res) => {
|
|
14353
14374
|
res.set("Cache-Control", "no-store")
|
|
14354
14375
|
if (!this.anywhereOriginAllowed(req.get("Origin"))) {
|
|
14355
|
-
res.status(403).json({ error: "
|
|
14376
|
+
res.status(403).json({ error: "Allowed only from pinokio.co or this computer" })
|
|
14356
14377
|
return
|
|
14357
14378
|
}
|
|
14358
14379
|
try {
|
|
14359
|
-
const { status, body } = await
|
|
14380
|
+
const { status, body } = await fn(req.body || {})
|
|
14360
14381
|
res.status(status).json(body)
|
|
14361
14382
|
} catch (error) {
|
|
14362
14383
|
res.status(500).json({ error: error && error.message ? error.message : String(error) })
|
|
14363
14384
|
}
|
|
14364
|
-
})
|
|
14385
|
+
})
|
|
14386
|
+
this.app.post("/anywhere/apps/launch", anywhereAppAction((body) => this.composeAnywhereLaunch(body)))
|
|
14387
|
+
this.app.post("/anywhere/apps/stop", anywhereAppAction((body) => this.composeAnywhereStop(body)))
|
|
14365
14388
|
const anywhereAction = (label, fn) => ex(async (req, res) => {
|
|
14366
14389
|
const anywhere = this.kernel.anywhere
|
|
14367
14390
|
if (!anywhere) {
|
|
@@ -110,7 +110,7 @@ test('the catalog maps installed apps to id, name and icon', async () => {
|
|
|
110
110
|
// a launch harness: one app "comfy" at /managed/api/comfy whose menu is set per case;
|
|
111
111
|
// kernel.status mirrors the kernel's session-aware running check
|
|
112
112
|
const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } = {}) => {
|
|
113
|
-
const calls = { process: [], cleared: [], reserved: [], counted: [] }
|
|
113
|
+
const calls = { process: [], cleared: [], reserved: [], counted: [], stopped: [] }
|
|
114
114
|
const api = {
|
|
115
115
|
exists: async (p) => p === '/managed/api/comfy',
|
|
116
116
|
launcher_path: async (name) => `/managed/api/${name}`,
|
|
@@ -118,6 +118,7 @@ const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } =
|
|
|
118
118
|
running,
|
|
119
119
|
logSessions: { reserveLaunch: async (uri) => { calls.reserved.push(uri) } },
|
|
120
120
|
process: (req) => { calls.process.push(req); return Promise.resolve() },
|
|
121
|
+
stop: async (req) => { calls.stopped.push(req.params.id) },
|
|
121
122
|
}
|
|
122
123
|
const server = Object.assign(Object.create(Server.prototype), {
|
|
123
124
|
kernel: {
|
|
@@ -128,7 +129,7 @@ const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } =
|
|
|
128
129
|
},
|
|
129
130
|
appPreferences: { recordLaunchByPath: async (p, options) => { calls.counted.push([p, options.source]) } },
|
|
130
131
|
})
|
|
131
|
-
return { server, calls, launch: (body) => server.composeAnywhereLaunch(body) }
|
|
132
|
+
return { server, calls, launch: (body) => server.composeAnywhereLaunch(body), stop: (body) => server.composeAnywhereStop(body) }
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
test('launch opens a loopback URL default at its Anywhere address, null until Serve carries it, other URLs as they are', async () => {
|
|
@@ -207,7 +208,32 @@ test('a drive-relative app id is refused on Windows', { skip: process.platform !
|
|
|
207
208
|
}
|
|
208
209
|
})
|
|
209
210
|
|
|
210
|
-
test('
|
|
211
|
+
test('stop ends every run under the app folder by its running key, and nothing else', async () => {
|
|
212
|
+
const running = {
|
|
213
|
+
'/managed/api/comfy/start.js': true,
|
|
214
|
+
'/managed/api/comfy/start.js&session=abc': true,
|
|
215
|
+
'/managed/api/comfy/sub/worker.js?cwd=/managed/api/comfy': true,
|
|
216
|
+
'/managed/api/comfy/old.js': false,
|
|
217
|
+
'/managed/api/comfyui/start.js': true,
|
|
218
|
+
'/managed/api/other/start.js': true,
|
|
219
|
+
}
|
|
220
|
+
const h = launchHarness({ running })
|
|
221
|
+
assert.deepEqual(await h.stop({ id: 'comfy' }), { status: 200, body: { action: 'stopped' } })
|
|
222
|
+
assert.deepEqual(h.calls.stopped, [
|
|
223
|
+
'/managed/api/comfy/start.js',
|
|
224
|
+
'/managed/api/comfy/start.js&session=abc',
|
|
225
|
+
'/managed/api/comfy/sub/worker.js?cwd=/managed/api/comfy',
|
|
226
|
+
])
|
|
227
|
+
const idle = launchHarness()
|
|
228
|
+
assert.deepEqual(await idle.stop({ id: 'comfy' }), { status: 200, body: { action: 'stopped' } })
|
|
229
|
+
assert.deepEqual(idle.calls.stopped, [])
|
|
230
|
+
for (const id of ['nope', '../comfy', 'comfy/start.js', '', undefined]) {
|
|
231
|
+
assert.equal((await h.stop({ id })).status, 400, String(id))
|
|
232
|
+
}
|
|
233
|
+
assert.equal(h.calls.stopped.length, 3)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('launch and stop are allowed from pinokio.co and this computer only', () => {
|
|
211
237
|
const server = Object.assign(Object.create(Server.prototype), { kernel: { anywhere: readyAnywhere() } })
|
|
212
238
|
for (const origin of [undefined, 'https://pinokio.co', 'http://localhost:42000', 'http://127.0.0.1:42000', 'https://pinokio.tail1.ts.net']) {
|
|
213
239
|
assert.equal(server.anywhereOriginAllowed(origin), true, String(origin))
|
package/test/conda-bin.test.js
CHANGED
|
@@ -211,11 +211,12 @@ test('Conda uses Miniforge assets and writes conda-forge-only config', async ()
|
|
|
211
211
|
})
|
|
212
212
|
|
|
213
213
|
test('Conda pins the managed Conda and Python versions consistently', async () => {
|
|
214
|
-
assert.equal(CONDA_PIN_VERSION, '26.
|
|
214
|
+
assert.equal(CONDA_PIN_VERSION, '26.7.0')
|
|
215
215
|
assert.equal(PYTHON_INSTALL_SPEC, 'python=3.10.20')
|
|
216
216
|
assert.equal(WINDOWS_PYTHON_SSL_FIX_SPEC, 'python=3.10.20=*_1_cpython')
|
|
217
217
|
|
|
218
|
-
assert.equal(isExpectedCondaPinned('26.
|
|
218
|
+
assert.equal(isExpectedCondaPinned('26.7.0'), true)
|
|
219
|
+
assert.equal(isExpectedCondaPinned('26.5.3'), false)
|
|
219
220
|
assert.equal(isExpectedCondaPinned('26.3.2'), false)
|
|
220
221
|
|
|
221
222
|
assert.equal(isExpectedPythonPinned('darwin', '3.10.20', 'cpython'), true)
|
|
@@ -235,7 +236,7 @@ test('Conda init writes the current Conda pin into an existing runtime', async (
|
|
|
235
236
|
|
|
236
237
|
assert.equal(
|
|
237
238
|
await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
|
|
238
|
-
['conda ==26.
|
|
239
|
+
['conda ==26.7.0', ...TLS_PACKAGE_SPECS].join('\n')
|
|
239
240
|
)
|
|
240
241
|
})
|
|
241
242
|
|
|
@@ -290,11 +291,11 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
|
|
|
290
291
|
assert.deepEqual(condaInstall.message[2], { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
|
|
291
292
|
assert.equal(
|
|
292
293
|
condaInstall.message[1],
|
|
293
|
-
'conda install -y --override-channels -c conda-forge "conda=26.
|
|
294
|
+
'conda install -y --override-channels -c conda-forge "conda=26.7.0" "python=3.10.20" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
|
|
294
295
|
)
|
|
295
296
|
assert.equal(
|
|
296
297
|
await fs.readFile(path.join(root, 'bin', 'miniforge', 'conda-meta', 'pinned'), 'utf8'),
|
|
297
|
-
['conda ==26.
|
|
298
|
+
['conda ==26.7.0', ...TLS_PACKAGE_SPECS].join('\n')
|
|
298
299
|
)
|
|
299
300
|
})
|
|
300
301
|
|
|
@@ -322,7 +323,7 @@ test('Conda install keeps the Windows Python 3.10.20 SSL-fixed build pin', async
|
|
|
322
323
|
assert.ok(condaInstall)
|
|
323
324
|
assert.equal(
|
|
324
325
|
condaInstall.message[1],
|
|
325
|
-
'conda install -y --override-channels -c conda-forge "conda=26.
|
|
326
|
+
'conda install -y --override-channels -c conda-forge "conda=26.7.0" "python=3.10.20=*_1_cpython" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
|
|
326
327
|
)
|
|
327
328
|
})
|
|
328
329
|
|
|
@@ -354,10 +355,73 @@ test('Conda install replaces the stale runtime and rebuilds declared Conda modul
|
|
|
354
355
|
assert.ok(condaInstall)
|
|
355
356
|
assert.equal(
|
|
356
357
|
condaInstall.message[1],
|
|
357
|
-
'conda install -y --override-channels -c conda-forge "conda=26.
|
|
358
|
+
'conda install -y --override-channels -c conda-forge "conda=26.7.0" "python=3.10.20" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19" uv=0.11.23 nodejs=22.21.1 pnpm'
|
|
358
359
|
)
|
|
359
360
|
})
|
|
360
361
|
|
|
362
|
+
test('Conda 26.5.3 triggers replacement even when certificates and pip are current', async t => {
|
|
363
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-version-update-'))
|
|
364
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
365
|
+
for (const platform of ['darwin', 'linux', 'win32']) {
|
|
366
|
+
for (const securityReady of [false, true]) {
|
|
367
|
+
const dir = path.join(root, platform, String(securityReady))
|
|
368
|
+
await writeHealthyCondaMeta(dir, platform, '26.5.3')
|
|
369
|
+
await writeFakeManagedConda(dir, platform)
|
|
370
|
+
if (!securityReady) {
|
|
371
|
+
for (const name of ['ca-certificates', 'certifi']) {
|
|
372
|
+
await fs.rm(path.join(dir, 'bin/miniforge/conda-meta', `${name}-2026.10.19.json`))
|
|
373
|
+
await writeCondaMeta(dir, name, '2026.7.22')
|
|
374
|
+
}
|
|
375
|
+
await fs.writeFile(path.join(dir, 'bin/miniforge/test-pip-version'), '26.2.1')
|
|
376
|
+
await fs.rm(path.join(dir, 'bin/miniforge/test-pip-seed'))
|
|
377
|
+
}
|
|
378
|
+
const trash = path.join(dir, 'bin/miniforge/Library/bin/libcrypto-3-x64.dll.c~.conda_trash')
|
|
379
|
+
await fs.mkdir(path.dirname(trash), { recursive: true })
|
|
380
|
+
await fs.writeFile(trash, 'stale trash')
|
|
381
|
+
const appFile = path.join(dir, 'api/demo/env/keep.txt')
|
|
382
|
+
await fs.mkdir(path.dirname(appFile), { recursive: true })
|
|
383
|
+
await fs.writeFile(appFile, 'existing app venv')
|
|
384
|
+
const bin = createBin(dir, platform)
|
|
385
|
+
await bin.tryList()
|
|
386
|
+
assert.equal(bin.correct_conda, false)
|
|
387
|
+
|
|
388
|
+
const kernel = createKernel(dir, platform)
|
|
389
|
+
kernel.bin.mods = []
|
|
390
|
+
let downloads = 0
|
|
391
|
+
kernel.bin.download = async () => { downloads++ }
|
|
392
|
+
kernel.bin.rm = async () => {}
|
|
393
|
+
const calls = []
|
|
394
|
+
kernel.bin.exec = async payload => {
|
|
395
|
+
calls.push(payload)
|
|
396
|
+
if (payload.conda?.skip) {
|
|
397
|
+
assert.equal(await pathExists(trash), false, 'replacement removes old runtime and trash')
|
|
398
|
+
assert.equal(await pathExists(path.join(dir, 'bin/miniforge/conda-meta')), false)
|
|
399
|
+
await fs.mkdir(path.join(dir, 'bin/miniforge/conda-meta'), { recursive: true })
|
|
400
|
+
await writeFakeManagedConda(dir, platform)
|
|
401
|
+
if (platform === 'win32') await fs.writeFile(path.join(dir, 'bin/miniforge/python.exe'), 'fake python\n')
|
|
402
|
+
} else {
|
|
403
|
+
assert.match(payload.message[1], /"conda=26\.7\.0"/)
|
|
404
|
+
assert.deepEqual(payload.message[2], { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
|
|
405
|
+
await writeHealthyCondaMeta(dir, platform)
|
|
406
|
+
}
|
|
407
|
+
return {}
|
|
408
|
+
}
|
|
409
|
+
const conda = createConda(kernel)
|
|
410
|
+
await conda.install({ dependencies: [] }, () => {})
|
|
411
|
+
assert.equal(await conda.check(), true)
|
|
412
|
+
await bin.tryList()
|
|
413
|
+
assert.equal(bin.correct_conda, true)
|
|
414
|
+
assert.equal(await fs.readFile(appFile, 'utf8'), 'existing app venv')
|
|
415
|
+
assert.equal(await pathExists(trash), false)
|
|
416
|
+
assert.equal(downloads, 1)
|
|
417
|
+
assert.equal(calls.length, 2, 'installer and bootstrap run once')
|
|
418
|
+
await conda.install({ dependencies: [] }, () => {})
|
|
419
|
+
assert.equal(downloads, 1, 'the updated runtime is not reinstalled on a retry')
|
|
420
|
+
assert.equal(calls.length, 2)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
|
|
361
425
|
test('Conda replacement stops a running managed Caddy without restoring it', async () => {
|
|
362
426
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-caddy-lifecycle-'))
|
|
363
427
|
await createMiniforge(root)
|