koishi-plugin-ishiki-workspace 0.0.4 → 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.`
@@ -356,6 +358,18 @@ var WorkspaceScheme = class extends import_koishi_plugin_ishiki2.FileScheme {
356
358
  // src/sandbox.ts
357
359
  var import_node_path4 = __toESM(require("node:path"));
358
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
+
359
373
  // src/js-bootstrap.ts
360
374
  var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
361
375
  (function () {
@@ -394,6 +408,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
394
408
  ELOOP: -40
395
409
  };
396
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
+
397
432
  function failure(error, syscall, target, destination) {
398
433
  if (!(error instanceof Error)) error = new Error(String(error));
399
434
  var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
@@ -410,6 +445,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
410
445
  return error;
411
446
  }
412
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
+
413
464
  function bridge(hostName, syscall, target, args, destination) {
414
465
  try {
415
466
  var operation = host[hostName];
@@ -430,29 +481,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
430
481
  return String(value);
431
482
  }
432
483
 
484
+ function Stats() {}
485
+ function Dirent() {}
486
+
433
487
  function stats(value) {
434
488
  var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
435
489
  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
- };
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();
456
509
  result.isFile = function () { return value.isFile; };
457
510
  result.isDirectory = function () { return value.isDirectory; };
458
511
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -464,7 +517,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
464
517
  }
465
518
 
466
519
  function dirent(name, parentPath, value) {
467
- var result = { name: String(name), parentPath: String(parentPath), path: String(parentPath) };
520
+ var result = Object.create(Dirent.prototype);
521
+ result.name = String(name);
522
+ result.parentPath = String(parentPath);
523
+ result.path = String(parentPath);
468
524
  result.isFile = function () { return value.isFile; };
469
525
  result.isDirectory = function () { return value.isDirectory; };
470
526
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -480,7 +536,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
480
536
  }
481
537
 
482
538
  function readStats(path, follow) {
483
- return bridge('fsStat', follow ? 'stat' : 'lstat', path, [path, !follow]);
539
+ var target = filePath(path);
540
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
484
541
  }
485
542
 
486
543
  function collect(path, relative, options, output) {
@@ -496,10 +553,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
496
553
  }
497
554
 
498
555
  function readdir(path, options) {
556
+ var target = filePath(path);
499
557
  if (typeof options === 'string') options = { encoding: options };
500
558
  options = options || {};
501
559
  var output = [];
502
- collect(path, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
560
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
503
561
  if (options.encoding === 'buffer') return output.map(function (entry) {
504
562
  return typeof entry === 'string' ? Buffer.from(entry) : entry;
505
563
  });
@@ -507,7 +565,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
507
565
  }
508
566
 
509
567
  function firstCreated(path) {
510
- var text = String(path);
568
+ var text = String(filePath(path));
511
569
  var absolute = text.charAt(0) === '/';
512
570
  var parts = text.split('/');
513
571
  var current = '';
@@ -526,24 +584,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
526
584
  }
527
585
 
528
586
  fs.readFileBuffer = function (path) {
587
+ var target = filePath(path);
529
588
  try {
530
- return raw.readFileBuffer(path);
589
+ return Buffer.from(raw.readFileBuffer(target));
531
590
  } catch (error) {
532
- throw failure(error, 'open', path);
591
+ throw failure(error, 'open', target);
533
592
  }
534
593
  };
535
594
  fs.readFileSync = function (path, options) {
595
+ var target = filePath(path);
536
596
  try {
537
- return raw.readFileSync(path, options);
597
+ return raw.readFileSync(target, options);
538
598
  } catch (error) {
539
- throw failure(error, 'open', path);
599
+ throw failure(error, 'open', target);
540
600
  }
541
601
  };
542
602
  fs.writeFileSync = function (path, data) {
543
- bridge('fsWrite', 'open', path, [path, bytes(data)]);
603
+ var target = filePath(path);
604
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
544
605
  };
545
606
  fs.appendFileSync = function (path, data) {
546
- bridge('fsAppend', 'open', path, [path, bytes(data)]);
607
+ var target = filePath(path);
608
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
547
609
  };
548
610
  fs.statSync = function (path) {
549
611
  return stats(readStats(path, true));
@@ -553,45 +615,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
553
615
  };
554
616
  fs.readdirSync = readdir;
555
617
  fs.mkdirSync = function (path, options) {
618
+ var target = filePath(path);
556
619
  var recursive = Boolean(options && typeof options === 'object' && options.recursive);
557
- var created = recursive ? firstCreated(path) : undefined;
558
- bridge('fsMkdir', 'mkdir', path, [path, recursive]);
620
+ var created = recursive ? firstCreated(target) : undefined;
621
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
559
622
  return created;
560
623
  };
561
624
  fs.rmSync = function (path, options) {
562
- bridge('fsRm', 'rm', path, [path, Boolean(options && options.recursive), Boolean(options && options.force)]);
625
+ var target = filePath(path);
626
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
563
627
  };
564
628
  fs.existsSync = function (path) {
629
+ var target = filePath(path);
565
630
  try {
566
- return bridge('fsExists', 'access', path, [path]);
631
+ return bridge('fsExists', 'access', target, [target]);
567
632
  } catch (error) {
568
633
  return false;
569
634
  }
570
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
+ };
571
655
  fs.symlinkSync = function (target, path) {
572
- bridge('fsSymlink', 'symlink', path, [target, path]);
656
+ var link = filePath(path);
657
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
573
658
  };
574
659
  fs.readlinkSync = function (path) {
575
- return bridge('fsReadlink', 'readlink', path, [path]);
660
+ var target = filePath(path);
661
+ return bridge('fsReadlink', 'readlink', target, [target]);
576
662
  };
577
663
  fs.chmodSync = function (path, mode) {
578
- bridge('fsChmod', 'chmod', path, [path, Number(mode)]);
664
+ var target = filePath(path);
665
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
579
666
  };
580
667
  fs.realpathSync = function (path) {
581
- return bridge('fsRealpath', 'realpath', path, [path]);
668
+ var target = filePath(path);
669
+ return bridge('fsRealpath', 'realpath', target, [target]);
582
670
  };
583
671
  fs.renameSync = function (from, to) {
584
- bridge('fsRename', 'rename', from, [from, to], to);
672
+ var source = filePath(from);
673
+ var destination = filePath(to);
674
+ bridge('fsRename', 'rename', source, [source, destination], destination);
585
675
  };
586
- fs.copyFileSync = function (from, to) {
587
- bridge('fsCopy', 'copyfile', from, [from, to], to);
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);
588
683
  };
589
684
  fs.unlinkSync = function (path) {
590
- bridge('fsRm', 'unlink', path, [path, false, false]);
685
+ var target = filePath(path);
686
+ bridge('fsRm', 'unlink', target, [target, false, false]);
591
687
  };
592
688
  fs.rmdirSync = function (path, options) {
593
- bridge('fsRm', 'rmdir', path, [path, Boolean(options && options.recursive), false]);
689
+ var target = filePath(path);
690
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
594
691
  };
692
+ fs.constants = constants;
693
+ fs.Stats = Stats;
694
+ fs.Dirent = Dirent;
595
695
 
596
696
  var callbackNames = [
597
697
  ['readFile', fs.readFileSync],
@@ -648,10 +748,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
648
748
  });
649
749
  };
650
750
  })(promiseNames[index]);
651
- fs.promises.access = function (path) {
751
+ fs.promises.access = function (path, mode) {
652
752
  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));
753
+ Promise.resolve().then(function () {
754
+ fs.accessSync(path, mode);
755
+ }).then(resolve, reject);
655
756
  });
656
757
  };
657
758
 
@@ -849,15 +950,14 @@ async function createSandbox(layout, options) {
849
950
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
850
951
  javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
851
952
  python: layout.python,
852
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
953
+ executionLimits: sandboxExecutionLimits(layout)
853
954
  });
854
955
  return {
855
956
  ...layout,
856
957
  fs: fs3,
857
958
  async exec(command, signal) {
858
- const timeout = AbortSignal.timeout(layout.timeoutMs);
859
959
  try {
860
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
960
+ const result = await bash.exec(command, { signal });
861
961
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
862
962
  } catch (error) {
863
963
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -885,8 +985,12 @@ var import_node_path6 = __toESM(require("node:path"));
885
985
  // src/skills.ts
886
986
  var import_node_fs3 = require("node:fs");
887
987
  var import_node_path5 = __toESM(require("node:path"));
988
+ var import_node_url = require("node:url");
888
989
  var import_yaml = require("yaml");
990
+ var import_meta = {};
889
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");
890
994
  function discoverSkills(roots, logger) {
891
995
  const found = /* @__PURE__ */ new Map();
892
996
  for (const root of roots) {
@@ -961,7 +1065,10 @@ function defaultShell(configured) {
961
1065
  __name(defaultShell, "defaultShell");
962
1066
  function resolveLayout(options) {
963
1067
  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);
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
+ );
965
1072
  const workspace = import_node_path6.default.join(options.home, "workspace");
966
1073
  (0, import_node_fs4.mkdirSync)(workspace, { recursive: true });
967
1074
  const backend = config.backend;
@@ -1001,6 +1108,9 @@ Run a bash command inside the sandbox.
1001
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.
1002
1109
  {% endif %}
1003
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 %}
1004
1114
  {% if gitBashPaths %}
1005
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.
1006
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.
@@ -1064,7 +1174,7 @@ function createWorkspaceTools(sandbox) {
1064
1174
  ${output.stdout}`);
