koishi-plugin-ishiki-workspace 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/execution-limits.d.ts +5 -0
- package/lib/index.cjs +437 -24
- package/lib/index.d.ts +2 -2
- package/lib/index.js +429 -17
- package/lib/js-bootstrap.d.ts +5 -0
- 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.`
|
|
@@ -355,7 +357,408 @@ var WorkspaceScheme = class extends import_koishi_plugin_ishiki2.FileScheme {
|
|
|
355
357
|
|
|
356
358
|
// src/sandbox.ts
|
|
357
359
|
var import_node_path4 = __toESM(require("node:path"));
|
|
358
|
-
|
|
360
|
+
|
|
361
|
+
// src/execution-limits.ts
|
|
362
|
+
function sandboxExecutionLimits(layout) {
|
|
363
|
+
return {
|
|
364
|
+
maxOutputSize: layout.maxExecutionOutputBytes,
|
|
365
|
+
maxExecutionTimeMs: layout.timeoutMs,
|
|
366
|
+
maxSqliteTimeoutMs: layout.timeoutMs,
|
|
367
|
+
maxPythonTimeoutMs: layout.timeoutMs,
|
|
368
|
+
maxJsTimeoutMs: layout.timeoutMs
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
__name(sandboxExecutionLimits, "sandboxExecutionLimits");
|
|
372
|
+
|
|
373
|
+
// src/js-bootstrap.ts
|
|
374
|
+
var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
375
|
+
(function () {
|
|
376
|
+
var hostKey = Object.getOwnPropertyNames(globalThis).filter(function (name) {
|
|
377
|
+
return name.indexOf('__jbHost_') === 0;
|
|
378
|
+
})[0];
|
|
379
|
+
if (!hostKey) throw new Error('js-exec host bridge is unavailable');
|
|
380
|
+
var host = globalThis[hostKey];
|
|
381
|
+
var fs = globalThis.fs;
|
|
382
|
+
function unwrap(result) {
|
|
383
|
+
if (!result || result.ok !== true) throw new Error(result && result.error || 'Host operation failed');
|
|
384
|
+
return result.value;
|
|
385
|
+
}
|
|
386
|
+
var raw = {};
|
|
387
|
+
var names = [
|
|
388
|
+
'readFileBuffer', 'readFileSync', 'writeFileSync', 'appendFileSync',
|
|
389
|
+
'statSync', 'lstatSync', 'readdirSync', 'mkdirSync', 'rmSync',
|
|
390
|
+
'symlinkSync', 'readlinkSync', 'chmodSync', 'realpathSync',
|
|
391
|
+
'renameSync', 'copyFileSync'
|
|
392
|
+
];
|
|
393
|
+
for (var index = 0; index < names.length; index++) raw[names[index]] = fs[names[index]];
|
|
394
|
+
|
|
395
|
+
var errnos = {
|
|
396
|
+
EPERM: -1,
|
|
397
|
+
ENOENT: -2,
|
|
398
|
+
EIO: -5,
|
|
399
|
+
EACCES: -13,
|
|
400
|
+
EEXIST: -17,
|
|
401
|
+
ENOTDIR: -20,
|
|
402
|
+
EISDIR: -21,
|
|
403
|
+
EINVAL: -22,
|
|
404
|
+
ENOSPC: -28,
|
|
405
|
+
EROFS: -30,
|
|
406
|
+
ENAMETOOLONG: -36,
|
|
407
|
+
ENOTEMPTY: -39,
|
|
408
|
+
ELOOP: -40
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
var constants = {
|
|
412
|
+
F_OK: 0,
|
|
413
|
+
R_OK: 4,
|
|
414
|
+
W_OK: 2,
|
|
415
|
+
X_OK: 1,
|
|
416
|
+
COPYFILE_EXCL: 1,
|
|
417
|
+
COPYFILE_FICLONE: 2,
|
|
418
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
419
|
+
S_IFMT: 61440,
|
|
420
|
+
S_IFREG: 32768,
|
|
421
|
+
S_IFDIR: 16384,
|
|
422
|
+
S_IFLNK: 40960,
|
|
423
|
+
O_RDONLY: 0,
|
|
424
|
+
O_WRONLY: 1,
|
|
425
|
+
O_RDWR: 2,
|
|
426
|
+
O_CREAT: 64,
|
|
427
|
+
O_EXCL: 128,
|
|
428
|
+
O_TRUNC: 512,
|
|
429
|
+
O_APPEND: 1024
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
function failure(error, syscall, target, destination) {
|
|
433
|
+
if (!(error instanceof Error)) error = new Error(String(error));
|
|
434
|
+
var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
|
|
435
|
+
if (matched) {
|
|
436
|
+
error.code = matched[1];
|
|
437
|
+
if (errnos[error.code] !== undefined) error.errno = errnos[error.code];
|
|
438
|
+
}
|
|
439
|
+
error.syscall = syscall;
|
|
440
|
+
if (target !== undefined) {
|
|
441
|
+
error.path = String(target);
|
|
442
|
+
error.message = error.message.replace(/'[^']*'(?=\s*$)/, "'" + String(target).replace(/'/g, "\\'") + "'");
|
|
443
|
+
}
|
|
444
|
+
if (destination !== undefined) error.dest = String(destination);
|
|
445
|
+
return error;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function filePath(value) {
|
|
449
|
+
if (
|
|
450
|
+
typeof value !== 'object' ||
|
|
451
|
+
value === null ||
|
|
452
|
+
typeof value.href !== 'string' ||
|
|
453
|
+
typeof value.protocol !== 'string'
|
|
454
|
+
) return value;
|
|
455
|
+
if (value.protocol !== 'file:') {
|
|
456
|
+
throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
|
|
457
|
+
}
|
|
458
|
+
if (value.hostname && value.hostname !== 'localhost') {
|
|
459
|
+
throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
|
|
460
|
+
}
|
|
461
|
+
return decodeURIComponent(value.pathname);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function bridge(hostName, syscall, target, args, destination) {
|
|
465
|
+
try {
|
|
466
|
+
var operation = host[hostName];
|
|
467
|
+
if (args.length === 1) return unwrap(operation(args[0]));
|
|
468
|
+
if (args.length === 2) return unwrap(operation(args[0], args[1]));
|
|
469
|
+
if (args.length === 3) return unwrap(operation(args[0], args[1], args[2]));
|
|
470
|
+
return unwrap(operation(args[0], args[1], args[2], args[3]));
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw failure(error, syscall, target, destination);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function bytes(value) {
|
|
477
|
+
if (value && value._data instanceof Uint8Array) return Array.from(value._data);
|
|
478
|
+
if (value && Array.isArray(value._data)) return value._data.slice();
|
|
479
|
+
if (Array.isArray(value)) return value.slice();
|
|
480
|
+
if (value instanceof Uint8Array) return Array.from(value);
|
|
481
|
+
return String(value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function Stats() {}
|
|
485
|
+
function Dirent() {}
|
|
486
|
+
|
|
487
|
+
function stats(value) {
|
|
488
|
+
var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
|
|
489
|
+
var size = Number(value.size);
|
|
490
|
+
var result = Object.create(Stats.prototype);
|
|
491
|
+
result.dev = 0;
|
|
492
|
+
result.ino = 0;
|
|
493
|
+
result.mode = Number(value.mode);
|
|
494
|
+
result.nlink = value.isDirectory ? 2 : 1;
|
|
495
|
+
result.uid = 1000;
|
|
496
|
+
result.gid = 1000;
|
|
497
|
+
result.rdev = 0;
|
|
498
|
+
result.size = size;
|
|
499
|
+
result.blksize = 4096;
|
|
500
|
+
result.blocks = Math.ceil(size / 512);
|
|
501
|
+
result.atime = mtime;
|
|
502
|
+
result.mtime = mtime;
|
|
503
|
+
result.ctime = mtime;
|
|
504
|
+
result.birthtime = mtime;
|
|
505
|
+
result.atimeMs = mtime.getTime();
|
|
506
|
+
result.mtimeMs = mtime.getTime();
|
|
507
|
+
result.ctimeMs = mtime.getTime();
|
|
508
|
+
result.birthtimeMs = mtime.getTime();
|
|
509
|
+
result.isFile = function () { return value.isFile; };
|
|
510
|
+
result.isDirectory = function () { return value.isDirectory; };
|
|
511
|
+
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
512
|
+
result.isBlockDevice = function () { return false; };
|
|
513
|
+
result.isCharacterDevice = function () { return false; };
|
|
514
|
+
result.isFIFO = function () { return false; };
|
|
515
|
+
result.isSocket = function () { return false; };
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function dirent(name, parentPath, value) {
|
|
520
|
+
var result = Object.create(Dirent.prototype);
|
|
521
|
+
result.name = String(name);
|
|
522
|
+
result.parentPath = String(parentPath);
|
|
523
|
+
result.path = String(parentPath);
|
|
524
|
+
result.isFile = function () { return value.isFile; };
|
|
525
|
+
result.isDirectory = function () { return value.isDirectory; };
|
|
526
|
+
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
527
|
+
result.isBlockDevice = function () { return false; };
|
|
528
|
+
result.isCharacterDevice = function () { return false; };
|
|
529
|
+
result.isFIFO = function () { return false; };
|
|
530
|
+
result.isSocket = function () { return false; };
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function childPath(parent, name) {
|
|
535
|
+
return parent.charAt(parent.length - 1) === '/' ? parent + name : parent + '/' + name;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function readStats(path, follow) {
|
|
539
|
+
var target = filePath(path);
|
|
540
|
+
return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function collect(path, relative, options, output) {
|
|
544
|
+
var listing = bridge('fsReaddir', 'scandir', path, [path]);
|
|
545
|
+
for (var index = 0; index < listing.length; index++) {
|
|
546
|
+
var name = listing[index];
|
|
547
|
+
var nested = relative === '' ? name : relative + '/' + name;
|
|
548
|
+
var full = childPath(path, name);
|
|
549
|
+
var value = readStats(full, false);
|
|
550
|
+
output.push(options.withFileTypes ? dirent(name, path, value) : nested);
|
|
551
|
+
if (options.recursive && value.isDirectory) collect(full, nested, options, output);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function readdir(path, options) {
|
|
556
|
+
var target = filePath(path);
|
|
557
|
+
if (typeof options === 'string') options = { encoding: options };
|
|
558
|
+
options = options || {};
|
|
559
|
+
var output = [];
|
|
560
|
+
collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
|
|
561
|
+
if (options.encoding === 'buffer') return output.map(function (entry) {
|
|
562
|
+
return typeof entry === 'string' ? Buffer.from(entry) : entry;
|
|
563
|
+
});
|
|
564
|
+
return output;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function firstCreated(path) {
|
|
568
|
+
var text = String(filePath(path));
|
|
569
|
+
var absolute = text.charAt(0) === '/';
|
|
570
|
+
var parts = text.split('/');
|
|
571
|
+
var current = '';
|
|
572
|
+
var found;
|
|
573
|
+
for (var index = 0; index < parts.length; index++) {
|
|
574
|
+
var part = parts[index];
|
|
575
|
+
if (part === '' || part === '.') continue;
|
|
576
|
+
if (current === '') current = absolute ? '/' + part : part;
|
|
577
|
+
else current = childPath(current, part);
|
|
578
|
+
if (!fs.existsSync(current)) {
|
|
579
|
+
found = current;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return found;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fs.readFileBuffer = function (path) {
|
|
587
|
+
var target = filePath(path);
|
|
588
|
+
try {
|
|
589
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
590
|
+
} catch (error) {
|
|
591
|
+
throw failure(error, 'open', target);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
fs.readFileSync = function (path, options) {
|
|
595
|
+
var target = filePath(path);
|
|
596
|
+
try {
|
|
597
|
+
return raw.readFileSync(target, options);
|
|
598
|
+
} catch (error) {
|
|
599
|
+
throw failure(error, 'open', target);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
fs.writeFileSync = function (path, data) {
|
|
603
|
+
var target = filePath(path);
|
|
604
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
605
|
+
};
|
|
606
|
+
fs.appendFileSync = function (path, data) {
|
|
607
|
+
var target = filePath(path);
|
|
608
|
+
bridge('fsAppend', 'open', target, [target, bytes(data)]);
|
|
609
|
+
};
|
|
610
|
+
fs.statSync = function (path) {
|
|
611
|
+
return stats(readStats(path, true));
|
|
612
|
+
};
|
|
613
|
+
fs.lstatSync = function (path) {
|
|
614
|
+
return stats(readStats(path, false));
|
|
615
|
+
};
|
|
616
|
+
fs.readdirSync = readdir;
|
|
617
|
+
fs.mkdirSync = function (path, options) {
|
|
618
|
+
var target = filePath(path);
|
|
619
|
+
var recursive = Boolean(options && typeof options === 'object' && options.recursive);
|
|
620
|
+
var created = recursive ? firstCreated(target) : undefined;
|
|
621
|
+
bridge('fsMkdir', 'mkdir', target, [target, recursive]);
|
|
622
|
+
return created;
|
|
623
|
+
};
|
|
624
|
+
fs.rmSync = function (path, options) {
|
|
625
|
+
var target = filePath(path);
|
|
626
|
+
bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
|
|
627
|
+
};
|
|
628
|
+
fs.existsSync = function (path) {
|
|
629
|
+
var target = filePath(path);
|
|
630
|
+
try {
|
|
631
|
+
return bridge('fsExists', 'access', target, [target]);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
fs.accessSync = function (path, mode) {
|
|
637
|
+
var target = filePath(path);
|
|
638
|
+
if (!bridge('fsExists', 'access', target, [target])) {
|
|
639
|
+
throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
|
|
640
|
+
}
|
|
641
|
+
var flags = mode === undefined ? 0 : Number(mode);
|
|
642
|
+
if ((flags & ~7) !== 0) {
|
|
643
|
+
throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
|
|
644
|
+
}
|
|
645
|
+
if (flags === 0) return;
|
|
646
|
+
var permissions = stats(readStats(target, true)).mode & 511;
|
|
647
|
+
var required = 0;
|
|
648
|
+
if (flags & constants.R_OK) required |= 256;
|
|
649
|
+
if (flags & constants.W_OK) required |= 128;
|
|
650
|
+
if (flags & constants.X_OK) required |= 64;
|
|
651
|
+
if ((permissions & required) !== required) {
|
|
652
|
+
throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
fs.symlinkSync = function (target, path) {
|
|
656
|
+
var link = filePath(path);
|
|
657
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
658
|
+
};
|
|
659
|
+
fs.readlinkSync = function (path) {
|
|
660
|
+
var target = filePath(path);
|
|
661
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
662
|
+
};
|
|
663
|
+
fs.chmodSync = function (path, mode) {
|
|
664
|
+
var target = filePath(path);
|
|
665
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
666
|
+
};
|
|
667
|
+
fs.realpathSync = function (path) {
|
|
668
|
+
var target = filePath(path);
|
|
669
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
670
|
+
};
|
|
671
|
+
fs.renameSync = function (from, to) {
|
|
672
|
+
var source = filePath(from);
|
|
673
|
+
var destination = filePath(to);
|
|
674
|
+
bridge('fsRename', 'rename', source, [source, destination], destination);
|
|
675
|
+
};
|
|
676
|
+
fs.copyFileSync = function (from, to, mode) {
|
|
677
|
+
var source = filePath(from);
|
|
678
|
+
var destination = filePath(to);
|
|
679
|
+
if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
|
|
680
|
+
throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
|
|
681
|
+
}
|
|
682
|
+
bridge('fsCopy', 'copyfile', source, [source, destination], destination);
|
|
683
|
+
};
|
|
684
|
+
fs.unlinkSync = function (path) {
|
|
685
|
+
var target = filePath(path);
|
|
686
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
687
|
+
};
|
|
688
|
+
fs.rmdirSync = function (path, options) {
|
|
689
|
+
var target = filePath(path);
|
|
690
|
+
bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
|
|
691
|
+
};
|
|
692
|
+
fs.constants = constants;
|
|
693
|
+
fs.Stats = Stats;
|
|
694
|
+
fs.Dirent = Dirent;
|
|
695
|
+
|
|
696
|
+
var callbackNames = [
|
|
697
|
+
['readFile', fs.readFileSync],
|
|
698
|
+
['writeFile', fs.writeFileSync],
|
|
699
|
+
['appendFile', fs.appendFileSync],
|
|
700
|
+
['stat', fs.statSync],
|
|
701
|
+
['lstat', fs.lstatSync],
|
|
702
|
+
['readdir', fs.readdirSync],
|
|
703
|
+
['mkdir', fs.mkdirSync],
|
|
704
|
+
['rm', fs.rmSync],
|
|
705
|
+
['symlink', fs.symlinkSync],
|
|
706
|
+
['readlink', fs.readlinkSync],
|
|
707
|
+
['chmod', fs.chmodSync],
|
|
708
|
+
['realpath', fs.realpathSync],
|
|
709
|
+
['rename', fs.renameSync],
|
|
710
|
+
['copyFile', fs.copyFileSync],
|
|
711
|
+
['unlink', fs.unlinkSync],
|
|
712
|
+
['rmdir', fs.rmdirSync]
|
|
713
|
+
];
|
|
714
|
+
function callbackify(name, sync) {
|
|
715
|
+
fs[name] = function () {
|
|
716
|
+
var args = Array.prototype.slice.call(arguments);
|
|
717
|
+
var callback = args.pop();
|
|
718
|
+
if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
|
|
719
|
+
Promise.resolve().then(function () {
|
|
720
|
+
return sync.apply(fs, args);
|
|
721
|
+
}).then(function (value) {
|
|
722
|
+
callback(null, value);
|
|
723
|
+
}, function (error) {
|
|
724
|
+
callback(error);
|
|
725
|
+
});
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
for (var index = 0; index < callbackNames.length; index++) callbackify(callbackNames[index][0], callbackNames[index][1]);
|
|
729
|
+
fs.exists = function (path, callback) {
|
|
730
|
+
if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
|
|
731
|
+
Promise.resolve().then(function () { callback(fs.existsSync(path)); });
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
var promiseNames = [
|
|
735
|
+
'readFile', 'writeFile', 'appendFile', 'stat', 'lstat', 'readdir',
|
|
736
|
+
'mkdir', 'rm', 'symlink', 'readlink', 'chmod', 'realpath',
|
|
737
|
+
'rename', 'copyFile', 'unlink', 'rmdir'
|
|
738
|
+
];
|
|
739
|
+
fs.promises = {};
|
|
740
|
+
for (var index = 0; index < promiseNames.length; index++) (function (name) {
|
|
741
|
+
var sync = fs[name + 'Sync'];
|
|
742
|
+
fs.promises[name] = function () {
|
|
743
|
+
var args = arguments;
|
|
744
|
+
return new Promise(function (resolve, reject) {
|
|
745
|
+
Promise.resolve().then(function () {
|
|
746
|
+
return sync.apply(fs, args);
|
|
747
|
+
}).then(resolve, reject);
|
|
748
|
+
});
|
|
749
|
+
};
|
|
750
|
+
})(promiseNames[index]);
|
|
751
|
+
fs.promises.access = function (path, mode) {
|
|
752
|
+
return new Promise(function (resolve, reject) {
|
|
753
|
+
Promise.resolve().then(function () {
|
|
754
|
+
fs.accessSync(path, mode);
|
|
755
|
+
}).then(resolve, reject);
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
globalThis.fs = fs;
|
|
760
|
+
})();
|
|
761
|
+
`;
|
|
359
762
|
|
|
360
763
|
// src/resource-fs.ts
|
|
361
764
|
var EROFS = /* @__PURE__ */ __name(() => new Error("EROFS: resource mounts are read-only"), "EROFS");
|
|
@@ -513,8 +916,9 @@ function traversable(stat2) {
|
|
|
513
916
|
}
|
|
514
917
|
__name(traversable, "traversable");
|
|
515
918
|
async function createSandbox(layout, options) {
|
|
919
|
+
const { Bash, InMemoryFs, MountableFs, OverlayFs, ReadWriteFs } = await import("just-bash");
|
|
516
920
|
const { home } = options;
|
|
517
|
-
class Writable extends
|
|
921
|
+
class Writable extends ReadWriteFs {
|
|
518
922
|
static {
|
|
519
923
|
__name(this, "Writable");
|
|
520
924
|
}
|
|
@@ -525,36 +929,35 @@ async function createSandbox(layout, options) {
|
|
|
525
929
|
return traversable(await super.lstat(path7));
|
|
526
930
|
}
|
|
527
931
|
}
|
|
528
|
-
const homeView = new
|
|
529
|
-
base: new
|
|
530
|
-
mounts: [{ mountPoint: HOME_MOUNT, filesystem: new
|
|
932
|
+
const homeView = new MountableFs({
|
|
933
|
+
base: new InMemoryFs(),
|
|
934
|
+
mounts: [{ mountPoint: HOME_MOUNT, filesystem: new OverlayFs({ root: home, mountPoint: "/", readOnly: true }) }]
|
|
531
935
|
});
|
|
532
|
-
const fs3 = new
|
|
936
|
+
const fs3 = new MountableFs({
|
|
533
937
|
base: homeView,
|
|
534
938
|
mounts: [
|
|
535
939
|
{ mountPoint: WORKSPACE_MOUNT, filesystem: new Writable({ root: import_node_path4.default.join(home, "workspace") }) },
|
|
536
940
|
...options.resources ? [{ mountPoint: ISHIKI_MOUNT, filesystem: new ResourceFs(options.resources) }] : [],
|
|
537
941
|
...layout.mounts.map((mount) => ({
|
|
538
942
|
mountPoint: mount.target,
|
|
539
|
-
filesystem: mount.readOnly ? new
|
|
943
|
+
filesystem: mount.readOnly ? new OverlayFs({ root: mount.source, mountPoint: "/", readOnly: true }) : new Writable({ root: mount.source })
|
|
540
944
|
}))
|
|
541
945
|
]
|
|
542
946
|
});
|
|
543
|
-
const bash = new
|
|
947
|
+
const bash = new Bash({
|
|
544
948
|
fs: fs3,
|
|
545
949
|
cwd: layout.cwd,
|
|
546
950
|
network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
|
|
547
|
-
javascript: layout.javascript,
|
|
951
|
+
javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
|
|
548
952
|
python: layout.python,
|
|
549
|
-
executionLimits:
|
|
953
|
+
executionLimits: sandboxExecutionLimits(layout)
|
|
550
954
|
});
|
|
551
955
|
return {
|
|
552
956
|
...layout,
|
|
553
957
|
fs: fs3,
|
|
554
958
|
async exec(command, signal) {
|
|
555
|
-
const timeout = AbortSignal.timeout(layout.timeoutMs);
|
|
556
959
|
try {
|
|
557
|
-
const result = await bash.exec(command, { signal
|
|
960
|
+
const result = await bash.exec(command, { signal });
|
|
558
961
|
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
|
|
559
962
|
} catch (error) {
|
|
560
963
|
return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
|
|
@@ -582,8 +985,12 @@ var import_node_path6 = __toESM(require("node:path"));
|
|
|
582
985
|
// src/skills.ts
|
|
583
986
|
var import_node_fs3 = require("node:fs");
|
|
584
987
|
var import_node_path5 = __toESM(require("node:path"));
|
|
988
|
+
var import_node_url = require("node:url");
|
|
585
989
|
var import_yaml = require("yaml");
|
|
990
|
+
var import_meta = {};
|
|
586
991
|
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
992
|
+
var moduleDirectory = typeof __dirname === "string" ? __dirname : import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
993
|
+
var BUILTIN_SKILLS_DIRECTORY = import_node_path5.default.resolve(moduleDirectory, "..", "resources", "skills");
|
|
587
994
|
function discoverSkills(roots, logger) {
|
|
588
995
|
const found = /* @__PURE__ */ new Map();
|
|
589
996
|
for (const root of roots) {
|
|
@@ -658,7 +1065,10 @@ function defaultShell(configured) {
|
|
|
658
1065
|
__name(defaultShell, "defaultShell");
|
|
659
1066
|
function resolveLayout(options) {
|
|
660
1067
|
const { config } = options;
|
|
661
|
-
const skills = discoverSkills(
|
|
1068
|
+
const skills = discoverSkills(
|
|
1069
|
+
[BUILTIN_SKILLS_DIRECTORY, import_node_path6.default.join(options.dataPath, "skills"), ...config.skills.map((dir) => import_node_path6.default.resolve(options.root, dir))],
|
|
1070
|
+
options.logger
|
|
1071
|
+
);
|
|
662
1072
|
const workspace = import_node_path6.default.join(options.home, "workspace");
|
|
663
1073
|
(0, import_node_fs4.mkdirSync)(workspace, { recursive: true });
|
|
664
1074
|
const backend = config.backend;
|
|
@@ -698,6 +1108,9 @@ Run a bash command inside the sandbox.
|
|
|
698
1108
|
The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
|
|
699
1109
|
{% endif %}
|
|
700
1110
|
Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
|
|
1111
|
+
{% if not native %}
|
|
1112
|
+
/tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
1113
|
+
{% endif %}
|
|
701
1114
|
{% if gitBashPaths %}
|
|
702
1115
|
Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
|
|
703
1116
|
Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
|
|
@@ -761,7 +1174,7 @@ function createWorkspaceTools(sandbox) {
|
|
|
761
1174
|
${output.stdout}`);
|
|
762
1175
|
if (output.stderr.length > 0) sections.push(`stderr:
|
|
763
1176
|
${output.stderr}`);
|
|
764
|
-
return { type: "text", value: sections.join("\n\n") };
|
|
1177
|
+
return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
|
|
765
1178
|
}, "toModelOutput"),
|
|
766
1179
|
execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
|
|
767
1180
|
}),
|
|
@@ -863,22 +1276,22 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
|
|
|
863
1276
|
this.logger.level = config.logLevel;
|
|
864
1277
|
this.dataPath = ctx.ishiki.dataPath;
|
|
865
1278
|
}
|
|
866
|
-
[import_koishi2.Service.invoke](
|
|
1279
|
+
[import_koishi2.Service.invoke](host, config) {
|
|
867
1280
|
const shellOptions = {
|
|
868
1281
|
config,
|
|
869
|
-
home:
|
|
870
|
-
root:
|
|
1282
|
+
home: host.home,
|
|
1283
|
+
root: host.root,
|
|
871
1284
|
dataPath: this.dataPath,
|
|
872
1285
|
logger: this.logger,
|
|
873
|
-
resources:
|
|
1286
|
+
resources: host.resources
|
|
874
1287
|
};
|
|
875
1288
|
const layout = resolveLayout(shellOptions);
|
|
876
1289
|
let shell;
|
|
877
1290
|
let tools;
|
|
878
1291
|
const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
|
|
879
1292
|
const disposers = [];
|
|
880
|
-
disposers.push(
|
|
881
|
-
disposers.push(
|
|
1293
|
+
disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
|
|
1294
|
+
disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
|
|
882
1295
|
return {
|
|
883
1296
|
name: "ishiki.workspace",
|
|
884
1297
|
extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Schema, Service, type Context } from "koishi";
|
|
2
|
-
import { Extension, type
|
|
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.`
|
|
@@ -323,7 +325,408 @@ var WorkspaceScheme = class extends FileScheme {
|
|
|
323
325
|
|
|
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
|
+
|
|
341
|
+
// src/js-bootstrap.ts
|
|
342
|
+
var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
343
|
+
(function () {
|
|
344
|
+
var hostKey = Object.getOwnPropertyNames(globalThis).filter(function (name) {
|
|
345
|
+
return name.indexOf('__jbHost_') === 0;
|
|
346
|
+
})[0];
|
|
347
|
+
if (!hostKey) throw new Error('js-exec host bridge is unavailable');
|
|
348
|
+
var host = globalThis[hostKey];
|
|
349
|
+
var fs = globalThis.fs;
|
|
350
|
+
function unwrap(result) {
|
|
351
|
+
if (!result || result.ok !== true) throw new Error(result && result.error || 'Host operation failed');
|
|
352
|
+
return result.value;
|
|
353
|
+
}
|
|
354
|
+
var raw = {};
|
|
355
|
+
var names = [
|
|
356
|
+
'readFileBuffer', 'readFileSync', 'writeFileSync', 'appendFileSync',
|
|
357
|
+
'statSync', 'lstatSync', 'readdirSync', 'mkdirSync', 'rmSync',
|
|
358
|
+
'symlinkSync', 'readlinkSync', 'chmodSync', 'realpathSync',
|
|
359
|
+
'renameSync', 'copyFileSync'
|
|
360
|
+
];
|
|
361
|
+
for (var index = 0; index < names.length; index++) raw[names[index]] = fs[names[index]];
|
|
362
|
+
|
|
363
|
+
var errnos = {
|
|
364
|
+
EPERM: -1,
|
|
365
|
+
ENOENT: -2,
|
|
366
|
+
EIO: -5,
|
|
367
|
+
EACCES: -13,
|
|
368
|
+
EEXIST: -17,
|
|
369
|
+
ENOTDIR: -20,
|
|
370
|
+
EISDIR: -21,
|
|
371
|
+
EINVAL: -22,
|
|
372
|
+
ENOSPC: -28,
|
|
373
|
+
EROFS: -30,
|
|
374
|
+
ENAMETOOLONG: -36,
|
|
375
|
+
ENOTEMPTY: -39,
|
|
376
|
+
ELOOP: -40
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
var constants = {
|
|
380
|
+
F_OK: 0,
|
|
381
|
+
R_OK: 4,
|
|
382
|
+
W_OK: 2,
|
|
383
|
+
X_OK: 1,
|
|
384
|
+
COPYFILE_EXCL: 1,
|
|
385
|
+
COPYFILE_FICLONE: 2,
|
|
386
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
387
|
+
S_IFMT: 61440,
|
|
388
|
+
S_IFREG: 32768,
|
|
389
|
+
S_IFDIR: 16384,
|
|
390
|
+
S_IFLNK: 40960,
|
|
391
|
+
O_RDONLY: 0,
|
|
392
|
+
O_WRONLY: 1,
|
|
393
|
+
O_RDWR: 2,
|
|
394
|
+
O_CREAT: 64,
|
|
395
|
+
O_EXCL: 128,
|
|
396
|
+
O_TRUNC: 512,
|
|
397
|
+
O_APPEND: 1024
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
function failure(error, syscall, target, destination) {
|
|
401
|
+
if (!(error instanceof Error)) error = new Error(String(error));
|
|
402
|
+
var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
|
|
403
|
+
if (matched) {
|
|
404
|
+
error.code = matched[1];
|
|
405
|
+
if (errnos[error.code] !== undefined) error.errno = errnos[error.code];
|
|
406
|
+
}
|
|
407
|
+
error.syscall = syscall;
|
|
408
|
+
if (target !== undefined) {
|
|
409
|
+
error.path = String(target);
|
|
410
|
+
error.message = error.message.replace(/'[^']*'(?=\s*$)/, "'" + String(target).replace(/'/g, "\\'") + "'");
|
|
411
|
+
}
|
|
412
|
+
if (destination !== undefined) error.dest = String(destination);
|
|
413
|
+
return error;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function filePath(value) {
|
|
417
|
+
if (
|
|
418
|
+
typeof value !== 'object' ||
|
|
419
|
+
value === null ||
|
|
420
|
+
typeof value.href !== 'string' ||
|
|
421
|
+
typeof value.protocol !== 'string'
|
|
422
|
+
) return value;
|
|
423
|
+
if (value.protocol !== 'file:') {
|
|
424
|
+
throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
|
|
425
|
+
}
|
|
426
|
+
if (value.hostname && value.hostname !== 'localhost') {
|
|
427
|
+
throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
|
|
428
|
+
}
|
|
429
|
+
return decodeURIComponent(value.pathname);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function bridge(hostName, syscall, target, args, destination) {
|
|
433
|
+
try {
|
|
434
|
+
var operation = host[hostName];
|
|
435
|
+
if (args.length === 1) return unwrap(operation(args[0]));
|
|
436
|
+
if (args.length === 2) return unwrap(operation(args[0], args[1]));
|
|
437
|
+
if (args.length === 3) return unwrap(operation(args[0], args[1], args[2]));
|
|
438
|
+
return unwrap(operation(args[0], args[1], args[2], args[3]));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
throw failure(error, syscall, target, destination);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function bytes(value) {
|
|
445
|
+
if (value && value._data instanceof Uint8Array) return Array.from(value._data);
|
|
446
|
+
if (value && Array.isArray(value._data)) return value._data.slice();
|
|
447
|
+
if (Array.isArray(value)) return value.slice();
|
|
448
|
+
if (value instanceof Uint8Array) return Array.from(value);
|
|
449
|
+
return String(value);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function Stats() {}
|
|
453
|
+
function Dirent() {}
|
|
454
|
+
|
|
455
|
+
function stats(value) {
|
|
456
|
+
var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
|
|
457
|
+
var size = Number(value.size);
|
|
458
|
+
var result = Object.create(Stats.prototype);
|
|
459
|
+
result.dev = 0;
|
|
460
|
+
result.ino = 0;
|
|
461
|
+
result.mode = Number(value.mode);
|
|
462
|
+
result.nlink = value.isDirectory ? 2 : 1;
|
|
463
|
+
result.uid = 1000;
|
|
464
|
+
result.gid = 1000;
|
|
465
|
+
result.rdev = 0;
|
|
466
|
+
result.size = size;
|
|
467
|
+
result.blksize = 4096;
|
|
468
|
+
result.blocks = Math.ceil(size / 512);
|
|
469
|
+
result.atime = mtime;
|
|
470
|
+
result.mtime = mtime;
|
|
471
|
+
result.ctime = mtime;
|
|
472
|
+
result.birthtime = mtime;
|
|
473
|
+
result.atimeMs = mtime.getTime();
|
|
474
|
+
result.mtimeMs = mtime.getTime();
|
|
475
|
+
result.ctimeMs = mtime.getTime();
|
|
476
|
+
result.birthtimeMs = mtime.getTime();
|
|
477
|
+
result.isFile = function () { return value.isFile; };
|
|
478
|
+
result.isDirectory = function () { return value.isDirectory; };
|
|
479
|
+
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
480
|
+
result.isBlockDevice = function () { return false; };
|
|
481
|
+
result.isCharacterDevice = function () { return false; };
|
|
482
|
+
result.isFIFO = function () { return false; };
|
|
483
|
+
result.isSocket = function () { return false; };
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function dirent(name, parentPath, value) {
|
|
488
|
+
var result = Object.create(Dirent.prototype);
|
|
489
|
+
result.name = String(name);
|
|
490
|
+
result.parentPath = String(parentPath);
|
|
491
|
+
result.path = String(parentPath);
|
|
492
|
+
result.isFile = function () { return value.isFile; };
|
|
493
|
+
result.isDirectory = function () { return value.isDirectory; };
|
|
494
|
+
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
495
|
+
result.isBlockDevice = function () { return false; };
|
|
496
|
+
result.isCharacterDevice = function () { return false; };
|
|
497
|
+
result.isFIFO = function () { return false; };
|
|
498
|
+
result.isSocket = function () { return false; };
|
|
499
|
+
return result;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function childPath(parent, name) {
|
|
503
|
+
return parent.charAt(parent.length - 1) === '/' ? parent + name : parent + '/' + name;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function readStats(path, follow) {
|
|
507
|
+
var target = filePath(path);
|
|
508
|
+
return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function collect(path, relative, options, output) {
|
|
512
|
+
var listing = bridge('fsReaddir', 'scandir', path, [path]);
|
|
513
|
+
for (var index = 0; index < listing.length; index++) {
|
|
514
|
+
var name = listing[index];
|
|
515
|
+
var nested = relative === '' ? name : relative + '/' + name;
|
|
516
|
+
var full = childPath(path, name);
|
|
517
|
+
var value = readStats(full, false);
|
|
518
|
+
output.push(options.withFileTypes ? dirent(name, path, value) : nested);
|
|
519
|
+
if (options.recursive && value.isDirectory) collect(full, nested, options, output);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function readdir(path, options) {
|
|
524
|
+
var target = filePath(path);
|
|
525
|
+
if (typeof options === 'string') options = { encoding: options };
|
|
526
|
+
options = options || {};
|
|
527
|
+
var output = [];
|
|
528
|
+
collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
|
|
529
|
+
if (options.encoding === 'buffer') return output.map(function (entry) {
|
|
530
|
+
return typeof entry === 'string' ? Buffer.from(entry) : entry;
|
|
531
|
+
});
|
|
532
|
+
return output;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function firstCreated(path) {
|
|
536
|
+
var text = String(filePath(path));
|
|
537
|
+
var absolute = text.charAt(0) === '/';
|
|
538
|
+
var parts = text.split('/');
|
|
539
|
+
var current = '';
|
|
540
|
+
var found;
|
|
541
|
+
for (var index = 0; index < parts.length; index++) {
|
|
542
|
+
var part = parts[index];
|
|
543
|
+
if (part === '' || part === '.') continue;
|
|
544
|
+
if (current === '') current = absolute ? '/' + part : part;
|
|
545
|
+
else current = childPath(current, part);
|
|
546
|
+
if (!fs.existsSync(current)) {
|
|
547
|
+
found = current;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
fs.readFileBuffer = function (path) {
|
|
555
|
+
var target = filePath(path);
|
|
556
|
+
try {
|
|
557
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
558
|
+
} catch (error) {
|
|
559
|
+
throw failure(error, 'open', target);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
fs.readFileSync = function (path, options) {
|
|
563
|
+
var target = filePath(path);
|
|
564
|
+
try {
|
|
565
|
+
return raw.readFileSync(target, options);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
throw failure(error, 'open', target);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
fs.writeFileSync = function (path, data) {
|
|
571
|
+
var target = filePath(path);
|
|
572
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
573
|
+
};
|
|
574
|
+
fs.appendFileSync = function (path, data) {
|
|
575
|
+
var target = filePath(path);
|
|
576
|
+
bridge('fsAppend', 'open', target, [target, bytes(data)]);
|
|
577
|
+
};
|
|
578
|
+
fs.statSync = function (path) {
|
|
579
|
+
return stats(readStats(path, true));
|
|
580
|
+
};
|
|
581
|
+
fs.lstatSync = function (path) {
|
|
582
|
+
return stats(readStats(path, false));
|
|
583
|
+
};
|
|
584
|
+
fs.readdirSync = readdir;
|
|
585
|
+
fs.mkdirSync = function (path, options) {
|
|
586
|
+
var target = filePath(path);
|
|
587
|
+
var recursive = Boolean(options && typeof options === 'object' && options.recursive);
|
|
588
|
+
var created = recursive ? firstCreated(target) : undefined;
|
|
589
|
+
bridge('fsMkdir', 'mkdir', target, [target, recursive]);
|
|
590
|
+
return created;
|
|
591
|
+
};
|
|
592
|
+
fs.rmSync = function (path, options) {
|
|
593
|
+
var target = filePath(path);
|
|
594
|
+
bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
|
|
595
|
+
};
|
|
596
|
+
fs.existsSync = function (path) {
|
|
597
|
+
var target = filePath(path);
|
|
598
|
+
try {
|
|
599
|
+
return bridge('fsExists', 'access', target, [target]);
|
|
600
|
+
} catch (error) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
fs.accessSync = function (path, mode) {
|
|
605
|
+
var target = filePath(path);
|
|
606
|
+
if (!bridge('fsExists', 'access', target, [target])) {
|
|
607
|
+
throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
|
|
608
|
+
}
|
|
609
|
+
var flags = mode === undefined ? 0 : Number(mode);
|
|
610
|
+
if ((flags & ~7) !== 0) {
|
|
611
|
+
throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
|
|
612
|
+
}
|
|
613
|
+
if (flags === 0) return;
|
|
614
|
+
var permissions = stats(readStats(target, true)).mode & 511;
|
|
615
|
+
var required = 0;
|
|
616
|
+
if (flags & constants.R_OK) required |= 256;
|
|
617
|
+
if (flags & constants.W_OK) required |= 128;
|
|
618
|
+
if (flags & constants.X_OK) required |= 64;
|
|
619
|
+
if ((permissions & required) !== required) {
|
|
620
|
+
throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
fs.symlinkSync = function (target, path) {
|
|
624
|
+
var link = filePath(path);
|
|
625
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
626
|
+
};
|
|
627
|
+
fs.readlinkSync = function (path) {
|
|
628
|
+
var target = filePath(path);
|
|
629
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
630
|
+
};
|
|
631
|
+
fs.chmodSync = function (path, mode) {
|
|
632
|
+
var target = filePath(path);
|
|
633
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
634
|
+
};
|
|
635
|
+
fs.realpathSync = function (path) {
|
|
636
|
+
var target = filePath(path);
|
|
637
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
638
|
+
};
|
|
639
|
+
fs.renameSync = function (from, to) {
|
|
640
|
+
var source = filePath(from);
|
|
641
|
+
var destination = filePath(to);
|
|
642
|
+
bridge('fsRename', 'rename', source, [source, destination], destination);
|
|
643
|
+
};
|
|
644
|
+
fs.copyFileSync = function (from, to, mode) {
|
|
645
|
+
var source = filePath(from);
|
|
646
|
+
var destination = filePath(to);
|
|
647
|
+
if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
|
|
648
|
+
throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
|
|
649
|
+
}
|
|
650
|
+
bridge('fsCopy', 'copyfile', source, [source, destination], destination);
|
|
651
|
+
};
|
|
652
|
+
fs.unlinkSync = function (path) {
|
|
653
|
+
var target = filePath(path);
|
|
654
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
655
|
+
};
|
|
656
|
+
fs.rmdirSync = function (path, options) {
|
|
657
|
+
var target = filePath(path);
|
|
658
|
+
bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
|
|
659
|
+
};
|
|
660
|
+
fs.constants = constants;
|
|
661
|
+
fs.Stats = Stats;
|
|
662
|
+
fs.Dirent = Dirent;
|
|
663
|
+
|
|
664
|
+
var callbackNames = [
|
|
665
|
+
['readFile', fs.readFileSync],
|
|
666
|
+
['writeFile', fs.writeFileSync],
|
|
667
|
+
['appendFile', fs.appendFileSync],
|
|
668
|
+
['stat', fs.statSync],
|
|
669
|
+
['lstat', fs.lstatSync],
|
|
670
|
+
['readdir', fs.readdirSync],
|
|
671
|
+
['mkdir', fs.mkdirSync],
|
|
672
|
+
['rm', fs.rmSync],
|
|
673
|
+
['symlink', fs.symlinkSync],
|
|
674
|
+
['readlink', fs.readlinkSync],
|
|
675
|
+
['chmod', fs.chmodSync],
|
|
676
|
+
['realpath', fs.realpathSync],
|
|
677
|
+
['rename', fs.renameSync],
|
|
678
|
+
['copyFile', fs.copyFileSync],
|
|
679
|
+
['unlink', fs.unlinkSync],
|
|
680
|
+
['rmdir', fs.rmdirSync]
|
|
681
|
+
];
|
|
682
|
+
function callbackify(name, sync) {
|
|
683
|
+
fs[name] = function () {
|
|
684
|
+
var args = Array.prototype.slice.call(arguments);
|
|
685
|
+
var callback = args.pop();
|
|
686
|
+
if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
|
|
687
|
+
Promise.resolve().then(function () {
|
|
688
|
+
return sync.apply(fs, args);
|
|
689
|
+
}).then(function (value) {
|
|
690
|
+
callback(null, value);
|
|
691
|
+
}, function (error) {
|
|
692
|
+
callback(error);
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
for (var index = 0; index < callbackNames.length; index++) callbackify(callbackNames[index][0], callbackNames[index][1]);
|
|
697
|
+
fs.exists = function (path, callback) {
|
|
698
|
+
if (typeof callback !== 'function') throw new TypeError('The "cb" argument must be of type function. Received ' + typeof callback);
|
|
699
|
+
Promise.resolve().then(function () { callback(fs.existsSync(path)); });
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
var promiseNames = [
|
|
703
|
+
'readFile', 'writeFile', 'appendFile', 'stat', 'lstat', 'readdir',
|
|
704
|
+
'mkdir', 'rm', 'symlink', 'readlink', 'chmod', 'realpath',
|
|
705
|
+
'rename', 'copyFile', 'unlink', 'rmdir'
|
|
706
|
+
];
|
|
707
|
+
fs.promises = {};
|
|
708
|
+
for (var index = 0; index < promiseNames.length; index++) (function (name) {
|
|
709
|
+
var sync = fs[name + 'Sync'];
|
|
710
|
+
fs.promises[name] = function () {
|
|
711
|
+
var args = arguments;
|
|
712
|
+
return new Promise(function (resolve, reject) {
|
|
713
|
+
Promise.resolve().then(function () {
|
|
714
|
+
return sync.apply(fs, args);
|
|
715
|
+
}).then(resolve, reject);
|
|
716
|
+
});
|
|
717
|
+
};
|
|
718
|
+
})(promiseNames[index]);
|
|
719
|
+
fs.promises.access = function (path, mode) {
|
|
720
|
+
return new Promise(function (resolve, reject) {
|
|
721
|
+
Promise.resolve().then(function () {
|
|
722
|
+
fs.accessSync(path, mode);
|
|
723
|
+
}).then(resolve, reject);
|
|
724
|
+
});
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
globalThis.fs = fs;
|
|
728
|
+
})();
|
|
729
|
+
`;
|
|
327
730
|
|
|
328
731
|
// src/resource-fs.ts
|
|
329
732
|
var EROFS = /* @__PURE__ */ __name(() => new Error("EROFS: resource mounts are read-only"), "EROFS");
|
|
@@ -481,6 +884,7 @@ function traversable(stat2) {
|
|
|
481
884
|
}
|
|
482
885
|
__name(traversable, "traversable");
|
|
483
886
|
async function createSandbox(layout, options) {
|
|
887
|
+
const { Bash, InMemoryFs, MountableFs, OverlayFs, ReadWriteFs } = await import("just-bash");
|
|
484
888
|
const { home } = options;
|
|
485
889
|
class Writable extends ReadWriteFs {
|
|
486
890
|
static {
|
|
@@ -512,17 +916,16 @@ async function createSandbox(layout, options) {
|
|
|
512
916
|
fs: fs3,
|
|
513
917
|
cwd: layout.cwd,
|
|
514
918
|
network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
|
|
515
|
-
javascript: layout.javascript,
|
|
919
|
+
javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
|
|
516
920
|
python: layout.python,
|
|
517
|
-
executionLimits:
|
|
921
|
+
executionLimits: sandboxExecutionLimits(layout)
|
|
518
922
|
});
|
|
519
923
|
return {
|
|
520
924
|
...layout,
|
|
521
925
|
fs: fs3,
|
|
522
926
|
async exec(command, signal) {
|
|
523
|
-
const timeout = AbortSignal.timeout(layout.timeoutMs);
|
|
524
927
|
try {
|
|
525
|
-
const result = await bash.exec(command, { signal
|
|
928
|
+
const result = await bash.exec(command, { signal });
|
|
526
929
|
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
|
|
527
930
|
} catch (error) {
|
|
528
931
|
return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
|
|
@@ -550,8 +953,11 @@ import path6 from "node:path";
|
|
|
550
953
|
// src/skills.ts
|
|
551
954
|
import { readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
|
|
552
955
|
import path5 from "node:path";
|
|
956
|
+
import { fileURLToPath } from "node:url";
|
|
553
957
|
import { parse as parseYaml } from "yaml";
|
|
554
958
|
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
959
|
+
var moduleDirectory = typeof __dirname === "string" ? __dirname : path5.dirname(fileURLToPath(import.meta.url));
|
|
960
|
+
var BUILTIN_SKILLS_DIRECTORY = path5.resolve(moduleDirectory, "..", "resources", "skills");
|
|
555
961
|
function discoverSkills(roots, logger) {
|
|
556
962
|
const found = /* @__PURE__ */ new Map();
|
|
557
963
|
for (const root of roots) {
|
|
@@ -626,7 +1032,10 @@ function defaultShell(configured) {
|
|
|
626
1032
|
__name(defaultShell, "defaultShell");
|
|
627
1033
|
function resolveLayout(options) {
|
|
628
1034
|
const { config } = options;
|
|
629
|
-
const skills = discoverSkills(
|
|
1035
|
+
const skills = discoverSkills(
|
|
1036
|
+
[BUILTIN_SKILLS_DIRECTORY, path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))],
|
|
1037
|
+
options.logger
|
|
1038
|
+
);
|
|
630
1039
|
const workspace = path6.join(options.home, "workspace");
|
|
631
1040
|
mkdirSync2(workspace, { recursive: true });
|
|
632
1041
|
const backend = config.backend;
|
|
@@ -666,6 +1075,9 @@ Run a bash command inside the sandbox.
|
|
|
666
1075
|
The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
|
|
667
1076
|
{% endif %}
|
|
668
1077
|
Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
|
|
1078
|
+
{% if not native %}
|
|
1079
|
+
/tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
1080
|
+
{% endif %}
|
|
669
1081
|
{% if gitBashPaths %}
|
|
670
1082
|
Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
|
|
671
1083
|
Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
|
|
@@ -729,7 +1141,7 @@ function createWorkspaceTools(sandbox) {
|
|
|
729
1141
|
${output.stdout}`);
|
|
730
1142
|
if (output.stderr.length > 0) sections.push(`stderr:
|
|
731
1143
|
${output.stderr}`);
|
|
732
|
-
return { type: "text", value: sections.join("\n\n") };
|
|
1144
|
+
return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
|
|
733
1145
|
}, "toModelOutput"),
|
|
734
1146
|
execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
|
|
735
1147
|
}),
|
|
@@ -831,22 +1243,22 @@ var WorkspaceExtension = class extends Extension {
|
|
|
831
1243
|
this.logger.level = config.logLevel;
|
|
832
1244
|
this.dataPath = ctx.ishiki.dataPath;
|
|
833
1245
|
}
|
|
834
|
-
[Service.invoke](
|
|
1246
|
+
[Service.invoke](host, config) {
|
|
835
1247
|
const shellOptions = {
|
|
836
1248
|
config,
|
|
837
|
-
home:
|
|
838
|
-
root:
|
|
1249
|
+
home: host.home,
|
|
1250
|
+
root: host.root,
|
|
839
1251
|
dataPath: this.dataPath,
|
|
840
1252
|
logger: this.logger,
|
|
841
|
-
resources:
|
|
1253
|
+
resources: host.resources
|
|
842
1254
|
};
|
|
843
1255
|
const layout = resolveLayout(shellOptions);
|
|
844
1256
|
let shell;
|
|
845
1257
|
let tools;
|
|
846
1258
|
const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
|
|
847
1259
|
const disposers = [];
|
|
848
|
-
disposers.push(
|
|
849
|
-
disposers.push(
|
|
1260
|
+
disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
|
|
1261
|
+
disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
|
|
850
1262
|
return {
|
|
851
1263
|
name: "ishiki.workspace",
|
|
852
1264
|
extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
|
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.
|