@ai-ecoverse/slicc-kernel 1.74.0 → 1.75.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.
@@ -783,6 +783,7 @@ function syncAccessHandles(scope = globalThis) {
783
783
  function isHandle(child) {
784
784
  return "kind" in child;
785
785
  }
786
+ var FILE_TIMES_MS = 100;
786
787
  var OpfsFs = class {
787
788
  root;
788
789
  meta;
@@ -1053,8 +1054,50 @@ var OpfsFs = class {
1053
1054
  const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
1054
1055
  return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
1055
1056
  }
1057
+ id = crypto.randomUUID();
1056
1058
  dirTimes = /* @__PURE__ */ new Map();
1057
1059
  dirDirty = /* @__PURE__ */ new Set();
1060
+ fileTimes = /* @__PURE__ */ new Map();
1061
+ written(target) {
1062
+ this.committed(target);
1063
+ if (!this.ranged) return;
1064
+ this.fileTimes.set(target, Date.now());
1065
+ this.scheduleFileTimes();
1066
+ }
1067
+ scheduleFileTimes() {
1068
+ if (this.flushing.file || this.fileTimes.size === 0) return;
1069
+ this.flushing.file = setTimeout(() => {
1070
+ void this.flushFileTimes().catch(() => void 0).finally(() => {
1071
+ this.flushing.file = void 0;
1072
+ this.scheduleFileTimes();
1073
+ });
1074
+ }, FILE_TIMES_MS);
1075
+ this.flushing.file.unref?.();
1076
+ }
1077
+ async flushFileTimes() {
1078
+ for (const [path, ms] of [...this.fileTimes]) {
1079
+ const mtimeFor = await this.modified(path).catch(() => 0);
1080
+ await this.meta.update(
1081
+ path,
1082
+ (entry) => this.fileTimes.get(path) !== ms || this.explicitWins(entry, ms) ? entry : { ...entry, path, mtimeMs: ms, mtimeFor, ctimeMs: ms }
1083
+ );
1084
+ if (this.fileTimes.get(path) === ms) this.fileTimes.delete(path);
1085
+ }
1086
+ }
1087
+ timesOf(path) {
1088
+ return this.dirTimes.get(path) ?? this.fileTimes.get(path);
1089
+ }
1090
+ explicitWins(entry, ms) {
1091
+ if (entry?.timesAt === void 0 || entry.timesBy === this.id) return false;
1092
+ return entry.timesAt >= ms;
1093
+ }
1094
+ movePending(from, to) {
1095
+ for (const [path, ms] of [...this.fileTimes]) {
1096
+ if (!within2(path, from)) continue;
1097
+ this.fileTimes.delete(path);
1098
+ if (to !== void 0) this.fileTimes.set(to + path.slice(from.length), ms);
1099
+ }
1100
+ }
1058
1101
  flushing = {};
1059
1102
  touch(...dirs) {
1060
1103
  const now = Date.now();
@@ -1078,11 +1121,10 @@ var OpfsFs = class {
1078
1121
  if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
1079
1122
  }
1080
1123
  }
1081
- async statOf(path, handle, meta) {
1124
+ async statOf(path, handle, meta, touched) {
1082
1125
  const directory = handle.kind === "directory";
1083
1126
  const file = directory ? void 0 : await handle.getFile();
1084
1127
  const modified = file?.lastModified ?? 0;
1085
- const touched = directory ? this.dirTimes.get(path) : void 0;
1086
1128
  const kept = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
1087
1129
  const mtime = touched ?? kept;
1088
1130
  return {
@@ -1114,13 +1156,14 @@ var OpfsFs = class {
1114
1156
  }
1115
1157
  async describe(found) {
1116
1158
  if (found.link) return this.owned(this.linkStat(found.link), found.path, found.link);
1159
+ const touched = this.timesOf(found.path);
1117
1160
  if (!this.owners) {
1118
1161
  const [meta2] = await this.meta.get([found.path]);
1119
- return this.statOf(found.path, found.handle, meta2);
1162
+ return this.statOf(found.path, found.handle, meta2, touched);
1120
1163
  }
1121
1164
  const entries = await this.meta.get(chain(found.path));
1122
1165
  const meta = entries.pop();
1123
- const stat = await this.statOf(found.path, found.handle, meta);
1166
+ const stat = await this.statOf(found.path, found.handle, meta, touched);
1124
1167
  return { ...stat, ...ownOf(meta) ?? inherit(entries) };
1125
1168
  }
1126
1169
  async stat(path) {
@@ -1177,13 +1220,14 @@ var OpfsFs = class {
1177
1220
  async pwrite(path, offset, bytes) {
1178
1221
  const [handle, , target] = await this.fileFor(path);
1179
1222
  await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
1180
- this.committed(target);
1223
+ this.written(target);
1181
1224
  }
1182
1225
  async truncate(path, size) {
1183
1226
  const handle = await this.existing(path, true);
1184
- const target = this.remote?.watched() ? (await this.locate(path, true)).path : path;
1227
+ const resolve = this.ranged || this.remote?.watched();
1228
+ const target = resolve ? (await this.locate(path, true)).path : path;
1185
1229
  await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
1186
- this.committed(target);
1230
+ this.written(target);
1187
1231
  }
1188
1232
  async writeSession(path, truncate) {
1189
1233
  const [handle, , target] = await this.fileFor(path);
@@ -1273,13 +1317,16 @@ var OpfsFs = class {
1273
1317
  async readdirStat(path) {
1274
1318
  const [dir, children2] = await this.listing(path);
1275
1319
  const base = dir === "/" ? "" : dir;
1320
+ const touched = children2.map(([name2]) => this.timesOf(`${base}/${name2}`));
1276
1321
  const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
1277
1322
  const handed2 = this.owners ? await this.handedTo(dir) : void 0;
1278
1323
  const owned4 = (stat, entry) => handed2 ? { ...stat, ...ownOf(entry) ?? handed2 } : stat;
1279
1324
  return Promise.all(
1280
1325
  children2.map(async ([name2, child], i) => {
1281
1326
  if (!isHandle(child)) return [name2, owned4(this.linkStat(child), child)];
1282
- const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
1327
+ const stat = await this.statOf(`${base}/${name2}`, child, metas[i], touched[i]).catch(
1328
+ () => null
1329
+ );
1283
1330
  return [name2, stat && owned4(stat, metas[i])];
1284
1331
  })
1285
1332
  );
@@ -1337,6 +1384,7 @@ var OpfsFs = class {
1337
1384
  throw translate(err, path);
1338
1385
  }
1339
1386
  this.changed(found.path);
1387
+ this.movePending(found.path);
1340
1388
  }
1341
1389
  await this.meta.remove([found.path]);
1342
1390
  this.touch(parentOf3(found.path));
@@ -1400,6 +1448,7 @@ var OpfsFs = class {
1400
1448
  const moved2 = await this.place(source.handle, dir, staging);
1401
1449
  if (swap) await this.rm(destination, { recursive: true });
1402
1450
  await this.meta.move(source.path, destination, carry);
1451
+ this.movePending(source.path, destination);
1403
1452
  if (!moved2) await this.rm(source.path, { recursive: true });
1404
1453
  let copied = !moved2;
1405
1454
  if (swap) {
@@ -1521,6 +1570,7 @@ var OpfsFs = class {
1521
1570
  const handle = found.handle;
1522
1571
  const file = handle.kind === "file" ? await handle.getFile() : void 0;
1523
1572
  this.dirTimes.delete(found.path);
1573
+ this.fileTimes.delete(found.path);
1524
1574
  const ctimeMs = Date.now();
1525
1575
  await this.meta.update(found.path, (entry) => ({
1526
1576
  ...entry,
@@ -1529,7 +1579,8 @@ var OpfsFs = class {
1529
1579
  mtimeMs: mtime.getTime(),
1530
1580
  mtimeFor: file?.lastModified ?? 0,
1531
1581
  ctimeMs,
1532
- timesAt: ctimeMs
1582
+ timesAt: ctimeMs,
1583
+ timesBy: this.id
1533
1584
  }));
1534
1585
  }
1535
1586
  async reconcile() {
@@ -2255,9 +2306,11 @@ var KernelPipe = class {
2255
2306
  // src/kernel/fd-table.ts
2256
2307
  var KernelError = class extends Error {
2257
2308
  code;
2258
- constructor(code) {
2309
+ detail;
2310
+ constructor(code, detail) {
2259
2311
  super(code);
2260
2312
  this.code = code;
2313
+ if (detail !== void 0) this.detail = detail;
2261
2314
  }
2262
2315
  };
2263
2316
  function pollFile(file) {
@@ -2400,6 +2453,7 @@ function kernelFdKind(file) {
2400
2453
  if (file.heldMeta && "device" in file.heldMeta) return "device";
2401
2454
  if (file.tty) return "tty";
2402
2455
  if (file.sealed) return "sealed";
2456
+ if (file.clipboard) return "clipboard";
2403
2457
  return file.seek ? "file" : "stream";
2404
2458
  }
2405
2459
  var FdTable = class _FdTable {
@@ -4190,6 +4244,203 @@ function serveClient(port, host) {
4190
4244
  return { detach, closed: ended.promise };
4191
4245
  }
4192
4246
 
4247
+ // src/kernel/clipboard.ts
4248
+ var CLIPBOARD_MAX_BYTES = 1024 * 1024;
4249
+ var CLIPBOARD_TIMEOUT_MS = 1e4;
4250
+ var encoder = new TextEncoder();
4251
+ var decoder = new TextDecoder("utf-8", { fatal: true });
4252
+ var READY = { readable: true, writable: true, hangup: false };
4253
+ function lineOf(err) {
4254
+ const text = err instanceof Error ? err.message : String(err);
4255
+ return (text.split("\n")[0] ?? "").slice(0, 200);
4256
+ }
4257
+ function clipboardFailure(err) {
4258
+ if (err instanceof KernelError) return err;
4259
+ const name2 = err?.name;
4260
+ return new KernelError(name2 === "NotAllowedError" ? "EACCES" : "EIO", lineOf(err));
4261
+ }
4262
+ function joined2(chunks, size) {
4263
+ if (chunks.length === 1) return chunks[0];
4264
+ const out = new Uint8Array(size);
4265
+ let at = 0;
4266
+ for (const chunk2 of chunks) {
4267
+ out.set(chunk2, at);
4268
+ at += chunk2.length;
4269
+ }
4270
+ return out;
4271
+ }
4272
+ async function interruptible(work, signal) {
4273
+ if (!signal) return work;
4274
+ if (signal.aborted) throw new KernelError("EINTR");
4275
+ let stop = () => {
4276
+ };
4277
+ const interrupted2 = new Promise((_, reject) => {
4278
+ stop = () => reject(new KernelError("EINTR"));
4279
+ signal.addEventListener("abort", stop, { once: true });
4280
+ });
4281
+ try {
4282
+ return await Promise.race([work, interrupted2]);
4283
+ } finally {
4284
+ signal.removeEventListener("abort", stop);
4285
+ }
4286
+ }
4287
+ var ClipboardChannel = class {
4288
+ stamp;
4289
+ access;
4290
+ broker;
4291
+ snapshot;
4292
+ taken;
4293
+ offset = 0;
4294
+ chunks = [];
4295
+ size = 0;
4296
+ wrote = false;
4297
+ failed = false;
4298
+ constructor(broker, access2, stamp) {
4299
+ this.broker = broker;
4300
+ this.access = access2;
4301
+ this.stamp = stamp;
4302
+ }
4303
+ get unread() {
4304
+ return this.taken === void 0 && this.snapshot === void 0;
4305
+ }
4306
+ get settled() {
4307
+ return this.taken !== void 0;
4308
+ }
4309
+ async read(max, signal) {
4310
+ if (this.access === "write") throw new KernelError("EBADF");
4311
+ if (!this.taken) {
4312
+ if (!this.snapshot) {
4313
+ const stamp = this.stamp;
4314
+ const asked = this.broker.ask(async (hook) => {
4315
+ const text = await hook.read(stamp);
4316
+ if (typeof text !== "string") throw new KernelError("EIO", "the clipboard gave no text");
4317
+ return encoder.encode(text);
4318
+ });
4319
+ this.snapshot = asked;
4320
+ asked.then(
4321
+ (bytes) => {
4322
+ if (this.snapshot === asked) this.taken = bytes;
4323
+ },
4324
+ () => {
4325
+ if (this.snapshot === asked) this.snapshot = void 0;
4326
+ }
4327
+ );
4328
+ }
4329
+ this.taken = await interruptible(this.snapshot, signal);
4330
+ }
4331
+ const out = this.taken.subarray(this.offset, this.offset + max);
4332
+ this.offset += out.length;
4333
+ return out;
4334
+ }
4335
+ write(bytes) {
4336
+ if (this.access === "read") throw new KernelError("EBADF");
4337
+ this.wrote = true;
4338
+ if (this.failed) throw new KernelError("EFBIG");
4339
+ if (this.size + bytes.length > CLIPBOARD_MAX_BYTES) {
4340
+ this.failed = true;
4341
+ this.chunks = [];
4342
+ this.size = 0;
4343
+ throw new KernelError("EFBIG");
4344
+ }
4345
+ if (bytes.length > 0) this.chunks.push(bytes.slice());
4346
+ this.size += bytes.length;
4347
+ return bytes.length;
4348
+ }
4349
+ poll() {
4350
+ return READY;
4351
+ }
4352
+ async close() {
4353
+ this.snapshot = void 0;
4354
+ this.taken = void 0;
4355
+ const commits = this.access === "write" || this.access === "both" && this.wrote;
4356
+ if (!commits || this.failed) return;
4357
+ const bytes = joined2(this.chunks, this.size);
4358
+ this.chunks = [];
4359
+ this.size = 0;
4360
+ let text;
4361
+ try {
4362
+ text = decoder.decode(bytes);
4363
+ } catch {
4364
+ throw new KernelError("EILSEQ", "the text is not valid UTF-8");
4365
+ }
4366
+ const stamp = this.stamp;
4367
+ await this.broker.ask(async (hook) => {
4368
+ await hook.write(text, stamp);
4369
+ });
4370
+ }
4371
+ };
4372
+ var ClipboardBroker = class {
4373
+ hook;
4374
+ users;
4375
+ timeoutMs;
4376
+ tagOf;
4377
+ constructor(hook, users, timeoutMs = CLIPBOARD_TIMEOUT_MS, tagOf = () => void 0) {
4378
+ this.hook = hook;
4379
+ this.users = users;
4380
+ this.timeoutMs = timeoutMs;
4381
+ this.tagOf = tagOf;
4382
+ }
4383
+ async ask(work) {
4384
+ const hook = this.hook;
4385
+ if (!hook) throw new KernelError("ENOTSUP", "no clipboard in this embedder");
4386
+ let timer;
4387
+ const late = new Promise((_, reject) => {
4388
+ timer = setTimeout(
4389
+ () => reject(new KernelError("EIO", "clipboard timed out")),
4390
+ this.timeoutMs
4391
+ );
4392
+ });
4393
+ const asked = Promise.resolve().then(() => work(hook)).catch((err) => {
4394
+ throw clipboardFailure(err);
4395
+ });
4396
+ try {
4397
+ return await Promise.race([asked, late]);
4398
+ } finally {
4399
+ clearTimeout(timer);
4400
+ }
4401
+ }
4402
+ open(access2, stamp) {
4403
+ const channel = new ClipboardChannel(this, access2, stamp);
4404
+ return new OpenFile({
4405
+ read: (max, signal) => channel.read(max, signal),
4406
+ write: async (bytes) => channel.write(bytes),
4407
+ poll: () => channel.poll(),
4408
+ close: () => channel.close(),
4409
+ clipboard: channel
4410
+ });
4411
+ }
4412
+ };
4413
+
4414
+ // src/clipboard-relay.ts
4415
+ function clipboardAsker(port) {
4416
+ const asks = /* @__PURE__ */ new Map();
4417
+ let next2 = 0;
4418
+ const ask = (op, stamp, text) => {
4419
+ const id2 = ++next2;
4420
+ const waiting = Promise.withResolvers();
4421
+ asks.set(id2, waiting);
4422
+ const timer = setTimeout(() => {
4423
+ if (asks.delete(id2)) waiting.reject(new Error("clipboard timed out"));
4424
+ }, CLIPBOARD_TIMEOUT_MS + 1e3);
4425
+ port.postMessage({ clipboardAsk: { id: id2, op, stamp, ...text !== void 0 ? { text } : {} } });
4426
+ return waiting.promise.finally(() => clearTimeout(timer));
4427
+ };
4428
+ const hook = {
4429
+ read: async (stamp) => await ask("read", stamp) ?? "",
4430
+ write: async (text, stamp) => {
4431
+ await ask("write", stamp, text);
4432
+ }
4433
+ };
4434
+ const answer = ({ id: id2, text, error }) => {
4435
+ const waiting = asks.get(id2);
4436
+ if (!waiting) return;
4437
+ asks.delete(id2);
4438
+ if (error) waiting.reject(Object.assign(new Error(error.message), { name: error.name }));
4439
+ else waiting.resolve(text);
4440
+ };
4441
+ return { hook, answer };
4442
+ }
4443
+
4193
4444
  // src/kernel/net/loopback-names.ts
4194
4445
  var HOST_LOOPBACK = "host.slicc.internal";
4195
4446
  var HOST_LOOPBACK_ADDRESS = [10, 0, 2, 2];
@@ -9191,20 +9442,20 @@ var SAB_DEFAULT_WINDOW_BYTES = 1024 * 1024;
9191
9442
  var SAB_MIN_BYTES = SAB_HEADER_BYTES + 4096;
9192
9443
  var SYNC_SAB_REQ_MSG = "sync-sab-req";
9193
9444
  var SYNC_SAB_NEXT_MSG = "sync-sab-next";
9194
- var encoder = new TextEncoder();
9195
- var decoder = new TextDecoder();
9445
+ var encoder2 = new TextEncoder();
9446
+ var decoder2 = new TextDecoder();
9196
9447
  function encodeSabResult(result) {
9197
9448
  if (!result.ok) {
9198
9449
  return {
9199
9450
  status: SAB_STATUS_ERRNO,
9200
- payload: encoder.encode(JSON.stringify({ errno: result.errno, message: result.message }))
9451
+ payload: encoder2.encode(JSON.stringify({ errno: result.errno, message: result.message }))
9201
9452
  };
9202
9453
  }
9203
9454
  switch (result.kind) {
9204
9455
  case "bytes":
9205
9456
  return { status: SAB_STATUS_BYTES, payload: result.bytes };
9206
9457
  case "json":
9207
- return { status: SAB_STATUS_JSON, payload: encoder.encode(JSON.stringify(result.json)) };
9458
+ return { status: SAB_STATUS_JSON, payload: encoder2.encode(JSON.stringify(result.json)) };
9208
9459
  default:
9209
9460
  return { status: SAB_STATUS_VOID, payload: new Uint8Array(0) };
9210
9461
  }
@@ -10678,7 +10929,7 @@ function defaultTermios() {
10678
10929
  c_cc: cCc
10679
10930
  };
10680
10931
  }
10681
- var encoder2 = new TextEncoder();
10932
+ var encoder3 = new TextEncoder();
10682
10933
  function isContinuation(byte) {
10683
10934
  return (byte & 192) === 128;
10684
10935
  }
@@ -10800,7 +11051,7 @@ var KernelTty = class {
10800
11051
  this.line.push(byte);
10801
11052
  this.readable.push(Uint8Array.from(this.line));
10802
11053
  this.line = [];
10803
- if (echo) this.output(encoder2.encode("\n"));
11054
+ if (echo) this.output(encoder3.encode("\n"));
10804
11055
  } else {
10805
11056
  this.line.push(byte);
10806
11057
  if (echo) this.echo(byte);
@@ -10825,7 +11076,7 @@ var KernelTty = class {
10825
11076
  const utf82 = (this.termios.c_iflag & IUTF8) !== 0;
10826
11077
  const cells = utf82 ? dropped2.filter((b) => !isContinuation(b)).length : dropped2.length;
10827
11078
  if (cells > 0 && this.termios.c_lflag & ECHO) {
10828
- this.output(encoder2.encode("\b \b".repeat(cells)), false);
11079
+ this.output(encoder3.encode("\b \b".repeat(cells)), false);
10829
11080
  }
10830
11081
  }
10831
11082
  echo(byte) {
@@ -11314,7 +11565,7 @@ async function socketSyscall(req, proc) {
11314
11565
  dialing?.catch(() => void 0);
11315
11566
  throw new KernelError("EINPROGRESS");
11316
11567
  }
11317
- if (dialing !== void 0) await interruptible(dialing, proc.blocking());
11568
+ if (dialing !== void 0) await interruptible2(dialing, proc.blocking());
11318
11569
  return ok();
11319
11570
  }
11320
11571
  case "sock-shutdown":
@@ -11338,7 +11589,7 @@ async function socketSyscall(req, proc) {
11338
11589
  }
11339
11590
  }
11340
11591
  }
11341
- function interruptible(dialing, signal) {
11592
+ function interruptible2(dialing, signal) {
11342
11593
  return new Promise((resolve, reject) => {
11343
11594
  const abort = () => reject(new KernelError("EINTR"));
11344
11595
  signal.addEventListener("abort", abort, { once: true });
@@ -12005,6 +12256,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
12005
12256
  "fd-tty-names",
12006
12257
  "fd-tty-owner",
12007
12258
  "fd-open-sealed",
12259
+ "fd-open-clipboard",
12008
12260
  "tty-get",
12009
12261
  "tty-set",
12010
12262
  "tty-winsz",
@@ -12247,7 +12499,7 @@ var WasmProcess = class {
12247
12499
  return await this.procSyscall(req, cancelled);
12248
12500
  } catch (e) {
12249
12501
  if (e instanceof KernelError || e instanceof SpawnError) {
12250
- return { ok: false, errno: e.code, message: e.code };
12502
+ return { ok: false, errno: e.code, message: e.detail ?? e.code };
12251
12503
  }
12252
12504
  const code = e?.code;
12253
12505
  if (typeof code === "string" && /^E[A-Z]+$/.test(code)) {
@@ -12290,6 +12542,7 @@ var WasmProcess = class {
12290
12542
  const read = req.peek ? file.peek : file.read;
12291
12543
  if (!read) throw new KernelError(req.peek && file.read ? "EOPNOTSUPP" : "EBADF");
12292
12544
  if (file.tty) this.checkForeground(file.tty);
12545
+ if (file.clipboard) return this.readClipboard(file.clipboard, req, cancelled);
12293
12546
  if (req.max <= 0) return new Uint8Array(0);
12294
12547
  if (file instanceof KernelSocket && file.unconnected) throw new KernelError("ENOTCONN");
12295
12548
  const ready2 = pollFile(file).readable;
@@ -12297,6 +12550,13 @@ var WasmProcess = class {
12297
12550
  const signal = ready2 ? this.interrupt.signal : this.blockingSignal(cancelled);
12298
12551
  return read.call(file, Math.max(0, Math.min(req.max, MAX_READ)), signal);
12299
12552
  }
12553
+ async readClipboard(channel, req, cancelled) {
12554
+ if (channel.access === "write") throw new KernelError("EBADF");
12555
+ if (req.max <= 0) return new Uint8Array(0);
12556
+ if (channel.unread) channel.stamp = await this.clipboardStamp();
12557
+ const signal = channel.settled ? this.interrupt.signal : this.blockingSignal(cancelled);
12558
+ return channel.read(Math.min(req.max, MAX_READ), signal);
12559
+ }
12300
12560
  async write(fd, body, nonblock = false, cancelled) {
12301
12561
  const file = this.fds.get(fd).file;
12302
12562
  if (file.sealed) return file.sealed.send(body, await this.sealedCaller(), this.gone.signal);
@@ -12400,6 +12660,36 @@ var WasmProcess = class {
12400
12660
  if (!broker) throw new KernelError("ENOENT");
12401
12661
  return this.fds.install(broker.open(), 3);
12402
12662
  }
12663
+ async openClipboard(flags2) {
12664
+ const broker = this.options.clipboard;
12665
+ if (!broker) throw new KernelError("ENOENT");
12666
+ const mode = flags2 & WRITE_ACCESS;
12667
+ const access2 = mode === 0 ? "read" : mode === 1 ? "write" : "both";
12668
+ return this.fds.install(broker.open(access2, await this.clipboardStamp()), 3);
12669
+ }
12670
+ async clipboardStamp() {
12671
+ const shown = await this.options.identity?.();
12672
+ const broker = this.options.clipboard;
12673
+ const tag = broker.tagOf(this.pid);
12674
+ return {
12675
+ pid: shown?.pid ?? this.pid,
12676
+ uid: this.cred.euid,
12677
+ gid: this.cred.egid,
12678
+ users: broker.users,
12679
+ ...tag !== void 0 ? { tag } : {}
12680
+ };
12681
+ }
12682
+ async stampClipboards() {
12683
+ if (!this.options.clipboard) return;
12684
+ const open = this.fds.numbers().filter((fd) => this.fds.get(fd).file.clipboard);
12685
+ if (open.length === 0) return;
12686
+ const stamp = await this.clipboardStamp().catch(() => void 0);
12687
+ if (!stamp) return;
12688
+ for (const fd of open) {
12689
+ const channel = this.fds.get(fd).file.clipboard;
12690
+ if (channel) channel.stamp = stamp;
12691
+ }
12692
+ }
12403
12693
  async sealedCaller() {
12404
12694
  const shown = await this.options.identity?.();
12405
12695
  const broker = this.options.sealed;
@@ -12474,10 +12764,13 @@ var WasmProcess = class {
12474
12764
  kind: "json",
12475
12765
  json: await this.write(req.fd, req.body, req.nonblock, cancelled)
12476
12766
  };
12477
- case "fd-close":
12767
+ case "fd-close": {
12478
12768
  this.options.locks?.closed(this.pid, req.fd);
12769
+ const channel = this.fds.get(req.fd).file.clipboard;
12770
+ if (channel) channel.stamp = await this.clipboardStamp();
12479
12771
  await Promise.resolve(this.fds.close(req.fd));
12480
12772
  return { ok: true, kind: "void" };
12773
+ }
12481
12774
  case "fd-pipe": {
12482
12775
  const pipe = openPipe();
12483
12776
  const read = this.fds.install(pipe.read, 3);
@@ -12535,6 +12828,8 @@ var WasmProcess = class {
12535
12828
  return { ok: true, kind: "json", json: this.options.jobs?.terminalNames() ?? [] };
12536
12829
  case "fd-open-sealed":
12537
12830
  return { ok: true, kind: "json", json: this.openSealed() };
12831
+ case "fd-open-clipboard":
12832
+ return { ok: true, kind: "json", json: await this.openClipboard(req.flags) };
12538
12833
  case "fd-tty-owner":
12539
12834
  return { ok: true, kind: "json", json: this.terminalByName(req.name).owner ?? null };
12540
12835
  case "fd-select": {
@@ -12986,6 +13281,7 @@ var WasmProcess = class {
12986
13281
  this.options.locks?.release(this.pid);
12987
13282
  this.asyncOps.close();
12988
13283
  for (const pid of this.children.pids()) this.options.onReap?.(pid);
13284
+ await this.stampClipboards();
12989
13285
  await this.fds.closeAll();
12990
13286
  if (!this.options.nodes) this.options.openFiles?.delete(this.nodes);
12991
13287
  await this.options.http?.closeAll();
@@ -13168,6 +13464,7 @@ function processOptions(opts, header3, hooks, created3) {
13168
13464
  ...inherited(opts),
13169
13465
  ...opts.owners ? { owners: true } : {},
13170
13466
  ...opts.sealed ? { sealed: opts.sealed } : {},
13467
+ ...opts.clipboard ? { clipboard: opts.clipboard } : {},
13171
13468
  ...opts.readonlyAt ? { readonlyAt: opts.readonlyAt } : {},
13172
13469
  ...opts.dac ? { guard: { dac: opts.dac, fs: opts.fs, created: created3 } } : {},
13173
13470
  ...opts.identity ? { identity: opts.identity } : {},
@@ -13672,14 +13969,14 @@ var SEALED_OPS = /* @__PURE__ */ new Set([
13672
13969
  "cancel",
13673
13970
  "materialize"
13674
13971
  ]);
13675
- var encoder3 = new TextEncoder();
13676
- var decoder2 = new TextDecoder("utf-8", { fatal: true });
13972
+ var encoder4 = new TextEncoder();
13973
+ var decoder3 = new TextDecoder("utf-8", { fatal: true });
13677
13974
  function parseSealed(bytes) {
13678
13975
  try {
13679
13976
  if (bytes.length > SEALED_MAX_BYTES) throw new KernelError("EMSGSIZE");
13680
13977
  let parsed2;
13681
13978
  try {
13682
- parsed2 = JSON.parse(decoder2.decode(bytes));
13979
+ parsed2 = JSON.parse(decoder3.decode(bytes));
13683
13980
  } catch {
13684
13981
  throw new KernelError("EINVAL");
13685
13982
  }
@@ -13705,13 +14002,13 @@ function serialized(reply) {
13705
14002
  const text = JSON.stringify(reply);
13706
14003
  plain = text === void 0 ? void 0 : JSON.parse(text);
13707
14004
  } catch (err) {
13708
- return JSON.stringify(errorReply("EIO", lineOf(err)));
14005
+ return JSON.stringify(errorReply("EIO", lineOf2(err)));
13709
14006
  }
13710
14007
  if (!isRecord(plain)) return JSON.stringify(errorReply("EIO", "the vault gave no reply"));
13711
14008
  const { v: _, ...rest2 } = plain;
13712
14009
  return JSON.stringify({ v: 1, ...rest2 });
13713
14010
  }
13714
- function lineOf(err) {
14011
+ function lineOf2(err) {
13715
14012
  const text = err instanceof Error ? err.message : String(err);
13716
14013
  return (text.split("\n")[0] ?? "").slice(0, 200);
13717
14014
  }
@@ -13741,7 +14038,7 @@ var SealedChannel = class {
13741
14038
  gone.removeEventListener("abort", withdraw);
13742
14039
  if (this.inflight !== flight) return;
13743
14040
  this.inflight = void 0;
13744
- if (reply) this.reply = encoder3.encode(serialized(reply));
14041
+ if (reply) this.reply = encoder4.encode(serialized(reply));
13745
14042
  });
13746
14043
  this.inflight = flight;
13747
14044
  return length2;
@@ -13755,7 +14052,7 @@ var SealedChannel = class {
13755
14052
  if (signal.aborted) return void 0;
13756
14053
  const asked = Promise.resolve().then(() => hook(req, { signal })).then(
13757
14054
  (reply2) => isRecord(reply2) ? reply2 : errorReply("EIO", "the vault gave no reply"),
13758
- (err) => errorReply("EIO", lineOf(err))
14055
+ (err) => errorReply("EIO", lineOf2(err))
13759
14056
  );
13760
14057
  const reply = await Promise.race([asked, stopped]);
13761
14058
  if (!signal.aborted) return reply;
@@ -14478,13 +14775,13 @@ var HostfsClient = class {
14478
14775
  const response = await this.send("/api/hostfs/watch", { method: "POST", signal });
14479
14776
  onOpen();
14480
14777
  const reader = response.body.getReader();
14481
- const decoder3 = new TextDecoder();
14778
+ const decoder4 = new TextDecoder();
14482
14779
  let buffered = "";
14483
14780
  try {
14484
14781
  for (; ; ) {
14485
14782
  const { done: done4, value } = await reader.read();
14486
14783
  if (done4) return;
14487
- buffered += decoder3.decode(value, { stream: true });
14784
+ buffered += decoder4.decode(value, { stream: true });
14488
14785
  let end = buffered.indexOf("\n");
14489
14786
  while (end >= 0) {
14490
14787
  const line = buffered.slice(0, end).trim();
@@ -15863,7 +16160,7 @@ var LAYOUT = [
15863
16160
  ];
15864
16161
  var LOAD_SAMPLE_MS = 5e3;
15865
16162
  var LOAD_WINDOWS_MS = [6e4, 3e5, 9e5];
15866
- var encoder4 = new TextEncoder();
16163
+ var encoder5 = new TextEncoder();
15867
16164
  var USERS_MODES = ["off", "report", "enforce"];
15868
16165
  function usersMode(mode = "off") {
15869
16166
  if (USERS_MODES.includes(mode)) return mode;
@@ -16020,6 +16317,7 @@ var Launcher = class {
16020
16317
  usersMode;
16021
16318
  dac;
16022
16319
  sealed;
16320
+ clipboard;
16023
16321
  rootFs;
16024
16322
  fileWatches;
16025
16323
  nextPid = 1e3;
@@ -16030,6 +16328,12 @@ var Launcher = class {
16030
16328
  this.rootFs = options.fs;
16031
16329
  this.dac = this.usersMode === "off" ? void 0 : new Dac(this.usersMode, options.onDenial);
16032
16330
  if (this.dac) this.dac.tagOf = (pid) => this.tagOf(pid);
16331
+ this.clipboard = new ClipboardBroker(
16332
+ options.clipboard,
16333
+ this.usersMode,
16334
+ void 0,
16335
+ (pid) => this.tagOf(pid)
16336
+ );
16033
16337
  this.sealed = new SealedBroker(
16034
16338
  options.sealed,
16035
16339
  this.usersMode,
@@ -16446,6 +16750,7 @@ var Launcher = class {
16446
16750
  cred: req.cred,
16447
16751
  ...this.owners ? { owners: true } : {},
16448
16752
  sealed: this.sealed,
16753
+ clipboard: this.clipboard,
16449
16754
  ...this.dac ? { dac: this.dac } : {},
16450
16755
  program: req.program,
16451
16756
  argv0: req.argv0,
@@ -17206,7 +17511,7 @@ var Launcher = class {
17206
17511
  },
17207
17512
  account
17208
17513
  );
17209
- const report = (message3) => options.onData(encoder4.encode(`${message3}\r
17514
+ const report = (message3) => options.onData(encoder5.encode(`${message3}\r
17210
17515
  `));
17211
17516
  const handle = await this.launch(planned, {
17212
17517
  env,
@@ -17243,7 +17548,7 @@ var Launcher = class {
17243
17548
  };
17244
17549
  const stdout = collect(out, options.onStdout);
17245
17550
  const stderr = collect(err, options.onStderr);
17246
- const report = (message3) => stderr(encoder4.encode(`${message3}
17551
+ const report = (message3) => stderr(encoder5.encode(`${message3}
17247
17552
  `));
17248
17553
  if (!planned) {
17249
17554
  report(`${file}: command not found`);
@@ -17431,13 +17736,14 @@ function usersOp(l, req) {
17431
17736
  if (req.op === "groups-remove-member") return l.users.removeMember(req.group, req.user);
17432
17737
  return l.users.list();
17433
17738
  }
17434
- function hostOptions(req, port, sealed) {
17739
+ function hostOptions(req, port, sealed, clipboard) {
17435
17740
  return {
17436
17741
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
17437
17742
  ...req.users !== void 0 ? { users: req.users } : {},
17438
17743
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
17439
17744
  ...req.protect ? { protect: protectInit(req.protect) } : {},
17440
- ...req.sealed ? { sealed } : {}
17745
+ ...req.sealed ? { sealed } : {},
17746
+ ...req.clipboard ? { clipboard } : {}
17441
17747
  };
17442
17748
  }
17443
17749
  function policyAsker(port) {
@@ -17467,6 +17773,7 @@ function serveKernel(port, deps) {
17467
17773
  const reply = (id2, body, transfer) => port.postMessage({ id: id2, ...body }, transfer);
17468
17774
  const clients = /* @__PURE__ */ new Set();
17469
17775
  const sealed = sealedAsker(port);
17776
+ const clipboard = clipboardAsker(port);
17470
17777
  const approvals = sudoApprovals(port);
17471
17778
  const locks = deps.locks === null ? void 0 : deps.locks ?? locksOf();
17472
17779
  let holding;
@@ -17531,7 +17838,7 @@ function serveKernel(port, deps) {
17531
17838
  ...deps.createDriverWorker ? { createDriverWorker: deps.createDriverWorker } : {},
17532
17839
  ...req.modules ? { modules: req.modules } : {},
17533
17840
  ...req.env ? { env: req.env } : {},
17534
- ...hostOptions(req, port, sealed.hook),
17841
+ ...hostOptions(req, port, sealed.hook, clipboard.hook),
17535
17842
  ...req.approve ? { approve: approvals.ask } : {},
17536
17843
  ...net,
17537
17844
  ...media ? { media } : {},
@@ -17600,6 +17907,8 @@ function serveKernel(port, deps) {
17600
17907
  port.addEventListener("message", (event) => {
17601
17908
  const sealedReply = event.data.sealedReply;
17602
17909
  if (sealedReply) return sealed.answer(sealedReply);
17910
+ const clipboardReply = event.data.clipboardReply;
17911
+ if (clipboardReply) return clipboard.answer(clipboardReply);
17603
17912
  const grant = event.data.hostfsGrant;
17604
17913
  if (grant) return granted2(grant);
17605
17914
  if (allowed(event.data)) return;