koishi-plugin-ishiki-workspace 0.0.3 → 0.0.5

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.
@@ -0,0 +1,5 @@
1
+ import type { BashOptions } from "just-bash";
2
+ import type { WorkspaceLayout } from "./shell.js";
3
+ type ExecutionLimits = NonNullable<BashOptions["executionLimits"]>;
4
+ export declare function sandboxExecutionLimits(layout: Pick<WorkspaceLayout, "timeoutMs" | "maxExecutionOutputBytes">): ExecutionLimits;
5
+ export {};
package/lib/index.cjs CHANGED
@@ -48,7 +48,7 @@ var WorkspaceConfig = import_koishi.Schema.object({
48
48
  network: import_koishi.Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
49
49
  javascript: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
50
50
  python: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
51
- skills: import_koishi.Schema.array(import_koishi.Schema.string()).description("额外技能目录,相对 profile 目录解析;与数据根的 skills 目录合并,同名以这里优先").default([]),
51
+ skills: import_koishi.Schema.array(import_koishi.Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([]),
52
52
  mounts: import_koishi.Schema.array(import_koishi.Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
53
53
  });
54
54
 
@@ -250,8 +250,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
250
250
  );
251
251
  var SANDBOX_INSTRUCTIONS = new import_koishi_plugin_ishiki.Template(
252
252
  `## Workspace
253
- You read and write files in a per-instance sandbox, not on the host: commands are interpreted by JavaScript, only the mounted paths below exist, and host paths and commands are invisible.
253
+ You read and write files in a per-instance sandbox, not on the host: commands run in just-bash, only the mounted paths below exist, and host paths and commands are invisible.
254
254
  The command set is not the host's.
255
+ Runtime details and API gaps are recorded in the bundled sandbox-environment skill; read it before writing JavaScript, TypeScript, Python, SQLite, or network-heavy commands.
255
256
  Current working directory: {{ cwd }}
256
257
  Mounts:
257
258
  - {{ homeMount }}: read-only, this instance's data directory (writes fail)
@@ -269,11 +270,12 @@ Network: {{ network }}
269
270
  Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
270
271
  Executor output safety limit: {{ maxExecutionOutputBytes }} bytes. Inside codemode you receive the raw output; the runtime handles model-visible previews and archives. Return structured results instead of stringifying or slicing them yourself.
271
272
  Shell state does not persist between calls: cd, aliases, functions, and exported variables are not retained. Put a directory change on the same line (cd <dir> && <command>).
273
+ /tmp belongs to this sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
272
274
  {% if javascript %}
273
- js-exec is available: run JavaScript / TypeScript in QuickJS. Some node modules can be required or imported; only files inside the sandbox are accessible.
275
+ js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
274
276
  {% endif %}
275
277
  {% if python %}
276
- python3 and python are available: CPython compiled to WebAssembly with the full standard library. The first call waits for it to load; reads and writes target this sandbox.
278
+ python3 and python are available: CPython compiled to WebAssembly. Most of the standard library is available, but subprocess and the sqlite3 module are absent. The first call waits for it to load; reads and writes target this sandbox.
277
279
  {% endif %}
278
280
  You can also read a known file through read's workspace:///<sandbox-path> URL (e.g. workspace:///home/workspace/a.txt): virtual files, persistent directories, and mounts all resolve to the same sandbox file.
279
281
  Use read_file to read a known file, write_file to replace a file, edit_file for a small edit, and bash for listing, searching, and pipelines.`
@@ -355,7 +357,408 @@ var WorkspaceScheme = class extends import_koishi_plugin_ishiki2.FileScheme {
355
357
 
356
358
  // src/sandbox.ts
357
359
  var import_node_path4 = __toESM(require("node:path"));
358
- var import_just_bash = require("just-bash");
360
+
361
+ // src/execution-limits.ts
362
+ function sandboxExecutionLimits(layout) {
363
+ return {
364
+ maxOutputSize: layout.maxExecutionOutputBytes,
365
+ maxExecutionTimeMs: layout.timeoutMs,
366
+ maxSqliteTimeoutMs: layout.timeoutMs,
367
+ maxPythonTimeoutMs: layout.timeoutMs,
368
+ maxJsTimeoutMs: layout.timeoutMs
369
+ };
370
+ }
371
+ __name(sandboxExecutionLimits, "sandboxExecutionLimits");
372
+
373
+ // src/js-bootstrap.ts
374
+ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
375
+ (function () {
376
+ var hostKey = Object.getOwnPropertyNames(globalThis).filter(function (name) {
377
+ return name.indexOf('__jbHost_') === 0;
378
+ })[0];
379
+ if (!hostKey) throw new Error('js-exec host bridge is unavailable');
380
+ var host = globalThis[hostKey];
381
+ var fs = globalThis.fs;
382
+ function unwrap(result) {
383
+ if (!result || result.ok !== true) throw new Error(result && result.error || 'Host operation failed');
384
+ return result.value;
385
+ }
386
+ var raw = {};
387
+ var names = [
388
+ 'readFileBuffer', 'readFileSync', 'writeFileSync', 'appendFileSync',
389
+ 'statSync', 'lstatSync', 'readdirSync', 'mkdirSync', 'rmSync',
390
+ 'symlinkSync', 'readlinkSync', 'chmodSync', 'realpathSync',
391
+ 'renameSync', 'copyFileSync'
392
+ ];
393
+ for (var index = 0; index < names.length; index++) raw[names[index]] = fs[names[index]];
394
+
395
+ var errnos = {
396
+ EPERM: -1,
397
+ ENOENT: -2,
398
+ EIO: -5,
399
+ EACCES: -13,
400
+ EEXIST: -17,
401
+ ENOTDIR: -20,
402
+ EISDIR: -21,
403
+ EINVAL: -22,
404
+ ENOSPC: -28,
405
+ EROFS: -30,
406
+ ENAMETOOLONG: -36,
407
+ ENOTEMPTY: -39,
408
+ ELOOP: -40
409
+ };
410
+
411
+ var constants = {
412
+ F_OK: 0,
413
+ R_OK: 4,
414
+ W_OK: 2,
415
+ X_OK: 1,
416
+ COPYFILE_EXCL: 1,
417
+ COPYFILE_FICLONE: 2,
418
+ COPYFILE_FICLONE_FORCE: 4,
419
+ S_IFMT: 61440,
420
+ S_IFREG: 32768,
421
+ S_IFDIR: 16384,
422
+ S_IFLNK: 40960,
423
+ O_RDONLY: 0,
424
+ O_WRONLY: 1,
425
+ O_RDWR: 2,
426
+ O_CREAT: 64,
427
+ O_EXCL: 128,
428
+ O_TRUNC: 512,
429
+ O_APPEND: 1024
430
+ };
431
+
432
+ function failure(error, syscall, target, destination) {
433
+ if (!(error instanceof Error)) error = new Error(String(error));
434
+ var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
435
+ if (matched) {
436
+ error.code = matched[1];
437
+ if (errnos[error.code] !== undefined) error.errno = errnos[error.code];
438
+ }
439
+ error.syscall = syscall;
440
+ if (target !== undefined) {
441
+ error.path = String(target);
442
+ error.message = error.message.replace(/'[^']*'(?=\s*$)/, "'" + String(target).replace(/'/g, "\\'") + "'");
443
+ }
444
+ if (destination !== undefined) error.dest = String(destination);
445
+ return error;
446
+ }
447
+
448
+ function filePath(value) {
449
+ if (
450
+ typeof value !== 'object' ||
451
+ value === null ||
452
+ typeof value.href !== 'string' ||
453
+ typeof value.protocol !== 'string'
454
+ ) return value;
455
+ if (value.protocol !== 'file:') {
456
+ throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
457
+ }
458
+ if (value.hostname && value.hostname !== 'localhost') {
459
+ throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
460
+ }
461
+ return decodeURIComponent(value.pathname);
462
+ }
463
+
464
+ function bridge(hostName, syscall, target, args, destination) {
465
+ try {
466
+ var operation = host[hostName];
467
+ if (args.length === 1) return unwrap(operation(args[0]));
468
+ if (args.length === 2) return unwrap(operation(args[0], args[1]));
469
+ if (args.length === 3) return unwrap(operation(args[0], args[1], args[2]));
470
+ return unwrap(operation(args[0], args[1], args[2], args[3]));
471
+ } catch (error) {
472
+ throw failure(error, syscall, target, destination);
473
+ }
474
+ }
475
+
476
+ function bytes(value) {
477
+ if (value && value._data instanceof Uint8Array) return Array.from(value._data);
478
+ if (value && Array.isArray(value._data)) return value._data.slice();
479
+ if (Array.isArray(value)) return value.slice();
480
+ if (value instanceof Uint8Array) return Array.from(value);
481
+ return String(value);
482
+ }
483
+
484
+ function Stats() {}
485
+ function Dirent() {}
486
+
487
+ function stats(value) {
488
+ var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
489
+ var size = Number(value.size);
490
+ var result = Object.create(Stats.prototype);
491
+ result.dev = 0;
492
+ result.ino = 0;
493
+ result.mode = Number(value.mode);
494
+ result.nlink = value.isDirectory ? 2 : 1;
495
+ result.uid = 1000;
496
+ result.gid = 1000;
497
+ result.rdev = 0;
498
+ result.size = size;
499
+ result.blksize = 4096;
500
+ result.blocks = Math.ceil(size / 512);
501
+ result.atime = mtime;
502
+ result.mtime = mtime;
503
+ result.ctime = mtime;
504
+ result.birthtime = mtime;
505
+ result.atimeMs = mtime.getTime();
506
+ result.mtimeMs = mtime.getTime();
507
+ result.ctimeMs = mtime.getTime();
508
+ result.birthtimeMs = mtime.getTime();
509
+ result.isFile = function () { return value.isFile; };
510
+ result.isDirectory = function () { return value.isDirectory; };
511
+ result.isSymbolicLink = function () { return value.isSymbolicLink; };
512
+ result.isBlockDevice = function () { return false; };
513
+ result.isCharacterDevice = function () { return false; };
514
+ result.isFIFO = function () { return false; };
515
+ result.isSocket = function () { return false; };
516
+ return result;
517
+ }
518
+
519
+ function dirent(name, parentPath, value) {
520
+ var result = Object.create(Dirent.prototype);
521
+ result.name = String(name);
522
+ result.parentPath = String(parentPath);
523
+ result.path = String(parentPath);
524
+ result.isFile = function () { return value.isFile; };
525
+ result.isDirectory = function () { return value.isDirectory; };
526
+ result.isSymbolicLink = function () { return value.isSymbolicLink; };
527
+ result.isBlockDevice = function () { return false; };
528
+ result.isCharacterDevice = function () { return false; };
529
+ result.isFIFO = function () { return false; };
530
+ result.isSocket = function () { return false; };
531
+ return result;
532
+ }
533
+
534
+ function childPath(parent, name) {
535
+ return parent.charAt(parent.length - 1) === '/' ? parent + name : parent + '/' + name;
536
+ }
537
+
538
+ function readStats(path, follow) {
539
+ var target = filePath(path);
540
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
541
+ }
542
+
543
+ function collect(path, relative, options, output) {
544
+ var listing = bridge('fsReaddir', 'scandir', path, [path]);
545
+ for (var index = 0; index < listing.length; index++) {
546
+ var name = listing[index];
547
+ var nested = relative === '' ? name : relative + '/' + name;
548
+ var full = childPath(path, name);
549
+ var value = readStats(full, false);
550
+ output.push(options.withFileTypes ? dirent(name, path, value) : nested);
551
+ if (options.recursive && value.isDirectory) collect(full, nested, options, output);
552
+ }
553
+ }
554
+
555
+ function readdir(path, options) {
556
+ var target = filePath(path);
557
+ if (typeof options === 'string') options = { encoding: options };
558
+ options = options || {};
559
+ var output = [];
560
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
561
+ if (options.encoding === 'buffer') return output.map(function (entry) {
562
+ return typeof entry === 'string' ? Buffer.from(entry) : entry;
563
+ });
564
+ return output;
565
+ }
566
+
567
+ function firstCreated(path) {
568
+ var text = String(filePath(path));
569
+ var absolute = text.charAt(0) === '/';
570
+ var parts = text.split('/');
571
+ var current = '';
572
+ var found;
573
+ for (var index = 0; index < parts.length; index++) {
574
+ var part = parts[index];
575
+ if (part === '' || part === '.') continue;
576
+ if (current === '') current = absolute ? '/' + part : part;
577
+ else current = childPath(current, part);
578
+ if (!fs.existsSync(current)) {
579
+ found = current;
580
+ break;
581
+ }
582
+ }
583
+ return found;
584
+ }
585
+
586
+ fs.readFileBuffer = function (path) {
587
+ var target = filePath(path);
588
+ try {
589
+ return Buffer.from(raw.readFileBuffer(target));
590
+ } catch (error) {
591
+ throw failure(error, 'open', target);
592
+ }
593
+ };
594
+ fs.readFileSync = function (path, options) {
595
+ var target = filePath(path);
596
+ try {
597
+ return raw.readFileSync(target, options);
598
+ } catch (error) {
599
+ throw failure(error, 'open', target);
600
+ }
601
+ };
602
+ fs.writeFileSync = function (path, data) {
603
+ var target = filePath(path);
604
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
605
+ };
606
+ fs.appendFileSync = function (path, data) {
607
+ var target = filePath(path);
608
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
609
+ };
610
+ fs.statSync = function (path) {
611
+ return stats(readStats(path, true));
612
+ };
613
+ fs.lstatSync = function (path) {
614
+ return stats(readStats(path, false));
615
+ };
616
+ fs.readdirSync = readdir;
617
+ fs.mkdirSync = function (path, options) {
618
+ var target = filePath(path);
619
+ var recursive = Boolean(options && typeof options === 'object' && options.recursive);
620
+ var created = recursive ? firstCreated(target) : undefined;
621
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
622
+ return created;
623
+ };
624
+ fs.rmSync = function (path, options) {
625
+ var target = filePath(path);
626
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
627
+ };
628
+ fs.existsSync = function (path) {
629
+ var target = filePath(path);
630
+ try {
631
+ return bridge('fsExists', 'access', target, [target]);
632
+ } catch (error) {
633
+ return false;
634
+ }
635
+ };
636
+ fs.accessSync = function (path, mode) {
637
+ var target = filePath(path);
638
+ if (!bridge('fsExists', 'access', target, [target])) {
639
+ throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
640
+ }
641
+ var flags = mode === undefined ? 0 : Number(mode);
642
+ if ((flags & ~7) !== 0) {
643
+ throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
644
+ }
645
+ if (flags === 0) return;
646
+ var permissions = stats(readStats(target, true)).mode & 511;
647
+ var required = 0;
648
+ if (flags & constants.R_OK) required |= 256;
649
+ if (flags & constants.W_OK) required |= 128;
650
+ if (flags & constants.X_OK) required |= 64;
651
+ if ((permissions & required) !== required) {
652
+ throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
653
+ }
654
+ };
655
+ fs.symlinkSync = function (target, path) {
656
+ var link = filePath(path);
657
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
658
+ };
659
+ fs.readlinkSync = function (path) {
660
+ var target = filePath(path);
661
+ return bridge('fsReadlink', 'readlink', target, [target]);
662
+ };
663
+ fs.chmodSync = function (path, mode) {
664
+ var target = filePath(path);
665
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
666
+ };
667
+ fs.realpathSync = function (path) {
668
+ var target = filePath(path);
669
+ return bridge('fsRealpath', 'realpath', target, [target]);
670
+ };
671
+ fs.renameSync = function (from, to) {
672
+ var source = filePath(from);
673
+ var destination = filePath(to);
674
+ bridge('fsRename', 'rename', source, [source, destination], destination);
675
+ };
676
+ fs.copyFileSync = function (from, to, mode) {
677
+ var source = filePath(from);
678
+ var destination = filePath(to);
679
+ if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
680
+ throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
681
+ }
682
+ bridge('fsCopy', 'copyfile', source, [source, destination], destination);
683
+ };
684
+ fs.unlinkSync = function (path) {
685
+ var target = filePath(path);
686
+ bridge('fsRm', 'unlink', target, [target, false, false]);
687
+ };
688
+ fs.rmdirSync = function (path, options) {
689
+ var target = filePath(path);
690
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
691
+ };
692
+ fs.constants = constants;
693
+ fs.Stats = Stats;
694
+ fs.Dirent = Dirent;
695
+
696
+ var callbackNames = [
697
+ ['readFile', fs.readFileSync],
698
+ ['writeFile', fs.writeFileSync],
699
+ ['appendFile', fs.appendFileSync],
700
+ ['stat', fs.statSync],
701
+ ['lstat', fs.lstatSync],
702
+ ['readdir', fs.readdirSync],
703
+ ['mkdir', fs.mkdirSync],
704
+ ['rm', fs.rmSync],
705
+ ['symlink', fs.symlinkSync],
706
+ ['readlink', fs.readlinkSync],
707
+ ['chmod', fs.chmodSync],
708
+ ['realpath', fs.realpathSync],
709
+ ['rename', fs.renameSync],
710
+ ['copyFile', fs.copyFileSync],
711
+ ['unlink', fs.unlinkSync],
712
+ ['rmdir', fs.rmdirSync]
713
+ ];
714
+ function callbackify(name, sync) {
715
+ fs[name] = function () {
716
+ var args = Array.prototype.slice.call(arguments);
717
+ var callback = args.pop();
718
+ if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
719
+ Promise.resolve().then(function () {
720
+ return sync.apply(fs, args);
721
+ }).then(function (value) {
722
+ callback(null, value);
723
+ }, function (error) {
724
+ callback(error);
725
+ });
726
+ };
727
+ }
728
+ for (var index = 0; index < callbackNames.length; index++) callbackify(callbackNames[index][0], callbackNames[index][1]);
729
+ fs.exists = function (path, callback) {
730
+ if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
731
+ Promise.resolve().then(function () { callback(fs.existsSync(path)); });
732
+ };
733
+
734
+ var promiseNames = [
735
+ 'readFile', 'writeFile', 'appendFile', 'stat', 'lstat', 'readdir',
736
+ 'mkdir', 'rm', 'symlink', 'readlink', 'chmod', 'realpath',
737
+ 'rename', 'copyFile', 'unlink', 'rmdir'
738
+ ];
739
+ fs.promises = {};
740
+ for (var index = 0; index < promiseNames.length; index++) (function (name) {
741
+ var sync = fs[name + 'Sync'];
742
+ fs.promises[name] = function () {
743
+ var args = arguments;
744
+ return new Promise(function (resolve, reject) {
745
+ Promise.resolve().then(function () {
746
+ return sync.apply(fs, args);
747
+ }).then(resolve, reject);
748
+ });
749
+ };
750
+ })(promiseNames[index]);
751
+ fs.promises.access = function (path, mode) {
752
+ return new Promise(function (resolve, reject) {
753
+ Promise.resolve().then(function () {
754
+ fs.accessSync(path, mode);
755
+ }).then(resolve, reject);
756
+ });
757
+ };
758
+
759
+ globalThis.fs = fs;
760
+ })();
761
+ `;
359
762
 
360
763
  // src/resource-fs.ts
361
764
  var EROFS = /* @__PURE__ */ __name(() => new Error("EROFS: resource mounts are read-only"), "EROFS");
@@ -513,8 +916,9 @@ function traversable(stat2) {
513
916
  }
514
917
  __name(traversable, "traversable");
515
918
  async function createSandbox(layout, options) {
919
+ const { Bash, InMemoryFs, MountableFs, OverlayFs, ReadWriteFs } = await import("just-bash");
516
920
  const { home } = options;
517
- class Writable extends import_just_bash.ReadWriteFs {
921
+ class Writable extends ReadWriteFs {
518
922
  static {
519
923
  __name(this, "Writable");
520
924
  }
@@ -525,36 +929,35 @@ async function createSandbox(layout, options) {
525
929
  return traversable(await super.lstat(path7));
526
930
  }
527
931
  }
528
- const homeView = new import_just_bash.MountableFs({
529
- base: new import_just_bash.InMemoryFs(),
530
- mounts: [{ mountPoint: HOME_MOUNT, filesystem: new import_just_bash.OverlayFs({ root: home, mountPoint: "/", readOnly: true }) }]
932
+ const homeView = new MountableFs({
933
+ base: new InMemoryFs(),
934
+ mounts: [{ mountPoint: HOME_MOUNT, filesystem: new OverlayFs({ root: home, mountPoint: "/", readOnly: true }) }]
531
935
  });
532
- const fs3 = new import_just_bash.MountableFs({
936
+ const fs3 = new MountableFs({
533
937
  base: homeView,
534
938
  mounts: [
535
939
  { mountPoint: WORKSPACE_MOUNT, filesystem: new Writable({ root: import_node_path4.default.join(home, "workspace") }) },
536
940
  ...options.resources ? [{ mountPoint: ISHIKI_MOUNT, filesystem: new ResourceFs(options.resources) }] : [],
537
941
  ...layout.mounts.map((mount) => ({
538
942
  mountPoint: mount.target,
539
- filesystem: mount.readOnly ? new import_just_bash.OverlayFs({ root: mount.source, mountPoint: "/", readOnly: true }) : new Writable({ root: mount.source })
943
+ filesystem: mount.readOnly ? new OverlayFs({ root: mount.source, mountPoint: "/", readOnly: true }) : new Writable({ root: mount.source })
540
944
  }))
541
945
  ]
542
946
  });
543
- const bash = new import_just_bash.Bash({
947
+ const bash = new Bash({
544
948
  fs: fs3,
545
949
  cwd: layout.cwd,
546
950
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
547
- javascript: layout.javascript,
951
+ javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
548
952
  python: layout.python,
549
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
953
+ executionLimits: sandboxExecutionLimits(layout)
550
954
  });
551
955
  return {
552
956
  ...layout,
553
957
  fs: fs3,
554
958
  async exec(command, signal) {
555
- const timeout = AbortSignal.timeout(layout.timeoutMs);
556
959
  try {
557
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
960
+ const result = await bash.exec(command, { signal });
558
961
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
559
962
  } catch (error) {
560
963
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -582,8 +985,12 @@ var import_node_path6 = __toESM(require("node:path"));
582
985
  // src/skills.ts
583
986
  var import_node_fs3 = require("node:fs");
584
987
  var import_node_path5 = __toESM(require("node:path"));
988
+ var import_node_url = require("node:url");
585
989
  var import_yaml = require("yaml");
990
+ var import_meta = {};
586
991
  var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
992
+ var moduleDirectory = typeof __dirname === "string" ? __dirname : import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
993
+ var BUILTIN_SKILLS_DIRECTORY = import_node_path5.default.resolve(moduleDirectory, "..", "resources", "skills");
587
994
  function discoverSkills(roots, logger) {
588
995
  const found = /* @__PURE__ */ new Map();
589
996
  for (const root of roots) {
@@ -658,7 +1065,10 @@ function defaultShell(configured) {
658
1065
  __name(defaultShell, "defaultShell");
659
1066
  function resolveLayout(options) {
660
1067
  const { config } = options;
661
- const skills = discoverSkills([import_node_path6.default.join(options.dataPath, "skills"), ...config.skills.map((dir) => import_node_path6.default.resolve(options.root, dir))], options.logger);
1068
+ const skills = discoverSkills(
1069
+ [BUILTIN_SKILLS_DIRECTORY, import_node_path6.default.join(options.dataPath, "skills"), ...config.skills.map((dir) => import_node_path6.default.resolve(options.root, dir))],
1070
+ options.logger
1071
+ );
662
1072
  const workspace = import_node_path6.default.join(options.home, "workspace");
663
1073
  (0, import_node_fs4.mkdirSync)(workspace, { recursive: true });
664
1074
  const backend = config.backend;
@@ -698,6 +1108,9 @@ Run a bash command inside the sandbox.
698
1108
  The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
699
1109
  {% endif %}
700
1110
  Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
1111
+ {% if not native %}
1112
+ /tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
1113
+ {% endif %}
701
1114
  {% if gitBashPaths %}
702
1115
  Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
703
1116
  Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
@@ -761,7 +1174,7 @@ function createWorkspaceTools(sandbox) {
761
1174
  ${output.stdout}`);
762
1175
  if (output.stderr.length > 0) sections.push(`stderr:
763
1176
  ${output.stderr}`);
764
- return { type: "text", value: sections.join("\n\n") };
1177
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
765
1178
  }, "toModelOutput"),
