koishi-plugin-ishiki-workspace 0.0.4 → 0.0.6

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
@@ -39,18 +39,27 @@ var import_koishi_plugin_ishiki4 = require("koishi-plugin-ishiki");
39
39
 
40
40
  // src/config.ts
41
41
  var import_koishi = require("koishi");
42
- var WorkspaceConfig = import_koishi.Schema.object({
43
- backend: import_koishi.Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
44
- shell: import_koishi.Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default(""),
45
- cwd: import_koishi.Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
46
- timeoutMs: import_koishi.Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
47
- maxExecutionOutputBytes: import_koishi.Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
48
- network: import_koishi.Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
49
- javascript: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
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([]),
52
- mounts: import_koishi.Schema.array(import_koishi.Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
53
- });
42
+ var WorkspaceConfig = import_koishi.Schema.intersect([
43
+ import_koishi.Schema.object({
44
+ backend: import_koishi.Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
45
+ timeoutMs: import_koishi.Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
46
+ maxExecutionOutputBytes: import_koishi.Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
47
+ skills: import_koishi.Schema.array(import_koishi.Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([])
48
+ }),
49
+ import_koishi.Schema.union([
50
+ import_koishi.Schema.object({
51
+ backend: import_koishi.Schema.const("native").required(),
52
+ shell: import_koishi.Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default("")
53
+ }).description("native 模式"),
54
+ import_koishi.Schema.object({
55
+ cwd: import_koishi.Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
56
+ network: import_koishi.Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
57
+ javascript: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
58
+ python: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
59
+ mounts: import_koishi.Schema.array(import_koishi.Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
60
+ }).description("sandbox 模式")
61
+ ])
62
+ ]);
54
63
 
55
64
  // src/native.ts
56
65
  var import_node_child_process = require("node:child_process");
@@ -250,8 +259,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
250
259
  );
251
260
  var SANDBOX_INSTRUCTIONS = new import_koishi_plugin_ishiki.Template(
252
261
  `## 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.
262
+ 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
263
  The command set is not the host's.
264
+ 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
265
  Current working directory: {{ cwd }}
256
266
  Mounts:
257
267
  - {{ homeMount }}: read-only, this instance's data directory (writes fail)
@@ -269,11 +279,12 @@ Network: {{ network }}
269
279
  Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
270
280
  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
281
  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>).
282
+ /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
283
  {% 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.
284
+ js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
274
285
  {% endif %}
275
286
  {% 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.
287
+ 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
288
  {% endif %}
278
289
  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
290
  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.`
@@ -356,6 +367,18 @@ var WorkspaceScheme = class extends import_koishi_plugin_ishiki2.FileScheme {
356
367
  // src/sandbox.ts
357
368
  var import_node_path4 = __toESM(require("node:path"));
358
369
 
370
+ // src/execution-limits.ts
371
+ function sandboxExecutionLimits(layout) {
372
+ return {
373
+ maxOutputSize: layout.maxExecutionOutputBytes,
374
+ maxExecutionTimeMs: layout.timeoutMs,
375
+ maxSqliteTimeoutMs: layout.timeoutMs,
376
+ maxPythonTimeoutMs: layout.timeoutMs,
377
+ maxJsTimeoutMs: layout.timeoutMs
378
+ };
379
+ }
380
+ __name(sandboxExecutionLimits, "sandboxExecutionLimits");
381
+
359
382
  // src/js-bootstrap.ts
360
383
  var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
361
384
  (function () {
@@ -394,6 +417,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
394
417
  ELOOP: -40
395
418
  };
396
419
 
420
+ var constants = {
421
+ F_OK: 0,
422
+ R_OK: 4,
423
+ W_OK: 2,
424
+ X_OK: 1,
425
+ COPYFILE_EXCL: 1,
426
+ COPYFILE_FICLONE: 2,
427
+ COPYFILE_FICLONE_FORCE: 4,
428
+ S_IFMT: 61440,
429
+ S_IFREG: 32768,
430
+ S_IFDIR: 16384,
431
+ S_IFLNK: 40960,
432
+ O_RDONLY: 0,
433
+ O_WRONLY: 1,
434
+ O_RDWR: 2,
435
+ O_CREAT: 64,
436
+ O_EXCL: 128,
437
+ O_TRUNC: 512,
438
+ O_APPEND: 1024
439
+ };
440
+
397
441
  function failure(error, syscall, target, destination) {
398
442
  if (!(error instanceof Error)) error = new Error(String(error));
399
443
  var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
@@ -410,6 +454,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
410
454
  return error;
411
455
  }
412
456
 
457
+ function filePath(value) {
458
+ if (
459
+ typeof value !== 'object' ||
460
+ value === null ||
461
+ typeof value.href !== 'string' ||
462
+ typeof value.protocol !== 'string'
463
+ ) return value;
464
+ if (value.protocol !== 'file:') {
465
+ throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
466
+ }
467
+ if (value.hostname && value.hostname !== 'localhost') {
468
+ throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
469
+ }
470
+ return decodeURIComponent(value.pathname);
471
+ }
472
+
413
473
  function bridge(hostName, syscall, target, args, destination) {
414
474
  try {
415
475
  var operation = host[hostName];
@@ -430,29 +490,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
430
490
  return String(value);
431
491
  }
432
492
 
493
+ function Stats() {}
494
+ function Dirent() {}
495
+
433
496
  function stats(value) {
434
497
  var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
435
498
  var size = Number(value.size);
436
- var result = {
437
- dev: 0,
438
- ino: 0,
439
- mode: Number(value.mode),
440
- nlink: value.isDirectory ? 2 : 1,
441
- uid: 1000,
442
- gid: 1000,
443
- rdev: 0,
444
- size: size,
445
- blksize: 4096,
446
- blocks: Math.ceil(size / 512),
447
- atime: mtime,
448
- mtime: mtime,
449
- ctime: mtime,
450
- birthtime: mtime,
451
- atimeMs: mtime.getTime(),
452
- mtimeMs: mtime.getTime(),
453
- ctimeMs: mtime.getTime(),
454
- birthtimeMs: mtime.getTime()
455
- };
499
+ var result = Object.create(Stats.prototype);
500
+ result.dev = 0;
501
+ result.ino = 0;
502
+ result.mode = Number(value.mode);
503
+ result.nlink = value.isDirectory ? 2 : 1;
504
+ result.uid = 1000;
505
+ result.gid = 1000;
506
+ result.rdev = 0;
507
+ result.size = size;
508
+ result.blksize = 4096;
509
+ result.blocks = Math.ceil(size / 512);
510
+ result.atime = mtime;
511
+ result.mtime = mtime;
512
+ result.ctime = mtime;
513
+ result.birthtime = mtime;
514
+ result.atimeMs = mtime.getTime();
515
+ result.mtimeMs = mtime.getTime();
516
+ result.ctimeMs = mtime.getTime();
517
+ result.birthtimeMs = mtime.getTime();
456
518
  result.isFile = function () { return value.isFile; };
457
519
  result.isDirectory = function () { return value.isDirectory; };
458
520
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -464,7 +526,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
464
526
  }
465
527
 
466
528
  function dirent(name, parentPath, value) {
467
- var result = { name: String(name), parentPath: String(parentPath), path: String(parentPath) };
529
+ var result = Object.create(Dirent.prototype);
530
+ result.name = String(name);
531
+ result.parentPath = String(parentPath);
532
+ result.path = String(parentPath);
468
533
  result.isFile = function () { return value.isFile; };
469
534
  result.isDirectory = function () { return value.isDirectory; };
470
535
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -480,7 +545,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
480
545
  }
481
546
 
482
547
  function readStats(path, follow) {
483
- return bridge('fsStat', follow ? 'stat' : 'lstat', path, [path, !follow]);
548
+ var target = filePath(path);
549
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
484
550
  }
485
551
 
486
552
  function collect(path, relative, options, output) {
@@ -496,10 +562,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
496
562
  }
497
563
 
498
564
  function readdir(path, options) {
565
+ var target = filePath(path);
499
566
  if (typeof options === 'string') options = { encoding: options };
500
567
  options = options || {};
501
568
  var output = [];
502
- collect(path, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
569
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
503
570
  if (options.encoding === 'buffer') return output.map(function (entry) {
504
571
  return typeof entry === 'string' ? Buffer.from(entry) : entry;
505
572
  });
@@ -507,7 +574,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
507
574
  }
508
575
 
509
576
  function firstCreated(path) {
510
- var text = String(path);
577
+ var text = String(filePath(path));
511
578
  var absolute = text.charAt(0) === '/';
512
579
  var parts = text.split('/');
513
580
  var current = '';
@@ -526,24 +593,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
526
593
  }
527
594
 
528
595
  fs.readFileBuffer = function (path) {
596
+ var target = filePath(path);
529
597
  try {
530
- return raw.readFileBuffer(path);
598
+ return Buffer.from(raw.readFileBuffer(target));
531
599
  } catch (error) {
532
- throw failure(error, 'open', path);
600
+ throw failure(error, 'open', target);
533
601
  }
534
602
  };
535
603
  fs.readFileSync = function (path, options) {
604
+ var target = filePath(path);
536
605
  try {
537
- return raw.readFileSync(path, options);
606
+ return raw.readFileSync(target, options);
538
607
  } catch (error) {
539
- throw failure(error, 'open', path);
608
+ throw failure(error, 'open', target);
540
609
  }
541
610
  };
542
611
  fs.writeFileSync = function (path, data) {
543
- bridge('fsWrite', 'open', path, [path, bytes(data)]);
612
+ var target = filePath(path);
613
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
544
614
  };
545
615
  fs.appendFileSync = function (path, data) {
546
- bridge('fsAppend', 'open', path, [path, bytes(data)]);
616
+ var target = filePath(path);
617
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
547
618
  };
548
619
  fs.statSync = function (path) {
549
620
  return stats(readStats(path, true));
@@ -553,45 +624,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
553
624
  };
554
625
  fs.readdirSync = readdir;
555
626
  fs.mkdirSync = function (path, options) {
627
+ var target = filePath(path);
556
628
  var recursive = Boolean(options && typeof options === 'object' && options.recursive);
557
- var created = recursive ? firstCreated(path) : undefined;
558
- bridge('fsMkdir', 'mkdir', path, [path, recursive]);
629
+ var created = recursive ? firstCreated(target) : undefined;
630
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
559
631
  return created;
560
632
  };
561
633
  fs.rmSync = function (path, options) {
562
- bridge('fsRm', 'rm', path, [path, Boolean(options && options.recursive), Boolean(options && options.force)]);
634
+ var target = filePath(path);
635
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
563
636
  };
564
637
  fs.existsSync = function (path) {
638
+ var target = filePath(path);
565
639
  try {
566
- return bridge('fsExists', 'access', path, [path]);
640
+ return bridge('fsExists', 'access', target, [target]);
567
641
  } catch (error) {
568
642
  return false;
569
643
  }
570
644
  };
645
+ fs.accessSync = function (path, mode) {
646
+ var target = filePath(path);
647
+ if (!bridge('fsExists', 'access', target, [target])) {
648
+ throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
649
+ }
650
+ var flags = mode === undefined ? 0 : Number(mode);
651
+ if ((flags & ~7) !== 0) {
652
+ throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
653
+ }
654
+ if (flags === 0) return;
655
+ var permissions = stats(readStats(target, true)).mode & 511;
656
+ var required = 0;
657
+ if (flags & constants.R_OK) required |= 256;
658
+ if (flags & constants.W_OK) required |= 128;
659
+ if (flags & constants.X_OK) required |= 64;
660
+ if ((permissions & required) !== required) {
661
+ throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
662
+ }
663
+ };
571
664
  fs.symlinkSync = function (target, path) {
572
- bridge('fsSymlink', 'symlink', path, [target, path]);
665
+ var link = filePath(path);
666
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
573
667
  };
574
668
  fs.readlinkSync = function (path) {
575
- return bridge('fsReadlink', 'readlink', path, [path]);
669
+ var target = filePath(path);
670
+ return bridge('fsReadlink', 'readlink', target, [target]);
576
671
  };
577
672
  fs.chmodSync = function (path, mode) {
578
- bridge('fsChmod', 'chmod', path, [path, Number(mode)]);
673
+ var target = filePath(path);
674
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
579
675
  };
580
676
  fs.realpathSync = function (path) {
581
- return bridge('fsRealpath', 'realpath', path, [path]);
677
+ var target = filePath(path);
678
+ return bridge('fsRealpath', 'realpath', target, [target]);
582
679
  };
583
680
  fs.renameSync = function (from, to) {
584
- bridge('fsRename', 'rename', from, [from, to], to);
681
+ var source = filePath(from);
682
+ var destination = filePath(to);
683
+ bridge('fsRename', 'rename', source, [source, destination], destination);
585
684
  };
586
- fs.copyFileSync = function (from, to) {
587
- bridge('fsCopy', 'copyfile', from, [from, to], to);
685
+ fs.copyFileSync = function (from, to, mode) {
686
+ var source = filePath(from);
687
+ var destination = filePath(to);
688
+ if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
689
+ throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
690
+ }
691
+ bridge('fsCopy', 'copyfile', source, [source, destination], destination);
588
692
  };
589
693
  fs.unlinkSync = function (path) {
590
- bridge('fsRm', 'unlink', path, [path, false, false]);
694
+ var target = filePath(path);
695
+ bridge('fsRm', 'unlink', target, [target, false, false]);
591
696
  };
592
697
  fs.rmdirSync = function (path, options) {
593
- bridge('fsRm', 'rmdir', path, [path, Boolean(options && options.recursive), false]);
698
+ var target = filePath(path);
699
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
594
700
  };
701
+ fs.constants = constants;
702
+ fs.Stats = Stats;
703
+ fs.Dirent = Dirent;
595
704
 
596
705
  var callbackNames = [
597
706
  ['readFile', fs.readFileSync],
@@ -648,10 +757,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
648
757
  });
649
758
  };
650
759
  })(promiseNames[index]);
651
- fs.promises.access = function (path) {
760
+ fs.promises.access = function (path, mode) {
652
761
  return new Promise(function (resolve, reject) {
653
- if (fs.existsSync(path)) resolve();
654
- else reject(failure(new Error("ENOENT: no such file or directory, access '" + path + "'"), 'access', path));
762
+ Promise.resolve().then(function () {
763
+ fs.accessSync(path, mode);
764
+ }).then(resolve, reject);
655
765
  });
656
766
  };
657
767
 
@@ -849,15 +959,14 @@ async function createSandbox(layout, options) {
849
959
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
850
960
  javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
851
961
  python: layout.python,
852
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
962
+ executionLimits: sandboxExecutionLimits(layout)
853
963
  });
854
964
  return {
855
965
  ...layout,
856
966
  fs: fs3,
857
967
  async exec(command, signal) {
858
- const timeout = AbortSignal.timeout(layout.timeoutMs);
859
968
  try {
860
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
969
+ const result = await bash.exec(command, { signal });
861
970
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
862
971
  } catch (error) {
863
972
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -885,8 +994,12 @@ var import_node_path6 = __toESM(require("node:path"));
885
994
  // src/skills.ts
886
995
  var import_node_fs3 = require("node:fs");
887
996
  var import_node_path5 = __toESM(require("node:path"));
997
+ var import_node_url = require("node:url");
888
998
  var import_yaml = require("yaml");
999
+ var import_meta = {};
889
1000
  var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1001
+ var moduleDirectory = typeof __dirname === "string" ? __dirname : import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
1002
+ var BUILTIN_SKILLS_DIRECTORY = import_node_path5.default.resolve(moduleDirectory, "..", "resources", "skills");
890
1003
  function discoverSkills(roots, logger) {
891
1004
  const found = /* @__PURE__ */ new Map();
892
1005
  for (const root of roots) {
@@ -961,7 +1074,10 @@ function defaultShell(configured) {
961
1074
  __name(defaultShell, "defaultShell");
962
1075
  function resolveLayout(options) {
963
1076
  const { config } = options;
964
- 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);
1077
+ const skills = discoverSkills(
1078
+ [BUILTIN_SKILLS_DIRECTORY, import_node_path6.default.join(options.dataPath, "skills"), ...config.skills.map((dir) => import_node_path6.default.resolve(options.root, dir))],
1079
+ options.logger
1080
+ );
965
1081
  const workspace = import_node_path6.default.join(options.home, "workspace");
966
1082
  (0, import_node_fs4.mkdirSync)(workspace, { recursive: true });
967
1083
  const backend = config.backend;
@@ -1001,6 +1117,9 @@ Run a bash command inside the sandbox.
1001
1117
  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.
1002
1118
  {% endif %}
1003
1119
  Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
1120
+ {% if not native %}
1121
+ /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.
1122
+ {% endif %}
1004
1123
  {% if gitBashPaths %}
1005
1124
  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.
1006
1125
  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.
@@ -1064,7 +1183,7 @@ function createWorkspaceTools(sandbox) {
1064
1183
  ${output.stdout}`);
1065
1184
  if (output.stderr.length > 0) sections.push(`stderr:
1066
1185
  ${output.stderr}`);
1067
- return { type: "text", value: sections.join("\n\n") };
1186
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
1068
1187
  }, "toModelOutput"),
1069
1188
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
1070
1189
  }),
@@ -1159,6 +1278,7 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
1159
1278
  __name(this, "WorkspaceExtension");
1160
1279
  }
1161
1280
  static inject = ["ishiki"];
1281
+ static usage = ``;
1162
1282
  Schema = WorkspaceConfig;
1163
1283
  dataPath;
1164
1284
  constructor(ctx, config) {
@@ -1166,22 +1286,22 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
1166
1286
  this.logger.level = config.logLevel;
1167
1287
  this.dataPath = ctx.ishiki.dataPath;
1168
1288
  }
1169
- [import_koishi2.Service.invoke](config, context) {
1289
+ [import_koishi2.Service.invoke](host, config) {
1170
1290
  const shellOptions = {
1171
1291
  config,
1172
- home: context.home,
1173
- root: context.root,
1292
+ home: host.home,
1293
+ root: host.root,
1174
1294
  dataPath: this.dataPath,
1175
1295
  logger: this.logger,
1176
- resources: context.resources
1296
+ resources: host.resources
1177
1297
  };
1178
1298
  const layout = resolveLayout(shellOptions);
1179
1299
  let shell;
1180
1300
  let tools;
1181
1301
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
1182
1302
  const disposers = [];
1183
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
1184
- disposers.push(context.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1303
+ disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
1304
+ disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1185
1305
  return {
1186
1306
  name: "ishiki.workspace",
1187
1307
  extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
package/lib/index.d.ts CHANGED
@@ -1,12 +1,13 @@
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
+ static readonly usage = "";
6
7
  readonly Schema: Schema<WorkspaceConfig>;
7
8
  private readonly dataPath;
8
9
  constructor(ctx: Context, config: WorkspaceExtension.Config);
9
- [Service.invoke](config: WorkspaceConfig, context: ExtensionContext): {
10
+ [Service.invoke](host: ExtensionHost, config: WorkspaceConfig): {
10
11
  name: string;
11
12
  extendTools: () => Promise<ToolSet>;
12
13
  extendInstructions: () => Promise<string>;
package/lib/index.js CHANGED
@@ -7,18 +7,27 @@ import { Extension } from "koishi-plugin-ishiki";
7
7
 
8
8
  // src/config.ts
9
9
  import { Schema } from "koishi";
10
- var WorkspaceConfig = Schema.object({
11
- backend: Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
12
- shell: Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default(""),
13
- cwd: Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
14
- timeoutMs: Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
15
- maxExecutionOutputBytes: Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
16
- network: Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
17
- javascript: Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
18
- python: Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
19
- skills: Schema.array(Schema.string()).description("额外技能目录,相对 profile 目录解析;与数据根的 skills 目录合并,同名以这里优先").default([]),
20
- mounts: Schema.array(Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
21
- });
10
+ var WorkspaceConfig = Schema.intersect([
11
+ Schema.object({
12
+ backend: Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
13
+ timeoutMs: Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
14
+ maxExecutionOutputBytes: Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
15
+ skills: Schema.array(Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([])
16
+ }),
17
+ Schema.union([
18
+ Schema.object({
19
+ backend: Schema.const("native").required(),
20
+ shell: Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default("")
21
+ }).description("native 模式"),
22
+ Schema.object({
23
+ cwd: Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
24
+ network: Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
25
+ javascript: Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
26
+ python: Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
27
+ mounts: Schema.array(Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
28
+ }).description("sandbox 模式")
29
+ ])
30
+ ]);
22
31
 
23
32
  // src/native.ts
24
33
  import { execFile } from "node:child_process";
@@ -218,8 +227,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
218
227
  );
219
228
  var SANDBOX_INSTRUCTIONS = new Template(
220
229
  `## 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.
230
+ 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
231
  The command set is not the host's.
232
+ 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
233
  Current working directory: {{ cwd }}
224
234
  Mounts:
225
235
  - {{ homeMount }}: read-only, this instance's data directory (writes fail)
@@ -237,11 +247,12 @@ Network: {{ network }}
237
247
  Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
238
248
  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
249
  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>).
250
+ /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
251
  {% 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.
252
+ js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
242
253
  {% endif %}
243
254
  {% 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.
255
+ 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
256
  {% endif %}
246
257
  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
258
  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.`
@@ -324,6 +335,18 @@ var WorkspaceScheme = class extends FileScheme {
324
335
  // src/sandbox.ts
325
336
  import path4 from "node:path";
326
337
 
338
+ // src/execution-limits.ts
339
+ function sandboxExecutionLimits(layout) {
340
+ return {
341
+ maxOutputSize: layout.maxExecutionOutputBytes,
342
+ maxExecutionTimeMs: layout.timeoutMs,
343
+ maxSqliteTimeoutMs: layout.timeoutMs,
344
+ maxPythonTimeoutMs: layout.timeoutMs,
345
+ maxJsTimeoutMs: layout.timeoutMs
346
+ };
347
+ }
348
+ __name(sandboxExecutionLimits, "sandboxExecutionLimits");
349
+
327
350
  // src/js-bootstrap.ts
328
351
  var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
329
352
  (function () {
@@ -362,6 +385,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
362
385
  ELOOP: -40
363
386
  };
364
387
 
388
+ var constants = {
389
+ F_OK: 0,
390
+ R_OK: 4,
391
+ W_OK: 2,
392
+ X_OK: 1,
393
+ COPYFILE_EXCL: 1,
394
+ COPYFILE_FICLONE: 2,
395
+ COPYFILE_FICLONE_FORCE: 4,
396
+ S_IFMT: 61440,
397
+ S_IFREG: 32768,
398
+ S_IFDIR: 16384,
399
+ S_IFLNK: 40960,
400
+ O_RDONLY: 0,
401
+ O_WRONLY: 1,
402
+ O_RDWR: 2,
403
+ O_CREAT: 64,
404
+ O_EXCL: 128,
405
+ O_TRUNC: 512,
406
+ O_APPEND: 1024
407
+ };
408
+
365
409
  function failure(error, syscall, target, destination) {
366
410
  if (!(error instanceof Error)) error = new Error(String(error));
367
411
  var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
@@ -378,6 +422,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
378
422
  return error;
379
423
  }
380
424
 
425
+ function filePath(value) {
426
+ if (
427
+ typeof value !== 'object' ||
428
+ value === null ||
429
+ typeof value.href !== 'string' ||
430
+ typeof value.protocol !== 'string'
431
+ ) return value;
432
+ if (value.protocol !== 'file:') {
433
+ throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
434
+ }
435
+ if (value.hostname && value.hostname !== 'localhost') {
436
+ throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
437
+ }
438
+ return decodeURIComponent(value.pathname);
439
+ }
440
+
381
441
  function bridge(hostName, syscall, target, args, destination) {
382
442
  try {
383
443
  var operation = host[hostName];
@@ -398,29 +458,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
398
458
  return String(value);
399
459
  }
400
460
 
461
+ function Stats() {}
462
+ function Dirent() {}
463
+
401
464
  function stats(value) {
402
465
  var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
403
466
  var size = Number(value.size);
404
- var result = {
405
- dev: 0,
406
- ino: 0,
407
- mode: Number(value.mode),
408
- nlink: value.isDirectory ? 2 : 1,
409
- uid: 1000,
410
- gid: 1000,
411
- rdev: 0,
412
- size: size,
413
- blksize: 4096,
414
- blocks: Math.ceil(size / 512),
415
- atime: mtime,
416
- mtime: mtime,
417
- ctime: mtime,
418
- birthtime: mtime,
419
- atimeMs: mtime.getTime(),
420
- mtimeMs: mtime.getTime(),
421
- ctimeMs: mtime.getTime(),
422
- birthtimeMs: mtime.getTime()
423
- };
467
+ var result = Object.create(Stats.prototype);
468
+ result.dev = 0;
469
+ result.ino = 0;
470
+ result.mode = Number(value.mode);
471
+ result.nlink = value.isDirectory ? 2 : 1;
472
+ result.uid = 1000;
473
+ result.gid = 1000;
474
+ result.rdev = 0;
475
+ result.size = size;
476
+ result.blksize = 4096;
477
+ result.blocks = Math.ceil(size / 512);
478
+ result.atime = mtime;
479
+ result.mtime = mtime;
480
+ result.ctime = mtime;
481
+ result.birthtime = mtime;
482
+ result.atimeMs = mtime.getTime();
483
+ result.mtimeMs = mtime.getTime();
484
+ result.ctimeMs = mtime.getTime();
485
+ result.birthtimeMs = mtime.getTime();
424
486
  result.isFile = function () { return value.isFile; };
425
487
  result.isDirectory = function () { return value.isDirectory; };
426
488
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -432,7 +494,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
432
494
  }
433
495
 
434
496
  function dirent(name, parentPath, value) {
435
- var result = { name: String(name), parentPath: String(parentPath), path: String(parentPath) };
497
+ var result = Object.create(Dirent.prototype);
498
+ result.name = String(name);
499
+ result.parentPath = String(parentPath);
500
+ result.path = String(parentPath);
436
501
  result.isFile = function () { return value.isFile; };
437
502
  result.isDirectory = function () { return value.isDirectory; };
438
503
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -448,7 +513,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
448
513
  }
449
514
 
450
515
  function readStats(path, follow) {
451
- return bridge('fsStat', follow ? 'stat' : 'lstat', path, [path, !follow]);
516
+ var target = filePath(path);
517
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
452
518
  }
453
519
 
454
520
  function collect(path, relative, options, output) {
@@ -464,10 +530,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
464
530
  }
465
531
 
466
532
  function readdir(path, options) {
533
+ var target = filePath(path);
467
534
  if (typeof options === 'string') options = { encoding: options };
468
535
  options = options || {};
469
536
  var output = [];
470
- collect(path, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
537
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
471
538
  if (options.encoding === 'buffer') return output.map(function (entry) {
472
539
  return typeof entry === 'string' ? Buffer.from(entry) : entry;
473
540
  });
@@ -475,7 +542,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
475
542
  }
476
543
 
477
544
  function firstCreated(path) {
478
- var text = String(path);
545
+ var text = String(filePath(path));
479
546
  var absolute = text.charAt(0) === '/';
480
547
  var parts = text.split('/');
481
548
  var current = '';
@@ -494,24 +561,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
494
561
  }
495
562
 
496
563
  fs.readFileBuffer = function (path) {
564
+ var target = filePath(path);
497
565
  try {
498
- return raw.readFileBuffer(path);
566
+ return Buffer.from(raw.readFileBuffer(target));
499
567
  } catch (error) {
500
- throw failure(error, 'open', path);
568
+ throw failure(error, 'open', target);
501
569
  }
502
570
  };
503
571
  fs.readFileSync = function (path, options) {
572
+ var target = filePath(path);
504
573
  try {
505
- return raw.readFileSync(path, options);
574
+ return raw.readFileSync(target, options);
506
575
  } catch (error) {
507
- throw failure(error, 'open', path);
576
+ throw failure(error, 'open', target);
508
577
  }
509
578
  };
510
579
  fs.writeFileSync = function (path, data) {
511
- bridge('fsWrite', 'open', path, [path, bytes(data)]);
580
+ var target = filePath(path);
581
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
512
582
  };
513
583
  fs.appendFileSync = function (path, data) {
514
- bridge('fsAppend', 'open', path, [path, bytes(data)]);
584
+ var target = filePath(path);
585
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
515
586
  };
516
587
  fs.statSync = function (path) {
517
588
  return stats(readStats(path, true));
@@ -521,45 +592,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
521
592
  };
522
593
  fs.readdirSync = readdir;
523
594
  fs.mkdirSync = function (path, options) {
595
+ var target = filePath(path);
524
596
  var recursive = Boolean(options && typeof options === 'object' && options.recursive);
525
- var created = recursive ? firstCreated(path) : undefined;
526
- bridge('fsMkdir', 'mkdir', path, [path, recursive]);
597
+ var created = recursive ? firstCreated(target) : undefined;
598
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
527
599
  return created;
528
600
  };
529
601
  fs.rmSync = function (path, options) {
530
- bridge('fsRm', 'rm', path, [path, Boolean(options && options.recursive), Boolean(options && options.force)]);
602
+ var target = filePath(path);
603
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
531
604
  };
532
605
  fs.existsSync = function (path) {
606
+ var target = filePath(path);
533
607
  try {
534
- return bridge('fsExists', 'access', path, [path]);
608
+ return bridge('fsExists', 'access', target, [target]);
535
609
  } catch (error) {
536
610
  return false;
537
611
  }
538
612
  };
613
+ fs.accessSync = function (path, mode) {
614
+ var target = filePath(path);
615
+ if (!bridge('fsExists', 'access', target, [target])) {
616
+ throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
617
+ }
618
+ var flags = mode === undefined ? 0 : Number(mode);
619
+ if ((flags & ~7) !== 0) {
620
+ throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
621
+ }
622
+ if (flags === 0) return;
623
+ var permissions = stats(readStats(target, true)).mode & 511;
624
+ var required = 0;
625
+ if (flags & constants.R_OK) required |= 256;
626
+ if (flags & constants.W_OK) required |= 128;
627
+ if (flags & constants.X_OK) required |= 64;
628
+ if ((permissions & required) !== required) {
629
+ throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
630
+ }
631
+ };
539
632
  fs.symlinkSync = function (target, path) {
540
- bridge('fsSymlink', 'symlink', path, [target, path]);
633
+ var link = filePath(path);
634
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
541
635
  };
542
636
  fs.readlinkSync = function (path) {
543
- return bridge('fsReadlink', 'readlink', path, [path]);
637
+ var target = filePath(path);
638
+ return bridge('fsReadlink', 'readlink', target, [target]);
544
639
  };
545
640
  fs.chmodSync = function (path, mode) {
546
- bridge('fsChmod', 'chmod', path, [path, Number(mode)]);
641
+ var target = filePath(path);
642
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
547
643
  };
548
644
  fs.realpathSync = function (path) {
549
- return bridge('fsRealpath', 'realpath', path, [path]);
645
+ var target = filePath(path);
646
+ return bridge('fsRealpath', 'realpath', target, [target]);
550
647
  };
551
648
  fs.renameSync = function (from, to) {
552
- bridge('fsRename', 'rename', from, [from, to], to);
649
+ var source = filePath(from);
650
+ var destination = filePath(to);
651
+ bridge('fsRename', 'rename', source, [source, destination], destination);
553
652
  };
554
- fs.copyFileSync = function (from, to) {
555
- bridge('fsCopy', 'copyfile', from, [from, to], to);
653
+ fs.copyFileSync = function (from, to, mode) {
654
+ var source = filePath(from);
655
+ var destination = filePath(to);
656
+ if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
657
+ throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
658
+ }
659
+ bridge('fsCopy', 'copyfile', source, [source, destination], destination);
556
660
  };
557
661
  fs.unlinkSync = function (path) {
558
- bridge('fsRm', 'unlink', path, [path, false, false]);
662
+ var target = filePath(path);
663
+ bridge('fsRm', 'unlink', target, [target, false, false]);
559
664
  };
560
665
  fs.rmdirSync = function (path, options) {
561
- bridge('fsRm', 'rmdir', path, [path, Boolean(options && options.recursive), false]);
666
+ var target = filePath(path);
667
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
562
668
  };
669
+ fs.constants = constants;
670
+ fs.Stats = Stats;
671
+ fs.Dirent = Dirent;
563
672
 
564
673
  var callbackNames = [
565
674
  ['readFile', fs.readFileSync],
@@ -616,10 +725,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
616
725
  });
617
726
  };
618
727
  })(promiseNames[index]);
619
- fs.promises.access = function (path) {
728
+ fs.promises.access = function (path, mode) {
620
729
  return new Promise(function (resolve, reject) {
621
- if (fs.existsSync(path)) resolve();
622
- else reject(failure(new Error("ENOENT: no such file or directory, access '" + path + "'"), 'access', path));
730
+ Promise.resolve().then(function () {
731
+ fs.accessSync(path, mode);
732
+ }).then(resolve, reject);
623
733
  });
624
734
  };
625
735
 
@@ -817,15 +927,14 @@ async function createSandbox(layout, options) {
817
927
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
818
928
  javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
819
929
  python: layout.python,
820
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
930
+ executionLimits: sandboxExecutionLimits(layout)
821
931
  });
822
932
  return {
823
933
  ...layout,
824
934
  fs: fs3,
825
935
  async exec(command, signal) {
826
- const timeout = AbortSignal.timeout(layout.timeoutMs);
827
936
  try {
828
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
937
+ const result = await bash.exec(command, { signal });
829
938
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
830
939
  } catch (error) {
831
940
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -853,8 +962,11 @@ import path6 from "node:path";
853
962
  // src/skills.ts
854
963
  import { readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
855
964
  import path5 from "node:path";
965
+ import { fileURLToPath } from "node:url";
856
966
  import { parse as parseYaml } from "yaml";
857
967
  var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
968
+ var moduleDirectory = typeof __dirname === "string" ? __dirname : path5.dirname(fileURLToPath(import.meta.url));
969
+ var BUILTIN_SKILLS_DIRECTORY = path5.resolve(moduleDirectory, "..", "resources", "skills");
858
970
  function discoverSkills(roots, logger) {
859
971
  const found = /* @__PURE__ */ new Map();
860
972
  for (const root of roots) {
@@ -929,7 +1041,10 @@ function defaultShell(configured) {
929
1041
  __name(defaultShell, "defaultShell");
930
1042
  function resolveLayout(options) {
931
1043
  const { config } = options;
932
- const skills = discoverSkills([path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))], options.logger);
1044
+ const skills = discoverSkills(
1045
+ [BUILTIN_SKILLS_DIRECTORY, path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))],
1046
+ options.logger
1047
+ );
933
1048
  const workspace = path6.join(options.home, "workspace");
934
1049
  mkdirSync2(workspace, { recursive: true });
935
1050
  const backend = config.backend;
@@ -969,6 +1084,9 @@ Run a bash command inside the sandbox.
969
1084
  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.
970
1085
  {% endif %}
971
1086
  Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
1087
+ {% if not native %}
1088
+ /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.
1089
+ {% endif %}
972
1090
  {% if gitBashPaths %}
973
1091
  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.
974
1092
  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.
@@ -1032,7 +1150,7 @@ function createWorkspaceTools(sandbox) {
1032
1150
  ${output.stdout}`);
1033
1151
  if (output.stderr.length > 0) sections.push(`stderr:
1034
1152
  ${output.stderr}`);
1035
- return { type: "text", value: sections.join("\n\n") };
1153
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
1036
1154
  }, "toModelOutput"),
1037
1155
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
1038
1156
  }),
@@ -1127,6 +1245,7 @@ var WorkspaceExtension = class extends Extension {
1127
1245
  __name(this, "WorkspaceExtension");
1128
1246
  }
1129
1247
  static inject = ["ishiki"];
1248
+ static usage = ``;
1130
1249
  Schema = WorkspaceConfig;
1131
1250
  dataPath;
1132
1251
  constructor(ctx, config) {
@@ -1134,22 +1253,22 @@ var WorkspaceExtension = class extends Extension {
1134
1253
  this.logger.level = config.logLevel;
1135
1254
  this.dataPath = ctx.ishiki.dataPath;
1136
1255
  }
1137
- [Service.invoke](config, context) {
1256
+ [Service.invoke](host, config) {
1138
1257
  const shellOptions = {
1139
1258
  config,
1140
- home: context.home,
1141
- root: context.root,
1259
+ home: host.home,
1260
+ root: host.root,
1142
1261
  dataPath: this.dataPath,
1143
1262
  logger: this.logger,
1144
- resources: context.resources
1263
+ resources: host.resources
1145
1264
  };
1146
1265
  const layout = resolveLayout(shellOptions);
1147
1266
  let shell;
1148
1267
  let tools;
1149
1268
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
1150
1269
  const disposers = [];
1151
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
1152
- disposers.push(context.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1270
+ disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
1271
+ disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
1153
1272
  return {
1154
1273
  name: "ishiki.workspace",
1155
1274
  extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
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,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ishiki-workspace",
3
- "version": "0.0.4",
4
- "description": "Workspace tools for Ishiki: sandboxed or native shell, file tools and skills",
3
+ "version": "0.0.6",
4
+ "description": "workspace tools for Ishiki",
5
5
  "keywords": [
6
6
  "agent",
7
7
  "chatbot",
@@ -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,16 +45,16 @@
44
45
  },
45
46
  "devDependencies": {
46
47
  "koishi": "^4.18.11",
47
- "koishi-plugin-ishiki": ">=0.0.4 <0.1.0"
48
+ "koishi-plugin-ishiki": ">=0.0.6 <0.1.0"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "koishi": "^4.18.11",
51
- "koishi-plugin-ishiki": ">=0.0.4 <0.1.0"
52
+ "koishi-plugin-ishiki": ">=0.0.6 <0.1.0"
52
53
  },
53
54
  "koishi": {
54
55
  "description": {
55
- "zh": "工作区工具插件",
56
- "en": "Workspace tools plugin"
56
+ "zh": "Ishiki 的工作区扩展,提供 bash 沙箱和文件读写能力",
57
+ "en": "Ishiki workspace extension with bash sandbox and file read/write"
57
58
  },
58
59
  "service": {
59
60
  "required": [
@@ -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.