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.
- package/lib/execution-limits.d.ts +5 -0
- package/lib/index.cjs +171 -61
- package/lib/index.d.ts +2 -2
- package/lib/index.js +170 -61
- package/lib/skills.d.ts +1 -0
- package/package.json +5 -4
- package/resources/skills/sandbox-environment/SKILL.md +28 -0
- package/resources/skills/sandbox-environment/references/javascript.md +44 -0
- package/resources/skills/sandbox-environment/references/python.md +14 -0
- package/resources/skills/sandbox-environment/references/shell.md +25 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
589
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
531
590
|
} catch (error) {
|
|
532
|
-
throw failure(error, 'open',
|
|
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(
|
|
597
|
+
return raw.readFileSync(target, options);
|
|
538
598
|
} catch (error) {
|
|
539
|
-
throw failure(error, 'open',
|
|
599
|
+
throw failure(error, 'open', target);
|
|
540
600
|
}
|
|
541
601
|
};
|
|
542
602
|
fs.writeFileSync = function (path, data) {
|
|
543
|
-
|
|
603
|
+
var target = filePath(path);
|
|
604
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
544
605
|
};
|
|
545
606
|
fs.appendFileSync = function (path, data) {
|
|
546
|
-
|
|
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(
|
|
558
|
-
bridge('fsMkdir', 'mkdir',
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
656
|
+
var link = filePath(path);
|
|
657
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
573
658
|
};
|
|
574
659
|
fs.readlinkSync = function (path) {
|
|
575
|
-
|
|
660
|
+
var target = filePath(path);
|
|
661
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
576
662
|
};
|
|
577
663
|
fs.chmodSync = function (path, mode) {
|
|
578
|
-
|
|
664
|
+
var target = filePath(path);
|
|
665
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
579
666
|
};
|
|
580
667
|
fs.realpathSync = function (path) {
|
|
581
|
-
|
|
668
|
+
var target = filePath(path);
|
|
669
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
582
670
|
};
|
|
583
671
|
fs.renameSync = function (from, to) {
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
685
|
+
var target = filePath(path);
|
|
686
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
591
687
|
};
|
|
592
688
|
fs.rmdirSync = function (path, options) {
|
|
593
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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](
|
|
1279
|
+
[import_koishi2.Service.invoke](host, config) {
|
|
1170
1280
|
const shellOptions = {
|
|
1171
1281
|
config,
|
|
1172
|
-
home:
|
|
1173
|
-
root:
|
|
1282
|
+
home: host.home,
|
|
1283
|
+
root: host.root,
|
|
1174
1284
|
dataPath: this.dataPath,
|
|
1175
1285
|
logger: this.logger,
|
|
1176
|
-
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(
|
|
1184
|
-
disposers.push(
|
|
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
|
|
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](
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
557
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
499
558
|
} catch (error) {
|
|
500
|
-
throw failure(error, 'open',
|
|
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(
|
|
565
|
+
return raw.readFileSync(target, options);
|
|
506
566
|
} catch (error) {
|
|
507
|
-
throw failure(error, 'open',
|
|
567
|
+
throw failure(error, 'open', target);
|
|
508
568
|
}
|
|
509
569
|
};
|
|
510
570
|
fs.writeFileSync = function (path, data) {
|
|
511
|
-
|
|
571
|
+
var target = filePath(path);
|
|
572
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
512
573
|
};
|
|
513
574
|
fs.appendFileSync = function (path, data) {
|
|
514
|
-
|
|
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(
|
|
526
|
-
bridge('fsMkdir', 'mkdir',
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
624
|
+
var link = filePath(path);
|
|
625
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
541
626
|
};
|
|
542
627
|
fs.readlinkSync = function (path) {
|
|
543
|
-
|
|
628
|
+
var target = filePath(path);
|
|
629
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
544
630
|
};
|
|
545
631
|
fs.chmodSync = function (path, mode) {
|
|
546
|
-
|
|
632
|
+
var target = filePath(path);
|
|
633
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
547
634
|
};
|
|
548
635
|
fs.realpathSync = function (path) {
|
|
549
|
-
|
|
636
|
+
var target = filePath(path);
|
|
637
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
550
638
|
};
|
|
551
639
|
fs.renameSync = function (from, to) {
|
|
552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
653
|
+
var target = filePath(path);
|
|
654
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
559
655
|
};
|
|
560
656
|
fs.rmdirSync = function (path, options) {
|
|
561
|
-
|
|
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
|
-
|
|
622
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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](
|
|
1246
|
+
[Service.invoke](host, config) {
|
|
1138
1247
|
const shellOptions = {
|
|
1139
1248
|
config,
|
|
1140
|
-
home:
|
|
1141
|
-
root:
|
|
1249
|
+
home: host.home,
|
|
1250
|
+
root: host.root,
|
|
1142
1251
|
dataPath: this.dataPath,
|
|
1143
1252
|
logger: this.logger,
|
|
1144
|
-
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(
|
|
1152
|
-
disposers.push(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|