@ai-ecoverse/slicc-kernel 1.88.0 → 1.88.1

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/README.md CHANGED
@@ -659,7 +659,7 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
659
659
  - `interpreters` is the chain of `#!` and `binfmt` interpreters the kernel will really run, outermost first, each absolute with its symlinks resolved, and empty for a program of the catalog (Emscripten, WASI or JS) and the `/usr/bin/<name>` entries for one. A plain `#!/usr/bin/env X` (or `env -S X …`) is reported as `X`: a command of the catalog as `/usr/bin/X`, a script command by its file. `env` with any other option or a `NAME=value` assignment is run and reported as `/usr/bin/env` itself;
660
660
  - `trusted` is true when the program and every interpreter are regular files owned by root and not writable by group or others, under directories that are all the same, up to `/` (for a command of the catalog, its package files). It's false unless `users` is `'enforce'`: under `'off'` files have no owners, and under `'report'` owners don't stop a write. The reply must be `{ values, placeholders }` (string maps) within 10 s; anything else is `EACCES`. Each variable of the child's environment whose value is exactly `placeholders[NAME]` gets `values[NAME]`; nothing else changes. The kernel keeps no copy and logs nothing of either. Like any environment, the values are inherited by whatever the child itself spawns or execs, as on Linux. A ticket isn't carried across `sudo`, which builds its own environment: a `sudo` run with `SLICC_SEALED_TICKET` set fails with `EACCES`, and burns the ticket.
661
661
 
662
- **The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
662
+ **The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. Under `O_NONBLOCK` it gives `EAGAIN` when the pipe or terminal under it would. A write that finds room returns at once, taking at most 64 KiB, and the scrubbed bytes the file couldn't take yet go out before anything written later. Until they have, `poll` reports it unwritable and a nonblocking write gets `EAGAIN`, taking nothing. A blocking write that waits for them, or for room, ends with `EINTR` at a signal, taking nothing; one interrupted after it began writing returns its whole count, and the rest follows. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
663
663
 
664
664
  ### `/dev/clipboard`
665
665
 