1065
1175
  if (output.stderr.length > 0) sections.push(`stderr:
1066
1176
  ${output.stderr}`);
1067
- return { type: "text", value: sections.join("\n\n") };
1177
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
1068
1178
  }, "toModelOutput"),
1069
1179
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
1070
1180
  }),
@@ -1166,22 +1276,22 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
1166
1276
  this.logger.level = config.logLevel;
1167
1277
  this.dataPath = ctx.ishiki.dataPath;
1168
1278
  }
1169
- [import_koishi2.Service.invoke](config, context) {
1279
+ [import_koishi2.Service.invoke](host, config) {
1170
1280
  const shellOptions = {
1171
1281
  config,
1172
- home: context.home,
1173
- root: context.root,
1282
+ home: host.home,
1283
+ root: host.root,
1174
1284
  dataPath: this.dataPath,
1175
1285
  logger: this.logger,
1176
- resources: context.resources
1286
+ resources: host.resources
1177
1287
  };
1178
1288
  const layout = resolveLayout(shellOptions);
1179
1289
  let shell;
1180
1290
  let tools;
1181
1291
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
1182
1292
  const disposers = [];
1183
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
1184
- 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)));
1185
1295
  return {
1186
1296
  name: "ishiki.workspace",
1187
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.`
@@ -324,6 +326,18 @@ var WorkspaceScheme = class extends FileScheme {
324
326
  // src/sandbox.ts
325
327
  import path4 from "node:path";
326
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
+
327
341
  // src/js-bootstrap.ts
328
342
  var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
329
343
  (function () {
@@ -362,6 +376,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
362
376
  ELOOP: -40
363
377
  };
364
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
+
365
400
  function failure(error, syscall, target, destination) {
366
401
  if (!(error instanceof Error)) error = new Error(String(error));
367
402
  var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
@@ -378,6 +413,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
378
413
  return error;
379
414
  }
380
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
+
381
432
  function bridge(hostName, syscall, target, args, destination) {
382
433
  try {
383
434
  var operation = host[hostName];
@@ -398,29 +449,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
398
449
  return String(value);
399
450
  }
400
451
 
452
+ function Stats() {}
453
+ function Dirent() {}
454
+
401
455
  function stats(value) {
402
456
  var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
403
457
  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
- };
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();
424
477
  result.isFile = function () { return value.isFile; };
425
478
  result.isDirectory = function () { return value.isDirectory; };
426
479
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -432,7 +485,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
432
485
  }
433
486
 
434
487
  function dirent(name, parentPath, value) {
435
- var result = { name: String(name), parentPath: String(parentPath), path: String(parentPath) };
488
+ var result = Object.create(Dirent.prototype);
489
+ result.name = String(name);
490
+ result.parentPath = String(parentPath);
491
+ result.path = String(parentPath);
436
492
  result.isFile = function () { return value.isFile; };
437
493
  result.isDirectory = function () { return value.isDirectory; };
438
494
  result.isSymbolicLink = function () { return value.isSymbolicLink; };
@@ -448,7 +504,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
448
504
  }
449
505
 
450
506
  function readStats(path, follow) {
451
- return bridge('fsStat', follow ? 'stat' : 'lstat', path, [path, !follow]);
507
+ var target = filePath(path);
508
+ return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
452
509
  }
453
510
 
454
511
  function collect(path, relative, options, output) {
@@ -464,10 +521,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
464
521
  }
465
522
 
466
523
  function readdir(path, options) {
524
+ var target = filePath(path);
467
525
  if (typeof options === 'string') options = { encoding: options };
468
526
  options = options || {};
469
527
  var output = [];
470
- collect(path, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
528
+ collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
471
529
  if (options.encoding === 'buffer') return output.map(function (entry) {
472
530
  return typeof entry === 'string' ? Buffer.from(entry) : entry;
473
531
  });
@@ -475,7 +533,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
475
533
  }
476
534
 
477
535
  function firstCreated(path) {
478
- var text = String(path);
536
+ var text = String(filePath(path));
479
537
  var absolute = text.charAt(0) === '/';
480
538
  var parts = text.split('/');
481
539
  var current = '';
@@ -494,24 +552,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
494
552
  }
495
553
 
496
554
  fs.readFileBuffer = function (path) {
555
+ var target = filePath(path);
497
556
  try {
498
- return raw.readFileBuffer(path);
557
+ return Buffer.from(raw.readFileBuffer(target));
499
558
  } catch (error) {
500
- throw failure(error, 'open', path);
559
+ throw failure(error, 'open', target);
501
560
  }
502
561
  };
503
562
  fs.readFileSync = function (path, options) {
563
+ var target = filePath(path);
504
564
  try {
505
- return raw.readFileSync(path, options);
565
+ return raw.readFileSync(target, options);
506
566
  } catch (error) {
507
- throw failure(error, 'open', path);
567
+ throw failure(error, 'open', target);
508
568
  }
509
569
  };
510
570
  fs.writeFileSync = function (path, data) {
511
- bridge('fsWrite', 'open', path, [path, bytes(data)]);
571
+ var target = filePath(path);
572
+ bridge('fsWrite', 'open', target, [target, bytes(data)]);
512
573
  };
513
574
  fs.appendFileSync = function (path, data) {
514
- bridge('fsAppend', 'open', path, [path, bytes(data)]);
575
+ var target = filePath(path);
576
+ bridge('fsAppend', 'open', target, [target, bytes(data)]);
515
577
  };
516
578
  fs.statSync = function (path) {
517
579
  return stats(readStats(path, true));
@@ -521,45 +583,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
521
583
  };
522
584
  fs.readdirSync = readdir;
523
585
  fs.mkdirSync = function (path, options) {
586
+ var target = filePath(path);
524
587
  var recursive = Boolean(options && typeof options === 'object' && options.recursive);
525
- var created = recursive ? firstCreated(path) : undefined;
526
- bridge('fsMkdir', 'mkdir', path, [path, recursive]);
588
+ var created = recursive ? firstCreated(target) : undefined;
589
+ bridge('fsMkdir', 'mkdir', target, [target, recursive]);
527
590
  return created;
528
591
  };
529
592
  fs.rmSync = function (path, options) {
530
- bridge('fsRm', 'rm', path, [path, Boolean(options && options.recursive), Boolean(options && options.force)]);
593
+ var target = filePath(path);
594
+ bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
531
595
  };
532
596
  fs.existsSync = function (path) {
597
+ var target = filePath(path);
533
598
  try {
534
- return bridge('fsExists', 'access', path, [path]);
599
+ return bridge('fsExists', 'access', target, [target]);
535
600
  } catch (error) {
536
601
  return false;
537
602
  }
538
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
+ };
539
623
  fs.symlinkSync = function (target, path) {
540
- bridge('fsSymlink', 'symlink', path, [target, path]);
624
+ var link = filePath(path);
625
+ bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
541
626
  };
542
627
  fs.readlinkSync = function (path) {
543
- return bridge('fsReadlink', 'readlink', path, [path]);
628
+ var target = filePath(path);
629
+ return bridge('fsReadlink', 'readlink', target, [target]);
544
630
  };
545
631
  fs.chmodSync = function (path, mode) {
546
- bridge('fsChmod', 'chmod', path, [path, Number(mode)]);
632
+ var target = filePath(path);
633
+ bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
547
634
  };
548
635
  fs.realpathSync = function (path) {
549
- return bridge('fsRealpath', 'realpath', path, [path]);
636
+ var target = filePath(path);
637
+ return bridge('fsRealpath', 'realpath', target, [target]);
550
638
  };
551
639
  fs.renameSync = function (from, to) {
552
- bridge('fsRename', 'rename', from, [from, to], to);
640
+ var source = filePath(from);
641
+ var destination = filePath(to);
642
+ bridge('fsRename', 'rename', source, [source, destination], destination);
553
643
  };
554
- fs.copyFileSync = function (from, to) {
555
- bridge('fsCopy', 'copyfile', from, [from, to], to);
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);
556
651
  };
557
652
  fs.unlinkSync = function (path) {
558
- bridge('fsRm', 'unlink', path, [path, false, false]);
653
+ var target = filePath(path);
654
+ bridge('fsRm', 'unlink', target, [target, false, false]);
559
655
  };
560
656
  fs.rmdirSync = function (path, options) {
561
- bridge('fsRm', 'rmdir', path, [path, Boolean(options && options.recursive), false]);
657
+ var target = filePath(path);
658
+ bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
562
659
  };
660
+ fs.constants = constants;
661
+ fs.Stats = Stats;
662
+ fs.Dirent = Dirent;
563
663
 
564
664
  var callbackNames = [
565
665
  ['readFile', fs.readFileSync],
@@ -616,10 +716,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
616
716
  });
617
717
  };
618
718
  })(promiseNames[index]);
619
- fs.promises.access = function (path) {
719
+ fs.promises.access = function (path, mode) {
620
720
  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));
721
+ Promise.resolve().then(function () {
722
+ fs.accessSync(path, mode);
723
+ }).then(resolve, reject);
623
724
  });
624
725
  };
625
726
 
@@ -817,15 +918,14 @@ async function createSandbox(layout, options) {
817
918
  network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
818
919
  javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
819
920
  python: layout.python,
820
- executionLimits: { maxOutputSize: layout.maxExecutionOutputBytes }
921
+ executionLimits: sandboxExecutionLimits(layout)
821
922
  });
822
923
  return {
823
924
  ...layout,
824
925
  fs: fs3,
825
926
  async exec(command, signal) {
826
- const timeout = AbortSignal.timeout(layout.timeoutMs);
827
927
  try {
828
- const result = await bash.exec(command, { signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]) });
928
+ const result = await bash.exec(command, { signal });
829
929
  return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
830
930
  } catch (error) {
831
931
  return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
@@ -853,8 +953,11 @@ import path6 from "node:path";
853
953
  // src/skills.ts
854
954
  import { readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
855
955
  import path5 from "node:path";
956
+ import { fileURLToPath } from "node:url";
856
957
  import { parse as parseYaml } from "yaml";
857
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");
858
961
  function discoverSkills(roots, logger) {
859
962
  const found = /* @__PURE__ */ new Map();
860
963
  for (const root of roots) {
@@ -929,7 +1032,10 @@ function defaultShell(configured) {
929
1032
  __name(defaultShell, "defaultShell");
930
1033
  function resolveLayout(options) {
931
1034
  const { config } = options;
932
- 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
+ );
933
1039
  const workspace = path6.join(options.home, "workspace");
934
1040
  mkdirSync2(workspace, { recursive: true });
935
1041
  const backend = config.backend;
@@ -969,6 +1075,9 @@ Run a bash command inside the sandbox.
969
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.
970
1076
  {% endif %}
971
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 %}
972
1081
  {% if gitBashPaths %}
973
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.
974
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.
@@ -1032,7 +1141,7 @@ function createWorkspaceTools(sandbox) {
1032
1141
  ${output.stdout}`);