766
1179
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
767
1180
  }),
@@ -863,22 +1276,22 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
863
1276
  this.logger.level = config.logLevel;
864
1277
  this.dataPath = ctx.ishiki.dataPath;
865
1278
  }
866
- [import_koishi2.Service.invoke](config, context) {
1279
+ [import_koishi2.Service.invoke](host, config) {
867
1280
  const shellOptions = {
868
1281
  config,
869
- home: context.home,
870
- root: context.root,
1282
+ home: host.home,
1283
+ root: host.root,
871
1284
  dataPath: this.dataPath,
872
1285
  logger: this.logger,
873
- resources: context.resources
1286
+ resources: host.resources
874
1287
  };
875
1288
  const layout = resolveLayout(shellOptions);
876
1289
  let shell;
877
1290
  let tools;
878
1291
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
879
1292
  const disposers = [];
880
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
881
- disposers.push(context.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1293
+ disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
1294
+ disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
882
1295
  return {
883
1296
  name: "ishiki.workspace",
884
1297
  extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
package/lib/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { Schema, Service, type Context } from "koishi";
2
- import { Extension, type ExtensionContext, type ToolSet } from "koishi-plugin-ishiki";
2
+ import { Extension, type ExtensionHost, type ToolSet } from "koishi-plugin-ishiki";
3
3
  import { WorkspaceConfig } from "./config.js";
4
4
  declare class WorkspaceExtension extends Extension<WorkspaceConfig> {
5
5
  static inject: string[];
6
6
  readonly Schema: Schema<WorkspaceConfig>;
7
7
  private readonly dataPath;
8
8
  constructor(ctx: Context, config: WorkspaceExtension.Config);
9
- [Service.invoke](config: WorkspaceConfig, context: ExtensionContext): {
9
+ [Service.invoke](host: ExtensionHost, config: WorkspaceConfig): {
10
10
  name: string;
11
11
  extendTools: () => Promise<ToolSet>;
12
12
  extendInstructions: () => Promise<string>;
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ var WorkspaceConfig = Schema.object({
16
16
  network: Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
17
17
  javascript: Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
18
18
  python: Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
19
- skills: Schema.array(Schema.string()).description("额外技能目录,相对 profile 目录解析;与数据根的 skills 目录合并,同名以这里优先").default([]),
19
+ skills: Schema.array(Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([]),
20
20
  mounts: Schema.array(Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
21
21
  });
22
22
 
@@ -218,8 +218,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
218
218
  );
219
219
  var SANDBOX_INSTRUCTIONS = new Template(
220
220
  `## Workspace
221
- You read and write files in a per-instance sandbox, not on the host: commands are interpreted by JavaScript, only the mounted paths below exist, and host paths and commands are invisible.
221
+ You read and write files in a per-instance sandbox, not on the host: commands run in just-bash, only the mounted paths below exist, and host paths and commands are invisible.
222
222
  The command set is not the host's.
223
+ Runtime details and API gaps are recorded in the bundled sandbox-environment skill; read it before writing JavaScript, TypeScript, Python, SQLite, or network-heavy commands.
223
224
  Current working directory: {{ cwd }}
224
225
  Mounts:
225
226
  - {{ homeMount }}: read-only, this instance's data directory (writes fail)
@@ -237,11 +238,12 @@ Network: {{ network }}
237
238
  Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
238
239
  Executor output safety limit: {{ maxExecutionOutputBytes }} bytes. Inside codemode you receive the raw output; the runtime handles model-visible previews and archives. Return structured results instead of stringifying or slicing them yourself.
239
240
  Shell state does not persist between calls: cd, aliases, functions, and exported variables are not retained. Put a directory change on the same line (cd <dir> && <command>).
241
+ /tmp belongs to this sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
240
242
  {% if javascript %}
241
- js-exec is available: run JavaScript / TypeScript in QuickJS. Some node modules can be required or imported; only files inside the sandbox are accessible.
243
+ js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
242
244
  {% endif %}
243
245
  {% if python %}
244
- python3 and python are available: CPython compiled to WebAssembly with the full standard library. The first call waits for it to load; reads and writes target this sandbox.
246
+ python3 and python are available: CPython compiled to WebAssembly. Most of the standard library is available, but subprocess and the sqlite3 module are absent. The first call waits for it to load; reads and writes target this sandbox.
245
247
  {% endif %}
246
248
  You can also read a known file through read's workspace:///<sandbox-path> URL (e.g. workspace:///home/workspace/a.txt): virtual files, persistent directories, and mounts all resolve to the same sandbox file.
247
249
  Use read_file to read a known file, write_file to replace a file, edit_file for a small edit, and bash for listing, searching, and pipelines.`
@@ -323,7 +325,408 @@ var WorkspaceScheme = class extends FileScheme {
323
325
 
324
326
  // src/sandbox.ts
325
327
  import path4 from "node:path";
326
- import { Bash, InMemoryFs, MountableFs, OverlayFs, ReadWriteFs } from "just-bash";
328
+
329
+ // src/execution-limits.ts
330
+ function sandboxExecutionLimits(layout) {
331
+ return {
332
+ maxOutputSize: layout.maxExecutionOutputBytes,
333
+ maxExecutionTimeMs: layout.timeoutMs,
334
+ maxSqliteTimeoutMs: layout.timeoutMs,
335
+ maxPythonTimeoutMs: layout.timeoutMs,
336
+ maxJsTimeoutMs: layout.timeoutMs
337
+ };
338
+ }
339
+ __name(sandboxExecutionLimits, "sandboxExecutionLimits");
340
+
341
+ // src/js-bootstrap.ts
342
+ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
343
+ (function () {
344
+ var hostKey = Object.getOwnPropertyNames(globalThis).filter(function (name) {
345
+ return name.indexOf('__jbHost_') === 0;
346
+ })[0];
347
+ if (!hostKey) throw new Error('js-exec host bridge is unavailable');
348
+ var host = globalThis[hostKey];
349
+ var fs = globalThis.fs;
350
+ function unwrap(result) {
351
+ if (!result || result.ok !== true) throw new Error(result && result.error || 'Host operation failed');
352
+ return result.value;
353
+ }
354
+ var raw = {};
355
+ var names = [
356
+ 'readFileBuffer', 'readFileSync', 'writeFileSync', 'appendFileSync',
357
+ 'statSync', 'lstatSync', 'readdirSync', 'mkdirSync', 'rmSync',
358
+ 'symlinkSync', 'readlinkSync', 'chmodSync', 'realpathSync',
359
+ 'renameSync', 'copyFileSync'
360
+ ];
361
+ for (var index = 0; index < names.length; index++) raw[names[index]] = fs[names[index]];
362
+
363
+ var errnos = {
364
+ EPERM: -1,
365
+ ENOENT: -2,
366
+ EIO: -5,
367
+ EACCES: -13,
368
+ EEXIST: -17,
369
+ ENOTDIR: -20,
370
+ EISDIR: -21,
371
+ EINVAL: -22,
372
+ ENOSPC: -28,
373
+ EROFS: -30,
374
+ ENAMETOOLONG: -36,
375
+ ENOTEMPTY: -39,
376
+ ELOOP: -40
377
+ };
378
+
379
+ var constants = {
380
+ F_OK: 0,
381
+ R_OK: 4,
382
+ W_OK: 2,
383
+ X_OK: 1,
384
+ COPYFILE_EXCL: 1,
385
+ COPYFILE_FICLONE: 2,
386
+ COPYFILE_FICLONE_FORCE: 4,
387
+ S_IFMT: 61440,
388
+ S_IFREG: 32768,
389
+ S_IFDIR: 16384,
390
+ S_IFLNK: 40960,
391
+ O_RDONLY: 0,
392
+ O_WRONLY: 1,
393
+ O_RDWR: 2,
394
+ O_CREAT: 64,
395
+ O_EXCL: 128,
396
+ O_TRUNC: 512,
397
+ O_APPEND: 1024
398
+ };
399
+
400
+ function failure(error, syscall, target, destination) {
401
+ if (!(error instanceof Error)) error = new Error(String(error));
402
+ var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
403
+ if (matched) {
404
+ error.code = matched[1];
405
+ if (errnos[error.code] !== undefined) error.errno = errnos[error.code];
406
+ }
407
+ error.syscall = syscall;
408
+ if (target !== undefined) {
409
+ error.path = String(target);
410
+ error.message = error.message.replace(/'[^']*'(?=\s*$)/, "'" + String(target).replace(/'/g, "\\'") + "'");
411
+ }
412
+ if (destination !== undefined) error.dest = String(destination);
413
+ return error;
414
+ }
415
+
416
+ function filePath(value) {
417
+ if (
418
+ typeof value !== 'object' ||
419
+ value === null ||
420
+ typeof value.href !== 'string' ||
421
+ typeof value.protocol !== 'string'
422
+ ) return value;
423
+ if (value.protocol !== 'file:') {
424
+ throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
425
+ }
426
+ if (value.hostname && value.hostname !== 'localhost') {
427
+ throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
428
+ }
429
+ return decodeURIComponent(value.pathname);
430
+ }
431
+
432
+ function bridge(hostName, syscall, target, args, destination) {
433
+ try {
434
+ var operation = host[hostName];
435
+ if (args.length === 1) return unwrap(operation(args[0]));
436
+ if (args.length === 2) return unwrap(operation(args[0], args[1]));
437
+ if (args.length === 3) return unwrap(operation(args[0], args[1], args[2]));
438
+ return unwrap(operation(args[0], args[1], args[2], args[3]));
439
+ } catch (error) {
440
+ throw failure(error, syscall, target, destination);
441
+ }
442
+ }
443
+
444
+ function bytes(value) {
445
+ if (value && value._data instanceof Uint8Array) return Array.from(value._data);
446
+ if (value && Array.isArray(value._data)) return value._data.slice();
447
+ if (Array.isArray(value)) return value.slice();
448
+ if (value instanceof Uint8Array) return Array.from(value);
449
+ return String(value);
450
+ }
451
+
452
+ function Stats() {}
453
+ function Dirent() {}
454
+
455
+ function stats(value) {
456
+ var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
457
+ var size = Number(value.size);
458
+ var result = Object.create(Stats.prototype);
459
+ result.dev = 0;
460
+ result.ino = 0;
461
+ result.mode = Number(value.mode);
462
+ result.nlink = value.isDirectory ? 2 : 1;
463
+ result.uid = 1000;
464
+ result.gid = 1000;
465
+ result.rdev = 0;
466
+ result.size = size;
467
+ result.blksize = 4096;
468
+ result.blocks = Math.ceil(size / 512);
469
+ result.atime = mtime;
470
+ result.mtime = mtime;
471
+ result.ctime = mtime;
472
+ result.birthtime = mtime;
473
+ result.atimeMs = mtime.getTime();
474
+ result.mtimeMs = mtime.getTime();
475
+ result.ctimeMs = mtime.getTime();
476
+ result.birthtimeMs = mtime.getTime();
477
+ result.isFile = function () { return value.isFile; };
478
+ result.isDirectory = function () { return value.isDirectory; };
479
+ result.isSymbolicLink = function () { return value.isSymbolicLink; };
480
+ result.isBlockDevice = function () { return false; };
481
+ result.isCharacterDevice = function () { return false; };
482
+ result.isFIFO = function () { return false; };
483
+ result.isSocket = function () { return false; };
484
+ return result;
485
+ }
486
+
487
+ function dirent(name, parentPath, value) {
488
+ var result = Object.create(Dirent.prototype);
489
+ result.name = String(name);
490
+ result.parentPath = String(parentPath);
491
+ result.path = String(parentPath);
492
+ result.isFile = function () { return value.isFile; };
493
+ result.isDirectory = function () { return value.isDirectory; };
494
+ result.isSymbolicLink = function () { return value.isSymbolicLink; };
495
+ result.isBlockDevice = function () { return false; };
496
+ result.isCharacterDevice = function () { return false; };
497
+ result.isFIFO = function () { return false; };
498
+ result.isSocket = function () { return false; };
499
+ return result;
500
+ }
501
+
502
+ function childPath(parent, name) {
503
+ return parent.charAt(parent.length - 1) === '/' ? parent + name : parent + '/' + name;
504
+ }
505
+
506
+ function readStats(path, follow) {
507
+ var target = filePath(path);
508
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
509
+ }
510
+
511
+ function collect(path, relative, options, output) {
512
+ var listing = bridge('fsReaddir', 'scandir', path, [path]);
513
+ for (var index = 0; index < listing.length; index++) {
514
+ var name = listing[index];
515
+ var nested = relative === '' ? name : relative + '/' + name;
516
+ var full = childPath(path, name);
517
+ var value = readStats(full, false);
518
+ output.push(options.withFileTypes ? dirent(name, path, value) : nested);
519
+ if (options.recursive && value.isDirectory) collect(full, nested, options, output);
520
+ }
521
+ }
522
+
523
+ function readdir(path, options) {
524
+ var target = filePath(path);
525
+ if (typeof options === 'string') options = { encoding: options };
526
+ options = options || {};
527
+ var output = [];
528
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
529
+ if (options.encoding === 'buffer') return output.map(function (entry) {
530
+ return typeof entry === 'string' ? Buffer.from(entry) : entry;
531
+ });
532
+ return output;
533
+ }
534
+
535
+ function firstCreated(path) {
536
+ var text = String(filePath(path));
537
+ var absolute = text.charAt(0) === '/';
538
+ var parts = text.split('/');
539
+ var current = '';
540
+ var found;
541
+ for (var index = 0; index < parts.length; index++) {
542
+ var part = parts[index];
543
+ if (part === '' || part === '.') continue;
544
+ if (current === '') current = absolute ? '/' + part : part;
545
+ else current = childPath(current, part);
546
+ if (!fs.existsSync(current)) {
547
+ found = current;
548
+ break;
549
+ }
550
+ }
551
+ return found;
552
+ }
553
+
554
+ fs.readFileBuffer = function (path) {
555
+ var target = filePath(path);
556
+ try {
557
+ return Buffer.from(raw.readFileBuffer(target));
558
+ } catch (error) {
559
+ throw failure(error, 'open', target);
560
+ }
561
+ };
562
+ fs.readFileSync = function (path, options) {
563
+ var target = filePath(path);
564
+ try {
565
+ return raw.readFileSync(target, options);
566
+ } catch (error) {
567
+ throw failure(error, 'open', target);
568
+ }
569
+ };
570
+ fs.writeFileSync = function (path, data) {
571
+ var target = filePath(path);
572
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
573
+ };
574
+ fs.appendFileSync = function (path, data) {
575
+ var target = filePath(path);
576
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
577
+ };
578
+ fs.statSync = function (path) {
579
+ return stats(readStats(path, true));
580
+ };
581
+ fs.lstatSync = function (path) {
582
+ return stats(readStats(path, false));
583
+ };
584
+ fs.readdirSync = readdir;
585
+ fs.mkdirSync = function (path, options) {
586
+ var target = filePath(path);
587
+ var recursive = Boolean(options && typeof options === 'object' && options.recursive);
588
+ var created = recursive ? firstCreated(target) : undefined;
589
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
590
+ return created;
591
+ };
592
+ fs.rmSync = function (path, options) {
593
+ var target = filePath(path);
594
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
595
+ };
596
+ fs.existsSync = function (path) {
597
+ var target = filePath(path);
598
+ try {
599
+ return bridge('fsExists', 'access', target, [target]);
600
+ } catch (error) {
601
+ return false;
602
+ }
603
+ };
604
+ fs.accessSync = function (path, mode) {
605
+ var target = filePath(path);
606
+ if (!bridge('fsExists', 'access', target, [target])) {
607
+ throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
608
+ }
609
+ var flags = mode === undefined ? 0 : Number(mode);
610
+ if ((flags & ~7) !== 0) {
611
+ throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
612
+ }
613
+ if (flags === 0) return;
614
+ var permissions = stats(readStats(target, true)).mode & 511;
615
+ var required = 0;
616
+ if (flags & constants.R_OK) required |= 256;
617
+ if (flags & constants.W_OK) required |= 128;
618
+ if (flags & constants.X_OK) required |= 64;
619
+ if ((permissions & required) !== required) {
620
+ throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
621
+ }
622
+ };
623
+ fs.symlinkSync = function (target, path) {
624
+ var link = filePath(path);
625
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
626
+ };
627
+ fs.readlinkSync = function (path) {
628
+ var target = filePath(path);
629
+ return bridge('fsReadlink', 'readlink', target, [target]);
630
+ };
631
+ fs.chmodSync = function (path, mode) {
632
+ var target = filePath(path);
633
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
634
+ };
635
+ fs.realpathSync = function (path) {
636
+ var target = filePath(path);
637
+ return bridge('fsRealpath', 'realpath', target, [target]);
638
+ };
639
+ fs.renameSync = function (from, to) {
640
+ var source = filePath(from);
641
+ var destination = filePath(to);
642
+ bridge('fsRename', 'rename', source, [source, destination], destination);
643
+ };
644
+ fs.copyFileSync = function (from, to, mode) {
645
+ var source = filePath(from);
646
+ var destination = filePath(to);
647
+ if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
648
+ throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
649
+ }
650
+ bridge('fsCopy', 'copyfile', source, [source, destination], destination);
651
+ };
652
+ fs.unlinkSync = function (path) {
653
+ var target = filePath(path);
654
+ bridge('fsRm', 'unlink', target, [target, false, false]);
655
+ };
656
+ fs.rmdirSync = function (path, options) {
657
+ var target = filePath(path);
658
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
659
+ };
660
+ fs.constants = constants;
661
+ fs.Stats = Stats;
662
+ fs.Dirent = Dirent;
663
+
664
+ var callbackNames = [
665
+ ['readFile', fs.readFileSync],
666
+ ['writeFile', fs.writeFileSync],
667
+ ['appendFile', fs.appendFileSync],
668
+ ['stat', fs.statSync],
669
+ ['lstat', fs.lstatSync],
670
+ ['readdir', fs.readdirSync],
671
+ ['mkdir', fs.mkdirSync],
672
+ ['rm', fs.rmSync],
673
+ ['symlink', fs.symlinkSync],
674
+ ['readlink', fs.readlinkSync],
675
+ ['chmod', fs.chmodSync],
676
+ ['realpath', fs.realpathSync],
677
+ ['rename', fs.renameSync],
678
+ ['copyFile', fs.copyFileSync],
679
+ ['unlink', fs.unlinkSync],
680
+ ['rmdir', fs.rmdirSync]
681
+ ];
682
+ function callbackify(name, sync) {
683
+ fs[name] = function () {
684
+ var args = Array.prototype.slice.call(arguments);
685
+ var callback = args.pop();
686
+ if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
687
+ Promise.resolve().then(function () {
688
+ return sync.apply(fs, args);
689
+ }).then(function (value) {
690
+ callback(null, value);
691
+ }, function (error) {
692
+ callback(error);
693
+ });
694
+ };
695
+ }
696
+ for (var index = 0; index < callbackNames.length; index++) callbackify(callbackNames[index][0], callbackNames[index][1]);
697
+ fs.exists = function (path, callback) {
698
+ if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
699
+ Promise.resolve().then(function () { callback(fs.existsSync(path)); });
700
+ };
701
+
702
+ var promiseNames = [
703
+ 'readFile', 'writeFile', 'appendFile', 'stat', 'lstat', 'readdir',
704
+ 'mkdir', 'rm', 'symlink', 'readlink', 'chmod', 'realpath',
705
+ 'rename', 'copyFile', 'unlink', 'rmdir'
706
+ ];
707
+ fs.promises = {};
708
+ for (var index = 0; index < promiseNames.length; index++) (function (name) {
709
+ var sync = fs[name + 'Sync'];
710
+ fs.promises[name] = function () {
711
+ var args = arguments;
712
+ return new Promise(function (resolve, reject) {
713
+ Promise.resolve().then(function () {
714
+ return sync.apply(fs, args);
715
+ }).then(resolve, reject);
716
+ });
717
+ };
718
+ })(promiseNames[index]);
719
+ fs.promises.access = function (path, mode) {
720
+ return new Promise(function (resolve, reject) {
721
+ Promise.resolve().then(function () {
722
+ fs.accessSync(path, mode);
723
+ }).then(resolve, reject);
724
+ });
725
+ };
726
+
727
+ globalThis.fs = fs;
728
+ })();
729
+ `;
327
730
 
328
731
  // src/resource-fs.ts
329
732
  var EROFS = /* @__PURE__ */ __name(() => new Error("EROFS: resource mounts are read-only"), "EROFS");
@@ -481,6 +884,7 @@ function traversable(stat2) {
481
884
  }
482
885
  __name(traversable, "traversable");
483
886
  async function createSandbox(layout, options) {
887
+ const { Bash, InMemoryFs, MountableFs, OverlayFs, ReadWriteFs } = await import("just-bash");
484
888
  const { home } = options;
485
889
  class Writable extends ReadWriteFs {
486
890
  static {
@@ -512,17 +916,16 @@ async function createSandbox(layout, options) {
512
916
  fs: fs3,
513
917
  cwd: layout.cwd,
514
918
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
515
- javascript: layout.javascript,
919
+ javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
516
920
  python: layout.python,
517
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
921
+ executionLimits: sandboxExecutionLimits(layout)
518
922
  });
519
923
  return {
520
924
  ...layout,
521
925
  fs: fs3,
522
926
  async exec(command, signal) {
523
- const timeout = AbortSignal.timeout(layout.timeoutMs);
524
927
  try {
525
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
928
+ const result = await bash.exec(command, { signal });
526
929
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
527
930
  } catch (error) {
528
931
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -550,8 +953,11 @@ import path6 from "node:path";
550
953
  // src/skills.ts
551
954
  import { readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
552
955
  import path5 from "node:path";
956
+ import { fileURLToPath } from "node:url";
553
957
  import { parse as parseYaml } from "yaml";
554
958
  var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
959
+ var moduleDirectory = typeof __dirname === "string" ? __dirname : path5.dirname(fileURLToPath(import.meta.url));
960
+ var BUILTIN_SKILLS_DIRECTORY = path5.resolve(moduleDirectory, "..", "resources", "skills");
555
961
  function discoverSkills(roots, logger) {
556
962
  const found = /* @__PURE__ */ new Map();
557
963
  for (const root of roots) {
@@ -626,7 +1032,10 @@ function defaultShell(configured) {
626
1032
  __name(defaultShell, "defaultShell");
627
1033
  function resolveLayout(options) {
628
1034
  const { config } = options;
629
- const skills = discoverSkills([path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))], options.logger);
1035
+ const skills = discoverSkills(
1036
+ [BUILTIN_SKILLS_DIRECTORY, path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))],
1037
+ options.logger
1038
+ );
630
1039
  const workspace = path6.join(options.home, "workspace");
631
1040
  mkdirSync2(workspace, { recursive: true });
632
1041
  const backend = config.backend;
@@ -666,6 +1075,9 @@ Run a bash command inside the sandbox.
666
1075
  The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
667
1076
  {% endif %}
668
1077
  Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
1078
+ {% if not native %}
1079
+ /tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
1080
+ {% endif %}
669
1081
  {% if gitBashPaths %}
670
1082
  Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
671
1083
  Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
@@ -729,7 +1141,7 @@ function createWorkspaceTools(sandbox) {
729
1141
  ${output.stdout}`);
730
1142
  if (output.stderr.length > 0) sections.push(`stderr:
731
1143
  ${output.stderr}`);
732
- return { type: "text", value: sections.join("\n\n") };
1144
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
733
1145
  }, "toModelOutput"),
734
1146
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
735
1147
  }),
@@ -831,22 +1243,22 @@ var WorkspaceExtension = class extends Extension {
831
1243
  this.logger.level = config.logLevel;
832
1244
  this.dataPath = ctx.ishiki.dataPath;
833
1245
  }
834
- [Service.invoke](config, context) {
1246
+ [Service.invoke](host, config) {
835
1247
  const shellOptions = {
836
1248
  config,
837
- home: context.home,
838
- root: context.root,
1249
+ home: host.home,
1250
+ root: host.root,
839
1251
  dataPath: this.dataPath,
840
1252
  logger: this.logger,
841
- resources: context.resources
1253
+ resources: host.resources
842
1254
  };
843
1255
  const layout = resolveLayout(shellOptions);
844
1256
  let shell;
845
1257
  let tools;
846
1258
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
847
1259
  const disposers = [];
848
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
849
- disposers.push(context.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1260
+ disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
1261
+ disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
850
1262
  return {
851
1263
  name: "ishiki.workspace",
852
1264
  extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
@@ -0,0 +1,5 @@
1
+ /**
2
+ * js-exec 的 fs 适配。just-bash 在 guest 中压平了 stat、丢弃了 readdir 选项,
3
+ * 并把桥接错误变成普通 Error;这里在用户代码加载前补齐 Node 的文件系统约定。
4
+ */
5
+ export declare const JAVASCRIPT_FS_BOOTSTRAP: string;
package/lib/skills.d.ts CHANGED
@@ -4,5 +4,6 @@ export interface Skill {
4
4
  readonly description: string;
5
5
  readonly directory: string;
6
6
  }
7
+ export declare const BUILTIN_SKILLS_DIRECTORY: string;
7
8
  export declare function discoverSkills(roots: readonly string[], logger: Logger): Skill[];
8
9
  export declare function skillCatalog(skills: readonly Skill[], native: boolean): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ishiki-workspace",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Workspace tools for Ishiki: sandboxed or native shell, file tools and skills",
5
5
  "keywords": [
6
6
  "agent",
@@ -21,7 +21,8 @@
21
21
  "directory": "packages/workspace"
22
22
  },
23
23
  "files": [
24
- "lib"
24
+ "lib",
25
+ "resources"
25
26
  ],
26
27
  "main": "./lib/index.cjs",
27
28
  "module": "./lib/index.js",
@@ -44,11 +45,11 @@
44
45
  },
45
46
  "devDependencies": {
46
47
  "koishi": "^4.18.11",
47
- "koishi-plugin-ishiki": ">=0.0.3 <0.1.0"
48
+ "koishi-plugin-ishiki": ">=0.0.5 <0.1.0"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "koishi": "^4.18.11",
51
- "koishi-plugin-ishiki": ">=0.0.3 <0.1.0"
52
+ "koishi-plugin-ishiki": ">=0.0.5 <0.1.0"
52
53
  },
53
54
  "koishi": {
54
55
  "description": {
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: sandbox-environment
3
+ description: Use before running or debugging shell commands, JavaScript, TypeScript, Python, SQLite, networking, or file operations in the Ishiki sandbox; it routes work to the supported runtime and records the environment's API gaps.
4
+ ---
5
+
6
+ # Sandbox Environment
7
+
8
+ The workspace is a just-bash environment with optional QuickJS and WebAssembly Python runtimes. Choose the executor before writing runtime-specific code.
9
+
10
+ ## Choose the executor
11
+
12
+ - Use the POSIX shell for file discovery, text pipelines, archive commands, and the SQLite command-line tool.
13
+ - Use `js-exec <file>` for JavaScript or TypeScript. The `node` command is a usage notice, so it does not execute scripts.
14
+ - Use `python3` or `python` for Python code. The first invocation waits for the WebAssembly runtime to load.
15
+
16
+ Read the matching reference before using runtime-specific APIs:
17
+
18
+ - Shell commands, mounts, SQLite, and networking: [references/shell.md](references/shell.md)
19
+ - JavaScript modules, globals, `fs`, and async callbacks: [references/javascript.md](references/javascript.md)
20
+ - Python modules, subprocesses, and persistence: [references/python.md](references/python.md)
21
+
22
+ ## Shared paths
23
+
24
+ - `/home` is read-only and contains this instance's data directory.
25
+ - `/home/workspace` is the writable working directory. Put scripts and generated files here.
26
+ - `/home/skills` is read-only. Read a skill, then run only scripts that the task needs.
27
+
28
+ Each shell call is isolated by the configured timeout. Environment variables, functions, aliases, and the current directory return to their defaults on the next call.
@@ -0,0 +1,44 @@
1
+ # JavaScript Runtime
2
+
3
+ Run JavaScript and TypeScript with `js-exec <file>`. It uses QuickJS and supports ESM imports, top-level await, TypeScript syntax, `fetch`, and `node:child_process`. The `node` command does not run code.
4
+
5
+ ## Modules and globals
6
+
7
+ The runtime exposes a partial Node.js surface. These modules are unavailable:
8
+
9
+ `node:zlib`, `node:crypto`, `node:net`, `node:http`, `node:https`, `node:dns`, `node:tls`, `node:readline`, `node:vm`, `node:v8`, `node:worker_threads`, `node:module`, `node:timers`, `node:tty`, and `node:sqlite3`.
10
+
11
+ These browser and JavaScript globals are unavailable:
12
+
13
+ `TextEncoder`, `TextDecoder`, `atob`, `btoa`, `setTimeout`, `setInterval`, `queueMicrotask`, `structuredClone`, `performance`, `WebAssembly`, and `crypto`.
14
+
15
+ Use the synchronous filesystem API or `fs.promises` for file work. Use `Buffer` for UTF-8 and base64 conversion.
16
+
17
+ ## Filesystem API
18
+
19
+ The workspace bootstrap supplies common Node `fs` behavior, including:
20
+
21
+ - `Stats` and `Dirent` instances, `fs.constants`, `fs.accessSync`, and recursive `readdir`
22
+ - callback, promise, and synchronous forms for common file and directory methods
23
+ - file URL path arguments and `Buffer` results from `fs.readFileBuffer`
24
+ - Node-shaped error fields such as `code`, `errno`, `syscall`, `path`, and `dest`
25
+
26
+ The `node:fs` module loader publishes a fixed set of named exports. Use the default import when accessing bootstrap additions: `import fs from 'node:fs'`. Namespace imports expose the upstream method list, so additions such as `fs.constants`, `fs.Stats`, `fs.Dirent`, and `fs.accessSync` are absent from `import * as fs`.
27
+
28
+ The sandbox does not expose file descriptors or streams. `openSync`, `readSync`, `writeSync`, `createReadStream`, and `createWriteStream` are absent. `cp`, `truncate`, `utimes`, `mkdtemp`, `watch`, and `opendir` are also absent. Use whole-file reads and writes, or run a shell command for unsupported operations.
29
+
30
+ ## Async callbacks
31
+
32
+ QuickJS stops when the script's synchronous top level finishes. Keep the script alive with top-level `await` until callback work completes, or use `fs.promises`:
33
+
34
+ ```js
35
+ await new Promise((resolve, reject) => {
36
+ fs.readFile("/home/workspace/input.txt", "utf8", (error, text) => {
37
+ if (error) reject(error);
38
+ else {
39
+ console.log(text);
40
+ resolve();
41
+ }
42
+ });
43
+ });
44
+ ```
@@ -0,0 +1,14 @@
1
+ # Python Runtime
2
+
3
+ `python3` and `python` run CPython 3.13 through Emscripten. The first call waits for the runtime to load. Scripts read and write the same sandbox paths as the shell.
4
+
5
+ ## Available modules
6
+
7
+ Most pure-Python standard library modules are available. Native integration is limited:
8
+
9
+ - `subprocess` is unavailable.
10
+ - `_sqlite3` and the `sqlite3` module are unavailable.
11
+ - Third-party packages cannot be installed.
12
+ - Long-running threads, sockets, and process management are unavailable.
13
+
14
+ Use `/home/workspace` for scripts and generated files. For SQLite work, use the shell's `sqlite3` command. To start Python from JavaScript, use `node:child_process`; Python cannot start shell commands or other programs.
@@ -0,0 +1,25 @@
1
+ # Shell Runtime
2
+
3
+ just-bash provides a POSIX-style command set, not a Linux host. Shell state does not persist between calls, and each call runs under the configured timeout.
4
+
5
+ ## Files and processes
6
+
7
+ - Use `/home/workspace` for writable files.
8
+ - `/home` and `/home/skills` are read-only mounts.
9
+ - Start a process and consume its output in the same call. Background processes do not outlive the call.
10
+ - Use `bash` pipelines for listing, searching, text transforms, and archive work that the JavaScript filesystem bridge does not expose.
11
+
12
+ ## SQLite
13
+
14
+ The `sqlite3` command is available:
15
+
16
+ ```bash
17
+ sqlite3 /home/workspace/data.db "CREATE TABLE notes (body TEXT); INSERT INTO notes VALUES ('hello');"
18
+ sqlite3 /home/workspace/data.db "SELECT body FROM notes;"
19
+ ```
20
+
21
+ The command does not support dot commands such as `.tables` or `.version`. Pass SQL as the command argument. Python's `sqlite3` module is unavailable, so use this command-line tool for database work.
22
+
23
+ ## Network
24
+
25
+ When networking is enabled, `curl` and JavaScript `fetch` can reach public addresses. Private and loopback addresses are rejected. Check the task's configured network access before choosing a remote API.