pinokiod 8.2.22 → 8.2.23

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.
@@ -253,12 +253,17 @@ class Huggingface {
253
253
  if (this.kernel.envs && result.env.HF_TOKEN_PATH === this.defaultTokenPath()) {
254
254
  this.kernel.envs.HF_TOKEN_PATH = result.env.HF_TOKEN_PATH
255
255
  }
256
- return true
256
+ return options.details === true ? { connected: true, invalid: false } : true
257
257
  } catch (error) {
258
- if (this.kernel.envs && error.tokenPath === this.defaultTokenPath()) {
258
+ // Match the pinned hf CLI's authentication errors, not arbitrary command failures.
259
+ const invalid = !error.killed && (
260
+ (error.code === 1 && /^Error: (?:Invalid user token\.[^\r\n]*|Not logged in)\r?$/m.test(stripAnsi(error.stderr))) ||
261
+ (typeof error.path === "string" && error.path === error.tokenPath && ["EISDIR", "ENOTDIR", "ENOTEMPTY"].includes(error.code))
262
+ )
263
+ if (invalid && this.kernel.envs && error.tokenPath === this.defaultTokenPath()) {
259
264
  this.kernel.envs.HF_TOKEN_PATH = this.kernel.path("cache", "HF_AUTH", "anonymous")
260
265
  }
261
- return false
266
+ return options.details === true ? { connected: false, invalid } : false
262
267
  }
263
268
  }