@@ -4580,12 +4580,12 @@ async function interruptible(work, signal) {
4580
4580
  if (signal.aborted) throw new KernelError("EINTR");
4581
4581
  let stop = () => {
4582
4582
  };
4583
- const interrupted2 = new Promise((_, reject) => {
4583
+ const interrupted3 = new Promise((_, reject) => {
4584
4584
  stop = () => reject(new KernelError("EINTR"));
4585
4585
  signal.addEventListener("abort", stop, { once: true });
4586
4586
  });
4587
4587
  try {
4588
- return await Promise.race([work, interrupted2]);
4588
+ return await Promise.race([work, interrupted3]);
4589
4589
  } finally {
4590
4590
  signal.removeEventListener("abort", stop);
4591
4591
  }
@@ -9594,11 +9594,12 @@ var MountFs = class {
9594
9594
  });
9595
9595
  }
9596
9596
  async chmod(path, mode) {
9597
- const found = this.at(path);
9598
- if (!found) return this.base.chmod(path, mode);
9599
- this.writable(found.mount, normalizePath(path));
9597
+ const real = await this.follow(path);
9598
+ const found = this.at(real);
9599
+ if (!found) return this.base.chmod(real, mode);
9600
+ this.writable(found.mount, real);
9600
9601
  if (!found.mount.caps.chmod) return;
9601
- this.table.forget(found.mount, normalizePath(path));
9602
+ this.table.forget(found.mount, real);
9602
9603
  await found.mount.conn.call({ op: "setattr", path: found.rel, mode });
9603
9604
  }
9604
9605
  async utimes(path, atime, mtime) {
@@ -10615,7 +10616,7 @@ function mountTable(mounts = []) {
10615
10616
  }
10616
10617
 
10617
10618
  // src/version.ts
10618
- var KERNEL_VERSION = true ? "1.88.0" : "0.0.0-dev";
10619
+ var KERNEL_VERSION = true ? "1.88.1" : "0.0.0-dev";
10619
10620
  var OS_RELEASE = "6.0.0-slicc";
10620
10621
  function procVersion() {
10621
10622
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12182,16 +12183,16 @@ function interruption(signal) {
12182
12183
  }
12183
12184
  async function selectFds(fds, read, write, timeoutMs, interrupt) {
12184
12185
  const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
12185
- const interrupted2 = interruption(interrupt);
12186
- interrupted2.promise.catch(() => {
12186
+ const interrupted3 = interruption(interrupt);
12187
+ interrupted3.promise.catch(() => {
12187
12188
  });
12188
12189
  try {
12189
- return await waitReady(fds, read, write, deadline, interrupted2.promise);
12190
+ return await waitReady(fds, read, write, deadline, interrupted3.promise);
12190
12191
  } finally {
12191
- interrupted2.done();
12192
+ interrupted3.done();
12192
12193
  }
12193
12194
  }
12194
- async function waitReady(fds, read, write, deadline, interrupted2) {
12195
+ async function waitReady(fds, read, write, deadline, interrupted3) {
12195
12196
  for (; ; ) {
12196
12197
  const now = ready(fds, read, write);
12197
12198
  if (now.read.length > 0 || now.write.length > 0 || Date.now() >= deadline) return now;
@@ -12203,7 +12204,7 @@ async function waitReady(fds, read, write, deadline, interrupted2) {
12203
12204
  let timer;
12204
12205
  const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => timer = setTimeout(resolve2, deadline - Date.now()))];
12205
12206
  try {
12206
- await Promise.race([...changes, ...expiry, interrupted2]);
12207
+ await Promise.race([...changes, ...expiry, interrupted3]);
12207
12208
  } finally {
12208
12209
  round.abort();
12209
12210
  clearTimeout(timer);
@@ -15132,63 +15133,137 @@ function compile(secrets, options = {}) {
15132
15133
 
15133
15134
  // src/kernel/scrub-file.ts
15134
15135
  var SCRUB_IDLE_MS = 50;
15136
+ var SCRUB_NONBLOCK_MAX = 64 * 1024;
15135
15137
  var encoder5 = new TextEncoder();
15136
15138
  function scrubberOf(secrets) {
15137
15139
  const usable = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
15138
15140
  return usable.length > 0 ? compile(usable, { idleMs: SCRUB_IDLE_MS }) : void 0;
15139
15141
  }
15140
- async function writeAll2(file, bytes, signal) {
15142
+ async function writeAll2(file, bytes) {
15141
15143
  const write = file.write;
15142
- for (let at = 0; at < bytes.length; ) {
15143
- at += await write.call(file, bytes.subarray(at), at === 0 ? signal : void 0);
15144
- }
15144
+ for (let at = 0; at < bytes.length; ) at += await write.call(file, bytes.subarray(at));
15145
15145
  }
15146
15146
  function bound(file, key) {
15147
15147
  const method = file[key];
15148
15148
  return typeof method === "function" ? { [key]: method.bind(file) } : {};
15149
15149
  }
15150
+ function interrupted2(err) {
15151
+ const code = err?.code;
15152
+ return code === "EINTR" || code === "EAGAIN";
15153
+ }
15150
15154
  function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
15151
15155
  const file = inner.file;
15152
15156
  const stream = scrubber.stream();
15153
- let queue = Promise.resolve();
15157
+ let tail = Promise.resolve();
15158
+ let pending = 0;
15159
+ let busy = false;
15154
15160
  let timer;
15155
15161
  let failed3;
15162
+ const waiters = /* @__PURE__ */ new Set();
15163
+ const quiet = () => !busy && pending === 0;
15164
+ const wake = () => {
15165
+ for (const waiter of [...waiters]) waiter();
15166
+ };
15156
15167
  const raise = () => {
15157
15168
  if (failed3 === void 0) return;
15158
15169
  const err = failed3;
15159
15170
  failed3 = void 0;
15160
15171
  throw err;
15161
15172
  };
15162
- const enqueue = (bytes, signal) => {
15163
- const next2 = queue.then(() => bytes.length > 0 ? writeAll2(file, bytes, signal) : void 0);
15164
- queue = next2.catch(() => void 0);
15165
- return next2;
15173
+ const later = (bytes) => {
15174
+ if (bytes.length === 0) return;
15175
+ pending += 1;
15176
+ tail = tail.then(() => writeAll2(file, bytes)).catch((err) => {
15177
+ if (failed3 === void 0) failed3 = err;
15178
+ }).finally(() => {
15179
+ pending -= 1;
15180
+ if (quiet()) wake();
15181
+ });
15182
+ };
15183
+ const settling = (signal) => new Promise((resolve2, reject) => {
15184
+ const done4 = () => {
15185
+ waiters.delete(done4);
15186
+ signal?.removeEventListener("abort", stop);
15187
+ resolve2();
15188
+ };
15189
+ const stop = () => {
15190
+ waiters.delete(done4);
15191
+ reject(new KernelError("EINTR"));
15192
+ };
15193
+ if (signal?.aborted) return stop();
15194
+ waiters.add(done4);
15195
+ signal?.addEventListener("abort", stop, { once: true });
15196
+ });
15197
+ const settled = async () => {
15198
+ while (!quiet()) await settling();
15199
+ };
15200
+ const changed = async (signal) => {
15201
+ const round = new AbortController();
15202
+ const stop = signal ? AbortSignal.any([signal, round.signal]) : round.signal;
15203
+ try {
15204
+ const own = settling(stop);
15205
+ await Promise.race(file.changed ? [own, file.changed(stop)] : [own]);
15206
+ } catch (err) {
15207
+ throw signal?.aborted ? new KernelError("EINTR") : err;
15208
+ } finally {
15209
+ round.abort();
15210
+ }
15211
+ };
15212
+ const turn = async (signal) => {
15213
+ for (let waited = false; ; waited = true) {
15214
+ if (quiet() && (pollFile(file).writable || !file.changed)) {
15215
+ busy = true;
15216
+ return;
15217
+ }
15218
+ if (signal?.aborted) throw new KernelError(waited ? "EINTR" : "EAGAIN");
15219
+ await changed(signal);
15220
+ }
15221
+ };
15222
+ const send = async (out, signal) => {
15223
+ const write = file.write;
15224
+ for (let at = 0; at < out.length; ) {
15225
+ try {
15226
+ at += await write.call(file, out.subarray(at), signal);
15227
+ } catch (err) {
15228
+ if (!signal?.aborted || !interrupted2(err)) throw err;
15229
+ }
15230
+ if (at < out.length && signal?.aborted) return later(out.subarray(at));
15231
+ }
15166
15232
  };
15167
15233
  const release = () => {
15168
15234
  timer = void 0;
15169
- enqueue(stream.idle()).catch((err) => {
15170
- failed3 = err;
15171
- });
15235
+ later(stream.idle());
15172
15236
  };
15173
15237
  return new OpenFile({
15174
15238
  ...bound(file, "read"),
15175
15239
  ...bound(file, "peek"),
15176
- ...bound(file, "poll"),
15177
- ...bound(file, "changed"),
15178
15240
  ...bound(file, "stat"),
15241
+ poll: () => {
15242
+ const state = pollFile(file);
15243
+ return quiet() ? state : { ...state, writable: false };
15244
+ },
15245
+ changed,
15179
15246
  ...file.tty ? { tty: file.tty } : {},
15180
15247
  ...file.clipboard ? { clipboard: file.clipboard } : {},
15181
15248
  ...file.write ? {
15182
15249
  write: async (bytes, signal) => {
15183
- if (timer !== void 0) clearTimeout(timer);
15184
- raise();
15185
- await enqueue(stream.push(bytes), signal);
15250
+ await turn(signal);
15251
+ const taken = signal?.aborted && bytes.length > SCRUB_NONBLOCK_MAX ? bytes.subarray(0, SCRUB_NONBLOCK_MAX) : bytes;
15252
+ try {
15253
+ raise();
15254
+ if (timer !== void 0) clearTimeout(timer);
15255
+ timer = void 0;
15256
+ await send(stream.push(taken), signal);
15257
+ } finally {
15258
+ busy = false;
15259
+ if (quiet()) wake();
15260
+ }
15186
15261
  timer = setTimeout(release, idleMs);
15187
- return bytes.length;
15262
+ return taken.length;
15188
15263
  }
15189
15264
  } : {},
15190
15265
  flush: async () => {
15191
- await queue;
15266
+ await settled();
15192
15267
  raise();
15193
15268
  await file.flush?.();
15194
15269
  },
@@ -15196,7 +15271,9 @@ function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
15196
15271
  if (timer !== void 0) clearTimeout(timer);
15197
15272
  let failure2;
15198
15273
  try {
15199
- await enqueue(stream.end());
15274
+ await settled();
15275
+ later(stream.end());
15276
+ await settled();
15200
15277
  raise();
15201
15278
  } catch (err) {
15202
15279
  failure2 = { err };
@@ -15372,12 +15449,12 @@ var SealedChannel = class {
15372
15449
  if (signal.aborted) throw new KernelError("EINTR");
15373
15450
  let stop = () => {
15374
15451
  };
15375
- const interrupted2 = new Promise((_, reject) => {
15452
+ const interrupted3 = new Promise((_, reject) => {
15376
15453
  stop = () => reject(new KernelError("EINTR"));
15377
15454
  signal.addEventListener("abort", stop, { once: true });
15378
15455
  });
15379
15456
  try {
15380
- await Promise.race([flight.done, interrupted2]);
15457
+ await Promise.race([flight.done, interrupted3]);
15381
15458
  } finally {
15382
15459
  signal.removeEventListener("abort", stop);
15383
15460
  }
@@ -18340,11 +18417,9 @@ var Launcher = class {
18340
18417
  groupSession(pgid) {
18341
18418
  return this.jobs.sessionOf(pgid);
18342
18419
  }
18343
- treeScrubs(pid, req) {
18420
+ treeScrubs(req) {
18344
18421
  const inherited2 = req.ppid !== void 0 ? this.scrubs.get(req.ppid) : void 0;
18345
- const scrubs = req.scrub ? [...inherited2 ?? [], req.scrub] : inherited2;
18346
- if (scrubs) this.scrubs.set(pid, scrubs);
18347
- return scrubs;
18422
+ return req.scrub ? [...inherited2 ?? [], req.scrub] : inherited2;
18348
18423
  }
18349
18424
  start(req) {
18350
18425
  this.sampling();
@@ -18352,7 +18427,7 @@ var Launcher = class {
18352
18427
  throw new SpawnError("ESRCH");
18353
18428
  }
18354
18429
  const pid = req.pid ?? this.nextPid++;
18355
- const scrubs = this.treeScrubs(pid, req);
18430
+ const scrubs = this.treeScrubs(req);
18356
18431
  const stack = this.stackFor(req.tag);
18357
18432
  const terminal = req.fds.stdioTerminal();
18358
18433
  const decided = req.decided ?? Promise.resolve(true);
@@ -18422,6 +18497,7 @@ var Launcher = class {
18422
18497
  ...this.shownIds(req)
18423
18498
  });
18424
18499
  this.processes.set(pid, handle);
18500
+ if (scrubs) this.scrubs.set(pid, scrubs);
18425
18501
  const { cred } = req;
18426
18502
  this.creds.set(pid, () => handle.cred?.() ?? copyCred(cred));
18427
18503
  if (req.tag !== void 0) this.tags.set(pid, req.tag);