1033
1142
  if (output.stderr.length > 0) sections.push(`stderr:
1034
1143
  ${output.stderr}`);
1035
- return { type: "text", value: sections.join("\n\n") };
1144
+ return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
1036
1145
  }, "toModelOutput"),
1037
1146
  execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
1038
1147
  }),
@@ -1134,22 +1243,22 @@ var WorkspaceExtension = class extends Extension {
1134
1243
  this.logger.level = config.logLevel;
1135
1244
  this.dataPath = ctx.ishiki.dataPath;
1136
1245
  }
1137
- [Service.invoke](config, context) {
1246
+ [Service.invoke](host, config) {
1138
1247
  const shellOptions = {
1139
1248
  config,
1140
- home: context.home,
1141
- root: context.root,
1249
+ home: host.home,
1250
+ root: host.root,
1142
1251
  dataPath: this.dataPath,
1143
1252
  logger: this.logger,
1144
- resources: context.resources
1253
+ resources: host.resources
1145
1254
  };
1146
1255
  const layout = resolveLayout(shellOptions);
1147
1256
  let shell;
1148
1257
  let tools;
1149
1258
  const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
1150
1259
  const disposers = [];
1151
- disposers.push(context.resources?.attach(new SkillScheme(layout.skills)));
1152
- 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)));
1153
1262
  return {
1154
1263
  name: "ishiki.workspace",
1155
1264
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-ishiki-workspace",
3
- "version": "0.0.4",
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.4 <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.4 <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.