264
269
  async profile() {
package/kernel/shells.js CHANGED
@@ -47,8 +47,8 @@ class Shells {
47
47
  }
48
48
  */
49
49
  async init() {
50
- const hfDisconnected = !this.kernel.envs?.HF_TOKEN_PATH &&
51
- !(await this.kernel.connect.connected("huggingface", { timeout: 5000 }))
50
+ const hfInvalid = !this.kernel.envs?.HF_TOKEN_PATH &&
51
+ (await this.kernel.connect.connected("huggingface", { timeout: 5000, details: true }))?.invalid === true
52
52
 
53
53
  // iterate through all the envs
54
54
  let sh = new Shell(this.kernel)
@@ -61,7 +61,7 @@ class Shells {
61
61
  await sh.init_env({
62
62
  env: this.kernel.bin.envs({}),
63
63
  })
64
- if (hfDisconnected && sh.env.HF_TOKEN_PATH === this.kernel.path("cache", "HF_AUTH", "token")) {
64
+ if (hfInvalid && sh.env.HF_TOKEN_PATH === this.kernel.path("cache", "HF_AUTH", "token")) {
65
65
  sh.env.HF_TOKEN_PATH = this.kernel.path("cache", "HF_AUTH", "anonymous")
66
66
  }
67
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.22",
3
+ "version": "8.2.23",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -133,7 +133,7 @@ document.addEventListener('DOMContentLoaded', function() {
133
133
  <header class='task-shell-header'>
134
134
  <div class='task-shell-header-main'>
135
135
  <h1 class='task-title'>Remote</h1>
136
- <p class='task-description'>Open this Pinokio and its running apps from your phone or another computer, anywhere, over a private encrypted network.</p>
136
+ <p class='task-description'>Access this Pinokio and its running apps from another computer, phone, or tablet over a private encrypted network.</p>
137
137
  </div>
138
138
  <div class='task-header-actions'>
139
139
  <div class='anywhere-switch' id='anywhere-switch' data-on='false'>
@@ -147,7 +147,7 @@ document.addEventListener('DOMContentLoaded', function() {
147
147
  <div class='anywhere-state' data-state='off'>
148
148
  <div class='anywhere-card'>
149
149
  <h2>Remote access is off</h2>
150
- <p>Turn it on to reach this computer from your phone. Nothing is exposed to the public internet: only devices signed in to your own Tailscale account can connect.</p>
150
+ <p>Turn it on to access this Pinokio from another device. Nothing is exposed to the public internet: only devices signed in to your own Tailscale account can connect.</p>
151
151
  <p><button class='anywhere-btn' data-action='start'><i class="fa-solid fa-power-off"></i> Turn on</button></p>
152
152
  </div>
153
153
  </div>
@@ -162,7 +162,7 @@ document.addEventListener('DOMContentLoaded', function() {
162
162
  <div class='anywhere-state' data-state='login'>
163
163
  <div class='anywhere-card'>
164
164
  <h2>Sign in in your browser</h2>
165
- <p>Remote access uses <b>Tailscale</b>, a free private network. Sign in with a <b>personal</b> Google, Apple, GitHub or Microsoft account. You will use the same account on your phone.</p>
165
+ <p>Remote access uses <b>Tailscale</b>, a free private network. Sign in with a <b>personal</b> Google, Apple, GitHub or Microsoft account. Use the same account on every device you want to connect.</p>
166
166
  <p><a class='anywhere-btn' id='anywhere-login-link' href='#' target='_blank' rel='noopener'><i class="fa-solid fa-arrow-up-right-from-square"></i> Sign in with Tailscale</a></p>
167
167
  <p class='anywhere-muted'>Waiting for you to finish signing in… <span id='anywhere-login-url' class='anywhere-muted'></span></p>
168
168
  </div>
@@ -179,7 +179,7 @@ document.addEventListener('DOMContentLoaded', function() {
179
179
  <div class='anywhere-state' data-state='https'>
180
180
  <div class='anywhere-card'>
181
181
  <h2>One more click: enable HTTPS</h2>
182
- <p id='anywhere-approval-text'>Tailscale needs your permission once to issue HTTPS certificates for your network. Apps need HTTPS for things like camera and microphone access on your phone.</p>
182
+ <p id='anywhere-approval-text'>Tailscale needs your permission once to issue HTTPS certificates for your network. Apps need HTTPS for things like camera and microphone access in your browser.</p>
183
183
  <p><a class='anywhere-btn' id='anywhere-approval-link' href='#' target='_blank' rel='noopener'><i class="fa-solid fa-lock"></i> Enable HTTPS</a>
184
184
  <a class='anywhere-btn secondary' href='https://login.tailscale.com/admin/dns' target='_blank' rel='noopener'>Open DNS settings</a></p>
185
185
  <p class='anywhere-muted'>Waiting for HTTPS to be enabled… (in the admin console: DNS → HTTPS Certificates → Enable)</p>
@@ -197,19 +197,20 @@ document.addEventListener('DOMContentLoaded', function() {
197
197
  <div class='anywhere-state' data-state='ready'>
198
198
  <div class='anywhere-grid'>
199
199
  <div class='anywhere-card'>
200
- <h2>Connect your phone</h2>
200
+ <h2>Connect another device</h2>
201
201
  <div class='anywhere-qr'>
202
202
  <img id='anywhere-qr' alt='QR code to connect' src=''>
203
203
  <div class='anywhere-address'><a id='anywhere-address' href='#' target='_blank' rel='noopener'></a></div>
204
204
  <div class='anywhere-muted'>signed in as <b id='anywhere-account'></b></div>
205
- <div class='anywhere-waiting' id='anywhere-waiting'><i class="fa-solid fa-circle-notch fa-spin"></i> waiting for a phone to join…</div>
205
+ <div class='anywhere-waiting' id='anywhere-waiting'><i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for another device to join…</div>
206
206
  </div>
207
207
  <h3>Steps</h3>
208
208
  <ol class='anywhere-steps'>
209
- <li>Install the <b>Tailscale</b> app on your phone (App Store / Play Store).</li>
209
+ <li>Install <b>Tailscale</b> on the computer, phone, or tablet you want to connect from.</li>
210
210
  <li>Sign in with <b class='anywhere-account-inline'></b> and turn the connection on.</li>
211
- <li>Scan the QR code, or open the address above in your phone's browser. Use <i>Add to Home Screen</i> for an app-like icon.</li>
211
+ <li>On that device, scan the QR code or open the address above in a browser.</li>
212
212
  </ol>
213
+ <p class='anywhere-muted'>Bookmark the address for quick access. On a phone or tablet, you can also use <i>Add to Home Screen</i>.</p>
213
214
  </div>
214
215
  <div>
215
216
  <div class='anywhere-card'>
@@ -326,12 +327,13 @@ document.addEventListener('DOMContentLoaded', function() {
326
327
  document.getElementById('anywhere-account').textContent = email
327
328
  for (const el of document.querySelectorAll('.anywhere-account-inline')) el.textContent = email
328
329
  const waiting = document.getElementById('anywhere-waiting')
329
- if (current.mobilePeerPresent) {
330
+ const peers = Array.isArray(current.peers) ? current.peers : []
331
+ if (peers.length > 0) {
330
332
  waiting.classList.add('joined')
331
- waiting.innerHTML = `<i class="fa-solid fa-check"></i> phone ${current.mobilePeerOnline ? 'connected' : 'joined (currently offline)'}`
333
+ waiting.innerHTML = `<i class="fa-solid fa-check"></i> ${peers.some((peer) => peer.online) ? 'Another device is online' : 'No other devices are online'}`
332
334
  } else {
333
335
  waiting.classList.remove('joined')
334
- waiting.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> waiting for a phone to join…`
336
+ waiting.innerHTML = `<i class="fa-solid fa-circle-notch fa-spin"></i> Waiting for another device to join…`
335
337
  }
336
338
  const apps = Array.isArray(current.apps) ? current.apps : []
337
339
  document.getElementById('anywhere-apps').innerHTML = apps.length === 0
@@ -346,9 +348,8 @@ document.addEventListener('DOMContentLoaded', function() {
346
348
  </div>`).join('')
347
349
  const serve = Array.isArray(current.serve) ? current.serve : []
348
350
  document.getElementById('anywhere-ports').innerHTML = serve.length === 0 ? '—' : serve.map((entry) => `${esc(entry.name || (entry.port === 443 ? 'Pinokio' : ''))} <a class='remote' href='${esc(entry.url)}' target='_blank' rel='noopener'>${esc(entry.url)}</a> → ${esc(entry.target)}`).join('<br>')
349
- const peers = Array.isArray(current.peers) ? current.peers : []
350
351
  document.getElementById('anywhere-peers').innerHTML = peers.length === 0
351
- ? `<p class='anywhere-muted'>No other devices yet. Sign in to Tailscale on your phone with the same account.</p>`
352
+ ? `<p class='anywhere-muted'>No other devices yet. Sign in to Tailscale on another device with the same account.</p>`
352
353
  : peers.map((peer) => `
353
354
  <div class='anywhere-row'>
354
355
  <span class='anywhere-dot ${peer.online ? 'online' : ''}'></span>
@@ -368,7 +369,7 @@ document.addEventListener('DOMContentLoaded', function() {
368
369
  const poll = async () => {
369
370
  try {
370
371
  const response = await fetch('/anywhere/status', { cache: 'no-store' })
371
- if (!response.ok) throw new Error(`Could not refresh Anywhere status (${response.status})`)
372
+ if (!response.ok) throw new Error(`Could not refresh Remote status (${response.status})`)
372
373
  render(await response.json())
373
374
  } catch (e) {
374
375
  render(Object.assign({}, current, { phase: 'error', error: e.message }))
@@ -405,7 +406,7 @@ document.addEventListener('DOMContentLoaded', function() {
405
406
  if (action === 'logout') {
406
407
  Swal.fire({
407
408
  title: 'Sign out of Tailscale?',
408
- text: 'Your phone will no longer be able to reach this Pinokio until you sign in again.',
409
+ text: 'Your other devices will no longer be able to reach this Pinokio until you sign in again.',
409
410
  showCancelButton: true,
410
411
  confirmButtonText: 'Sign out'
411
412
  }).then((result) => { if (result.isConfirmed) act('logout') })
@@ -5,6 +5,7 @@ const path = require('node:path')
5
5
  const test = require('node:test')
6
6
 
7
7
  const HF = require('../kernel/api/hf')
8
+ const Connect = require('../kernel/connect')
8
9
  const HuggingfaceConnect = require('../kernel/connect/providers/huggingface')
9
10
  const Shell = require('../kernel/shell')
10
11
  const Shells = require('../kernel/shells')
@@ -18,6 +19,31 @@ function createKernel(root) {
18
19
  }
19
20
  }
20
21
 
22
+ const connectionFailures = [
23
+ { name: 'revoked token', invalid: true, stderr: 'Error: Invalid user token. The token stored is invalid.\n' },
24
+ { name: 'missing token', invalid: true, stderr: 'Error: Not logged in\r\n' },
25
+ { name: 'colored authentication error', invalid: true, stderr: '\x1b[31mError: Invalid user token.\x1b[0m\r\n' },
26
+ { name: 'unusable CLI path', invalid: false, code: 'ENOTDIR', syscall: 'spawn' },
27
+ { name: 'timeout', invalid: false, code: null, killed: true },
28
+ { name: 'timeout after authentication output', invalid: false, killed: true, stderr: 'Error: Invalid user token.\n' },
29
+ { name: 'network error', invalid: false, stderr: 'Error: Connection refused\n' },
30
+ { name: 'rate limit', invalid: false, stderr: "Error: You've hit the rate limit for the /whoami-v2 endpoint.\n" },
31
+ { name: 'server error', invalid: false, stderr: 'Error: 500 Server Error\n' },
32
+ { name: 'permission error', invalid: false, stderr: 'Error: 403 Forbidden\n' },
33
+ { name: 'missing CLI', invalid: false, code: 'ENOENT' },
34
+ { name: 'unrecognized CLI error', invalid: false, stderr: 'Error: Unexpected failure\n' },
35
+ { name: 'incidental authentication text', invalid: false, stderr: 'Warning: Previous output: Error: Invalid user token.\n' }
36
+ ]
37
+
38
+ function connectionError(fixture, tokenPath) {
39
+ return Object.assign(new Error(fixture.name), {
40
+ code: 1,
41
+ stderr: '',
42
+ ...fixture,
43
+ tokenPath
44
+ })
45
+ }
46
+
21
47
  test('hf.login does not accept an invalid saved token', async () => {
22
48
  const context = {
23
49
  parentPath: '/pinokio/api/test/login.js',
@@ -68,11 +94,123 @@ test('whoami updates only the managed shared token path', async () => {
68
94
 
69
95
  provider.runHf = async () => {
70
96
  const error = new Error('invalid token')
97
+ error.code = 1
98
+ error.stderr = 'Error: Invalid user token. The token stored is invalid.\n'
71
99
  error.tokenPath = tokenPath
72
100
  throw error
73
101
  }
74
102
  assert.equal(await provider.connected(), false)
75
103
  assert.equal(kernel.envs.HF_TOKEN_PATH, anonymousPath)
104
+
105
+ provider.runHf = async () => {
106
+ throw connectionError(connectionFailures[0], customContext.env.HF_TOKEN_PATH)
107
+ }
108
+ kernel.envs.HF_TOKEN_PATH = tokenPath
109
+ assert.equal(await provider.connected({}, customContext), false)
110
+ assert.equal(kernel.envs.HF_TOKEN_PATH, tokenPath)
111
+
112
+ provider.runHf = async () => ({ env: { HF_TOKEN_PATH: tokenPath } })
113
+ kernel.envs.HF_TOKEN_PATH = anonymousPath
114
+ assert.deepEqual(await provider.connected({ details: true }), { connected: true, invalid: false })
115
+ assert.equal(kernel.envs.HF_TOKEN_PATH, tokenPath)
116
+ })
117
+
118
+ test('only confirmed authentication failures change shared credential selection', async (t) => {
119
+ const root = path.join(os.tmpdir(), 'pinokio-hf-failure-states')
120
+ const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
121
+ const anonymousPath = path.join(root, 'cache', 'HF_AUTH', 'anonymous')
122
+ for (const fixture of connectionFailures) {
123
+ await t.test(fixture.name, async () => {
124
+ for (const initialPath of [tokenPath, anonymousPath]) {
125
+ const kernel = createKernel(root)
126
+ kernel.envs = { HF_TOKEN_PATH: initialPath }
127
+ const provider = new HuggingfaceConnect(kernel, {})
128
+ provider.runHf = async () => { throw connectionError(fixture, tokenPath) }
129
+
130
+ assert.deepEqual(await provider.connected({ details: true }), {
131
+ connected: false,
132
+ invalid: fixture.invalid
133
+ })
134
+ assert.equal(kernel.envs.HF_TOKEN_PATH, fixture.invalid ? anonymousPath : initialPath)
135
+ assert.equal(await provider.connected(), false)
136
+ assert.equal(kernel.envs.HF_TOKEN_PATH, fixture.invalid ? anonymousPath : initialPath)
137
+ }
138
+ })
139
+ }
140
+ })
141
+
142
+ test('startup propagates only confirmed shared authentication failures to app shells', async (t) => {
143
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-startup-'))
144
+ const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
145
+ const anonymousPath = path.join(root, 'cache', 'HF_AUTH', 'anonymous')
146
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'TEST_FIXTURE=1\n')
147
+ await fs.mkdir(path.dirname(tokenPath), { recursive: true })
148
+ await fs.writeFile(tokenPath, 'synthetic-token')
149
+ await fs.writeFile(path.join(path.dirname(tokenPath), 'stored_tokens'), 'synthetic-refresh-state')
150
+ try {
151
+ for (const fixture of [{ name: 'valid token', connected: true }, ...connectionFailures]) {
152
+ await t.test(fixture.name, async () => {
153
+ const kernel = createKernel(root)
154
+ kernel.bin = { envs: () => ({}) }
155
+ kernel.bracketedPasteSupport = {}
156
+ kernel.which = () => null
157
+ kernel.connect = new Connect(kernel)
158
+ let checks = 0
159
+ kernel.connect.clients.huggingface.runHf = async () => {
160
+ checks++
161
+ if (fixture.connected) return { env: { HF_TOKEN_PATH: tokenPath } }
162
+ throw connectionError(fixture, tokenPath)
163
+ }
164
+ const shells = new Shells(kernel)
165
+ shells.ensureBracketedPasteSupport = async () => {}
166
+ await shells.init()
167
+ const expectedPath = fixture.invalid ? anonymousPath : tokenPath
168
+ assert.equal(kernel.envs.HF_TOKEN_PATH, expectedPath)
169
+ await shells.init()
170
+ assert.equal(checks, 1)
171
+ assert.equal(kernel.envs.HF_TOKEN_PATH, expectedPath)
172
+
173
+ const appShell = new Shell(kernel)
174
+ await appShell.init_env({ env: {} })
175
+ assert.equal(appShell.env.HF_TOKEN_PATH, expectedPath)
176
+ assert.equal(await fs.readFile(tokenPath, 'utf8'), 'synthetic-token')
177
+ assert.equal(await fs.readFile(path.join(path.dirname(tokenPath), 'stored_tokens'), 'utf8'), 'synthetic-refresh-state')
178
+ })
179
+ }
180
+ } finally {
181
+ await fs.rm(root, { recursive: true, force: true })
182
+ }
183
+ })
184
+
185
+ test('a broken CLI executable path does not disable saved credentials at startup', async () => {
186
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-hf-cli-path-'))
187
+ const tokenPath = path.join(root, 'cache', 'HF_AUTH', 'token')
188
+ try {
189
+ await fs.writeFile(path.join(root, 'ENVIRONMENT'), 'TEST_FIXTURE=1\n')
190
+ await fs.mkdir(path.dirname(tokenPath), { recursive: true })
191
+ await fs.writeFile(tokenPath, 'synthetic-token')
192
+ const brokenCliParent = path.join(root, 'cli-parent')
193
+ await fs.writeFile(brokenCliParent, 'file instead of directory')
194
+ const kernel = createKernel(root)
195
+ kernel.bin = { envs: () => ({}) }
196
+ kernel.bracketedPasteSupport = {}
197
+ kernel.which = () => null
198
+ kernel.connect = new Connect(kernel)
199
+ const provider = kernel.connect.clients.huggingface
200
+ provider.hfPath = () => path.join(brokenCliParent, 'hf')
201
+ assert.deepEqual(await provider.connected({ details: true, timeout: 1000 }), {
202
+ connected: false,
203
+ invalid: false
204
+ })
205
+ const shells = new Shells(kernel)
206
+ shells.ensureBracketedPasteSupport = async () => {}
207
+ await shells.init()
208
+ assert.equal(kernel.envs.HF_TOKEN_PATH, tokenPath)
209
+ assert.equal(await fs.readFile(tokenPath, 'utf8'), 'synthetic-token')
210
+ assert.equal(await fs.readFile(brokenCliParent, 'utf8'), 'file instead of directory')
211
+ } finally {
212
+ await fs.rm(root, { recursive: true, force: true })
213
+ }
76
214
  })
77
215
 
78
216
  test('provider login ignores only Pinokio\'s inherited anonymous path', async () => {
@@ -108,9 +246,11 @@ test('invalid managed tokens are replaced once without overriding apps', async (
108
246
  kernel.bracketedPasteSupport = {}
109
247
  kernel.which = () => null
110
248
  kernel.connect = {
111
- connected: async () => {
249
+ connected: async (provider, options) => {
250
+ assert.equal(provider, 'huggingface')
251
+ assert.deepEqual(options, { timeout: 5000, details: true })
112
252
  checks++
113
- return false
253
+ return { connected: false, invalid: true }
114
254
  }
115
255
  }
116
256
  const shells = new Shells(kernel)
@@ -1,126 +0,0 @@
1
- # Security TODO (deferred)
2
-
3
- Written 2026-10-28. These are security items deliberately left out of the
4
- current work, kept here so they can be revisited later.
5
-
6
- The common thread: Pinokio owns the environment, the process spawn, the
7
- network path, the secrets and the whole fleet of installed apps. Each item
8
- below is a control that only a launcher can apply to every app at once.
9
-
10
- ---
11
-
12
- ## A. Foundation (fix before making any "safe" claim)
13
-
14
- ### A1. Origin check on the WebSocket
15
- - Where: `server/socket.js` (`new WebSocket.Server({ server })`, no `verifyClient`;
16
- `ws._origin` is recorded but never enforced).
17
- - Problem: any page that can open a WebSocket to the daemon can send
18
- `{method:"shell.run"}`. HTTPS pages are stopped by mixed-content rules, and
19
- Chrome 154 warns before public HTTP pages, but Pinokio's own app UIs run on
20
- `http://localhost:<port>` and are exempt from both. An XSS bug in a Gradio
21
- app or ComfyUI custom node becomes a shell command on the machine. With the
22
- HTTPS/Caddy router on, `wss://pinokio.localhost` is reachable from HTTPS
23
- pages too.
24
- - Fix: accept only Pinokio's own origins (the daemon UI, `*.localhost` router
25
- hosts, the Anywhere Tailscale host), plus a per-session token for the UI.
26
-
27
- ### A2. Network exposure of the daemon
28
- - Where: `server/index.js` (`server.listen(port)` with no host, so all
29
- interfaces; `cors({origin:'*'})`; `express-session` secret is the literal
30
- string `"secret"`; no auth middleware).
31
- - Fix: bind 127.0.0.1 unless LAN sharing or Anywhere is enabled; random
32
- session secret.
33
-
34
- ### A3. Static file routes serve the whole Pinokio home
35
- - Where: `server/index.js` mounts `express.static(kernel.homedir)` at `/asset`
36
- and `/files` with `serveIndex(hidden: true)`.
37
- - Problem: `key.json`, every `ENVIRONMENT` file and `connect/*/auth.json` are
38
- downloadable by anything that can reach the daemon.
39
- - Fix: deny-list secret files/dirs, or move secrets out of the home tree.
40
-
41
- ### A4. Secrets hygiene
42
- - `kernel/key.js` `set()` logs `{ key, val, password }` to the console.
43
- - Secrets live in plaintext: `~/pinokio/ENVIRONMENT`, per-app `ENVIRONMENT`,
44
- `key.json`, `cache/HF_AUTH/token`, `connect/<provider>/auth.json`.
45
- - A new app's ENVIRONMENT inherits every custom key from the system
46
- ENVIRONMENT (`kernel/environment.js`, `ENV("app")`), so user secrets
47
- propagate into every app.
48
- - Fix: stop the logging; OS keychain for storage; stop inheriting secrets into
49
- app ENVIRONMENT by default.
50
-
51
- ### A5. Unverified downloads and unpinned system clones
52
- - `Bin.download` (`kernel/bin/index.js`) does no hash check. Direct URL
53
- downloads with no hash: cloudflared (`releases/latest`), Homebrew zip and VS
54
- Build Tools from `cocktailpeanut/bin`, aria2, cmake, mingw, standalone
55
- python. Only Miniforge and the pip seed wheel are SHA-256 verified.
56
- - `pinokiocomputer/proto`, `network`, `proxy_registry`, `python` are cloned
57
- from GitHub HEAD, unpinned, and re-cloned on every version change.
58
- - Fix: hash table per download (as done for Miniforge); pin system repos to a
59
- commit or tag.
60
-
61
- ### A6. PyTorch safe unpickling is disabled globally
62
- - Where: `kernel/shell.js` sets `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` for every
63
- app shell.
64
- - Problem: turns off the `weights_only=True` default PyTorch has shipped since
65
- 2.6. Malicious pickle models (nullifAI, Feb 2025) are a known vector.
66
- - Fix: make it per-app opt-in with a visible warning badge instead of global.
67
-
68
- ---
69
-
70
- ## B. Platform security features (the "install through Pinokio and you're safe" story)
71
-
72
- ### B1. Security advisory feed + Security Center
73
- - A small signed JSON list of blocked package versions, maintained by Pinokio,
74
- fetched periodically (the OneCRL idea, applied to AI apps).
75
- - Enforce through env Pinokio already controls in `Shell.init_env`:
76
- `PIP_CONSTRAINT` and `UV_CONSTRAINT` (constraints file with
77
- `pkg!=x.y.z` lines), `UV_EXCLUDE_NEWER`, and bluefairy shims for npm/bun.
78
- - Example it would have stopped: Ultralytics 8.3.41/42/45/46 (Dec 2024
79
- cryptominer), which reached ComfyUI users via the Impact Pack.
80
- - Security Center page: audit every app env (pip version, certifi, blocked
81
- packages), one-click repair, fleet-wide. No app can do this for other apps.
82
-
83
- ### B2. Release-age cooldown on by default
84
- - bluefairy exists (`kernel/bin/bluefairy.js`) but `protection_enabled`
85
- defaults to `false` (`server/lib/app_preferences.js`), only applies to
86
- `shell.run` (forced `"off"` for `shell.start`, `kernel.exec`, `bin.exec`),
87
- and shims only npm/bun/uv, not pip or conda.
88
- - pnpm 11 and Yarn 4.10 now ship a cooldown on by default. Turn it on
89
- globally (24h-72h), extend to pip, let the advisory feed override.
90
-
91
- ### B3. Credential brokering (apps never see real secrets)
92
- - Run a local credential-injecting proxy and point apps at it via
93
- `HF_ENDPOINT`, `OPENAI_BASE_URL`, `ANTHROPIC_BASE_URL`, etc. The app gets a
94
- per-app placeholder; Pinokio adds the real token on the way out.
95
- - Gives per-app usage visibility and instant revocation. A malicious custom
96
- node has nothing to exfiltrate.
97
- - Simpler interim: issue fine-grained per-app HF tokens.
98
-
99
- ### B4. Model file scanning
100
- - `kernel/vault/*` (Disk Saver) already walks and hashes every model file.
101
- Add a picklescan/modelscan pass for `.ckpt`, `.pt`, `.pth`, `.bin` and
102
- surface results per app. Pairs with A6.
103
-
104
- ### B5. Per-app sandbox (phased)
105
- - Chokepoint: every app command goes through `pty.spawn` in `kernel/shell.js`
106
- (`Shell.init_env` → `exec`). The `exec` API (`kernel/api/exec`) and `sudo`
107
- path (`shell.js` ~610) bypass it and need the same wrapper.
108
- - Reference implementation: Anthropic sandbox-runtime (sandbox-exec on macOS,
109
- bubblewrap on Linux, local proxy for a domain allowlist; Windows in
110
- progress).
111
- - Phase 1, observe only: show which domains an app contacted and what it
112
- wrote outside its folder. Breaks nothing, already a visible value-add.
113
- - Phase 2: enforce per-app network allowlists (huggingface.co, pypi.org,
114
- github.com by default).
115
- - Phase 3: filesystem confinement to app folder + shared caches.
116
- - Windows is the hard platform; ship macOS/Linux first.
117
-
118
- ### B6. Script execution boundary (longer term)
119
- - `kernel/loader.js` `requireJS()` loads every `pinokio.js` / `install.js` /
120
- `start.js` with plain `require()` in the daemon process, and `pinokio.js`
121
- runs when an app is merely listed (`api.meta`, menu rendering).
122
- - `Api.resolveMethod` (`kernel/api/index.js`) dispatches `kernel.*` methods,
123
- `exec`, `fs.download`, `script.start` and `sudo` to scripts with the live
124
- kernel object.
125
- - Options: isolate scripts (vm / worker), capability gate at `resolveMethod`,
126
- stop executing `pinokio.js` on listing.