koishi-plugin-ishiki-workspace 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/execution-limits.d.ts +5 -0
- package/lib/index.cjs +192 -72
- package/lib/index.d.ts +3 -2
- package/lib/index.js +191 -72
- package/lib/skills.d.ts +1 -0
- package/package.json +8 -7
- 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
|
@@ -39,18 +39,27 @@ var import_koishi_plugin_ishiki4 = require("koishi-plugin-ishiki");
|
|
|
39
39
|
|
|
40
40
|
// src/config.ts
|
|
41
41
|
var import_koishi = require("koishi");
|
|
42
|
-
var WorkspaceConfig = import_koishi.Schema.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
})
|
|
42
|
+
var WorkspaceConfig = import_koishi.Schema.intersect([
|
|
43
|
+
import_koishi.Schema.object({
|
|
44
|
+
backend: import_koishi.Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
|
|
45
|
+
timeoutMs: import_koishi.Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
|
|
46
|
+
maxExecutionOutputBytes: import_koishi.Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
|
|
47
|
+
skills: import_koishi.Schema.array(import_koishi.Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([])
|
|
48
|
+
}),
|
|
49
|
+
import_koishi.Schema.union([
|
|
50
|
+
import_koishi.Schema.object({
|
|
51
|
+
backend: import_koishi.Schema.const("native").required(),
|
|
52
|
+
shell: import_koishi.Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default("")
|
|
53
|
+
}).description("native 模式"),
|
|
54
|
+
import_koishi.Schema.object({
|
|
55
|
+
cwd: import_koishi.Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
|
|
56
|
+
network: import_koishi.Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
|
|
57
|
+
javascript: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
|
|
58
|
+
python: import_koishi.Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
|
|
59
|
+
mounts: import_koishi.Schema.array(import_koishi.Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
|
|
60
|
+
}).description("sandbox 模式")
|
|
61
|
+
])
|
|
62
|
+
]);
|
|
54
63
|
|
|
55
64
|
// src/native.ts
|
|
56
65
|
var import_node_child_process = require("node:child_process");
|
|
@@ -250,8 +259,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
|
|
|
250
259
|
);
|
|
251
260
|
var SANDBOX_INSTRUCTIONS = new import_koishi_plugin_ishiki.Template(
|
|
252
261
|
`## Workspace
|
|
253
|
-
You read and write files in a per-instance sandbox, not on the host: commands
|
|
262
|
+
You read and write files in a per-instance sandbox, not on the host: commands run in just-bash, only the mounted paths below exist, and host paths and commands are invisible.
|
|
254
263
|
The command set is not the host's.
|
|
264
|
+
Runtime details and API gaps are recorded in the bundled sandbox-environment skill; read it before writing JavaScript, TypeScript, Python, SQLite, or network-heavy commands.
|
|
255
265
|
Current working directory: {{ cwd }}
|
|
256
266
|
Mounts:
|
|
257
267
|
- {{ homeMount }}: read-only, this instance's data directory (writes fail)
|
|
@@ -269,11 +279,12 @@ Network: {{ network }}
|
|
|
269
279
|
Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
|
|
270
280
|
Executor output safety limit: {{ maxExecutionOutputBytes }} bytes. Inside codemode you receive the raw output; the runtime handles model-visible previews and archives. Return structured results instead of stringifying or slicing them yourself.
|
|
271
281
|
Shell state does not persist between calls: cd, aliases, functions, and exported variables are not retained. Put a directory change on the same line (cd <dir> && <command>).
|
|
282
|
+
/tmp belongs to this sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
272
283
|
{% if javascript %}
|
|
273
|
-
js-exec is available: run JavaScript / TypeScript in QuickJS
|
|
284
|
+
js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
|
|
274
285
|
{% endif %}
|
|
275
286
|
{% if python %}
|
|
276
|
-
python3 and python are available: CPython compiled to WebAssembly
|
|
287
|
+
python3 and python are available: CPython compiled to WebAssembly. Most of the standard library is available, but subprocess and the sqlite3 module are absent. The first call waits for it to load; reads and writes target this sandbox.
|
|
277
288
|
{% endif %}
|
|
278
289
|
You can also read a known file through read's workspace:///<sandbox-path> URL (e.g. workspace:///home/workspace/a.txt): virtual files, persistent directories, and mounts all resolve to the same sandbox file.
|
|
279
290
|
Use read_file to read a known file, write_file to replace a file, edit_file for a small edit, and bash for listing, searching, and pipelines.`
|
|
@@ -356,6 +367,18 @@ var WorkspaceScheme = class extends import_koishi_plugin_ishiki2.FileScheme {
|
|
|
356
367
|
// src/sandbox.ts
|
|
357
368
|
var import_node_path4 = __toESM(require("node:path"));
|
|
358
369
|
|
|
370
|
+
// src/execution-limits.ts
|
|
371
|
+
function sandboxExecutionLimits(layout) {
|
|
372
|
+
return {
|
|
373
|
+
maxOutputSize: layout.maxExecutionOutputBytes,
|
|
374
|
+
maxExecutionTimeMs: layout.timeoutMs,
|
|
375
|
+
maxSqliteTimeoutMs: layout.timeoutMs,
|
|
376
|
+
maxPythonTimeoutMs: layout.timeoutMs,
|
|
377
|
+
maxJsTimeoutMs: layout.timeoutMs
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
__name(sandboxExecutionLimits, "sandboxExecutionLimits");
|
|
381
|
+
|
|
359
382
|
// src/js-bootstrap.ts
|
|
360
383
|
var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
361
384
|
(function () {
|
|
@@ -394,6 +417,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
394
417
|
ELOOP: -40
|
|
395
418
|
};
|
|
396
419
|
|
|
420
|
+
var constants = {
|
|
421
|
+
F_OK: 0,
|
|
422
|
+
R_OK: 4,
|
|
423
|
+
W_OK: 2,
|
|
424
|
+
X_OK: 1,
|
|
425
|
+
COPYFILE_EXCL: 1,
|
|
426
|
+
COPYFILE_FICLONE: 2,
|
|
427
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
428
|
+
S_IFMT: 61440,
|
|
429
|
+
S_IFREG: 32768,
|
|
430
|
+
S_IFDIR: 16384,
|
|
431
|
+
S_IFLNK: 40960,
|
|
432
|
+
O_RDONLY: 0,
|
|
433
|
+
O_WRONLY: 1,
|
|
434
|
+
O_RDWR: 2,
|
|
435
|
+
O_CREAT: 64,
|
|
436
|
+
O_EXCL: 128,
|
|
437
|
+
O_TRUNC: 512,
|
|
438
|
+
O_APPEND: 1024
|
|
439
|
+
};
|
|
440
|
+
|
|
397
441
|
function failure(error, syscall, target, destination) {
|
|
398
442
|
if (!(error instanceof Error)) error = new Error(String(error));
|
|
399
443
|
var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
|
|
@@ -410,6 +454,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
410
454
|
return error;
|
|
411
455
|
}
|
|
412
456
|
|
|
457
|
+
function filePath(value) {
|
|
458
|
+
if (
|
|
459
|
+
typeof value !== 'object' ||
|
|
460
|
+
value === null ||
|
|
461
|
+
typeof value.href !== 'string' ||
|
|
462
|
+
typeof value.protocol !== 'string'
|
|
463
|
+
) return value;
|
|
464
|
+
if (value.protocol !== 'file:') {
|
|
465
|
+
throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
|
|
466
|
+
}
|
|
467
|
+
if (value.hostname && value.hostname !== 'localhost') {
|
|
468
|
+
throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
|
|
469
|
+
}
|
|
470
|
+
return decodeURIComponent(value.pathname);
|
|
471
|
+
}
|
|
472
|
+
|
|
413
473
|
function bridge(hostName, syscall, target, args, destination) {
|
|
414
474
|
try {
|
|
415
475
|
var operation = host[hostName];
|
|
@@ -430,29 +490,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
430
490
|
return String(value);
|
|
431
491
|
}
|
|
432
492
|
|
|
493
|
+
function Stats() {}
|
|
494
|
+
function Dirent() {}
|
|
495
|
+
|
|
433
496
|
function stats(value) {
|
|
434
497
|
var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
|
|
435
498
|
var size = Number(value.size);
|
|
436
|
-
var result =
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
};
|
|
499
|
+
var result = Object.create(Stats.prototype);
|
|
500
|
+
result.dev = 0;
|
|
501
|
+
result.ino = 0;
|
|
502
|
+
result.mode = Number(value.mode);
|
|
503
|
+
result.nlink = value.isDirectory ? 2 : 1;
|
|
504
|
+
result.uid = 1000;
|
|
505
|
+
result.gid = 1000;
|
|
506
|
+
result.rdev = 0;
|
|
507
|
+
result.size = size;
|
|
508
|
+
result.blksize = 4096;
|
|
509
|
+
result.blocks = Math.ceil(size / 512);
|
|
510
|
+
result.atime = mtime;
|
|
511
|
+
result.mtime = mtime;
|
|
512
|
+
result.ctime = mtime;
|
|
513
|
+
result.birthtime = mtime;
|
|
514
|
+
result.atimeMs = mtime.getTime();
|
|
515
|
+
result.mtimeMs = mtime.getTime();
|
|
516
|
+
result.ctimeMs = mtime.getTime();
|
|
517
|
+
result.birthtimeMs = mtime.getTime();
|
|
456
518
|
result.isFile = function () { return value.isFile; };
|
|
457
519
|
result.isDirectory = function () { return value.isDirectory; };
|
|
458
520
|
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
@@ -464,7 +526,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
464
526
|
}
|
|
465
527
|
|
|
466
528
|
function dirent(name, parentPath, value) {
|
|
467
|
-
var result =
|
|
529
|
+
var result = Object.create(Dirent.prototype);
|
|
530
|
+
result.name = String(name);
|
|
531
|
+
result.parentPath = String(parentPath);
|
|
532
|
+
result.path = String(parentPath);
|
|
468
533
|
result.isFile = function () { return value.isFile; };
|
|
469
534
|
result.isDirectory = function () { return value.isDirectory; };
|
|
470
535
|
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
@@ -480,7 +545,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
480
545
|
}
|
|
481
546
|
|
|
482
547
|
function readStats(path, follow) {
|
|
483
|
-
|
|
548
|
+
var target = filePath(path);
|
|
549
|
+
return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
|
|
484
550
|
}
|
|
485
551
|
|
|
486
552
|
function collect(path, relative, options, output) {
|
|
@@ -496,10 +562,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
496
562
|
}
|
|
497
563
|
|
|
498
564
|
function readdir(path, options) {
|
|
565
|
+
var target = filePath(path);
|
|
499
566
|
if (typeof options === 'string') options = { encoding: options };
|
|
500
567
|
options = options || {};
|
|
501
568
|
var output = [];
|
|
502
|
-
collect(
|
|
569
|
+
collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
|
|
503
570
|
if (options.encoding === 'buffer') return output.map(function (entry) {
|
|
504
571
|
return typeof entry === 'string' ? Buffer.from(entry) : entry;
|
|
505
572
|
});
|
|
@@ -507,7 +574,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
507
574
|
}
|
|
508
575
|
|
|
509
576
|
function firstCreated(path) {
|
|
510
|
-
var text = String(path);
|
|
577
|
+
var text = String(filePath(path));
|
|
511
578
|
var absolute = text.charAt(0) === '/';
|
|
512
579
|
var parts = text.split('/');
|
|
513
580
|
var current = '';
|
|
@@ -526,24 +593,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
526
593
|
}
|
|
527
594
|
|
|
528
595
|
fs.readFileBuffer = function (path) {
|
|
596
|
+
var target = filePath(path);
|
|
529
597
|
try {
|
|
530
|
-
return raw.readFileBuffer(
|
|
598
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
531
599
|
} catch (error) {
|
|
532
|
-
throw failure(error, 'open',
|
|
600
|
+
throw failure(error, 'open', target);
|
|
533
601
|
}
|
|
534
602
|
};
|
|
535
603
|
fs.readFileSync = function (path, options) {
|
|
604
|
+
var target = filePath(path);
|
|
536
605
|
try {
|
|
537
|
-
return raw.readFileSync(
|
|
606
|
+
return raw.readFileSync(target, options);
|
|
538
607
|
} catch (error) {
|
|
539
|
-
throw failure(error, 'open',
|
|
608
|
+
throw failure(error, 'open', target);
|
|
540
609
|
}
|
|
541
610
|
};
|
|
542
611
|
fs.writeFileSync = function (path, data) {
|
|
543
|
-
|
|
612
|
+
var target = filePath(path);
|
|
613
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
544
614
|
};
|
|
545
615
|
fs.appendFileSync = function (path, data) {
|
|
546
|
-
|
|
616
|
+
var target = filePath(path);
|
|
617
|
+
bridge('fsAppend', 'open', target, [target, bytes(data)]);
|
|
547
618
|
};
|
|
548
619
|
fs.statSync = function (path) {
|
|
549
620
|
return stats(readStats(path, true));
|
|
@@ -553,45 +624,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
553
624
|
};
|
|
554
625
|
fs.readdirSync = readdir;
|
|
555
626
|
fs.mkdirSync = function (path, options) {
|
|
627
|
+
var target = filePath(path);
|
|
556
628
|
var recursive = Boolean(options && typeof options === 'object' && options.recursive);
|
|
557
|
-
var created = recursive ? firstCreated(
|
|
558
|
-
bridge('fsMkdir', 'mkdir',
|
|
629
|
+
var created = recursive ? firstCreated(target) : undefined;
|
|
630
|
+
bridge('fsMkdir', 'mkdir', target, [target, recursive]);
|
|
559
631
|
return created;
|
|
560
632
|
};
|
|
561
633
|
fs.rmSync = function (path, options) {
|
|
562
|
-
|
|
634
|
+
var target = filePath(path);
|
|
635
|
+
bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
|
|
563
636
|
};
|
|
564
637
|
fs.existsSync = function (path) {
|
|
638
|
+
var target = filePath(path);
|
|
565
639
|
try {
|
|
566
|
-
return bridge('fsExists', 'access',
|
|
640
|
+
return bridge('fsExists', 'access', target, [target]);
|
|
567
641
|
} catch (error) {
|
|
568
642
|
return false;
|
|
569
643
|
}
|
|
570
644
|
};
|
|
645
|
+
fs.accessSync = function (path, mode) {
|
|
646
|
+
var target = filePath(path);
|
|
647
|
+
if (!bridge('fsExists', 'access', target, [target])) {
|
|
648
|
+
throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
|
|
649
|
+
}
|
|
650
|
+
var flags = mode === undefined ? 0 : Number(mode);
|
|
651
|
+
if ((flags & ~7) !== 0) {
|
|
652
|
+
throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
|
|
653
|
+
}
|
|
654
|
+
if (flags === 0) return;
|
|
655
|
+
var permissions = stats(readStats(target, true)).mode & 511;
|
|
656
|
+
var required = 0;
|
|
657
|
+
if (flags & constants.R_OK) required |= 256;
|
|
658
|
+
if (flags & constants.W_OK) required |= 128;
|
|
659
|
+
if (flags & constants.X_OK) required |= 64;
|
|
660
|
+
if ((permissions & required) !== required) {
|
|
661
|
+
throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
571
664
|
fs.symlinkSync = function (target, path) {
|
|
572
|
-
|
|
665
|
+
var link = filePath(path);
|
|
666
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
573
667
|
};
|
|
574
668
|
fs.readlinkSync = function (path) {
|
|
575
|
-
|
|
669
|
+
var target = filePath(path);
|
|
670
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
576
671
|
};
|
|
577
672
|
fs.chmodSync = function (path, mode) {
|
|
578
|
-
|
|
673
|
+
var target = filePath(path);
|
|
674
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
579
675
|
};
|
|
580
676
|
fs.realpathSync = function (path) {
|
|
581
|
-
|
|
677
|
+
var target = filePath(path);
|
|
678
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
582
679
|
};
|
|
583
680
|
fs.renameSync = function (from, to) {
|
|
584
|
-
|
|
681
|
+
var source = filePath(from);
|
|
682
|
+
var destination = filePath(to);
|
|
683
|
+
bridge('fsRename', 'rename', source, [source, destination], destination);
|
|
585
684
|
};
|
|
586
|
-
fs.copyFileSync = function (from, to) {
|
|
587
|
-
|
|
685
|
+
fs.copyFileSync = function (from, to, mode) {
|
|
686
|
+
var source = filePath(from);
|
|
687
|
+
var destination = filePath(to);
|
|
688
|
+
if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
|
|
689
|
+
throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
|
|
690
|
+
}
|
|
691
|
+
bridge('fsCopy', 'copyfile', source, [source, destination], destination);
|
|
588
692
|
};
|
|
589
693
|
fs.unlinkSync = function (path) {
|
|
590
|
-
|
|
694
|
+
var target = filePath(path);
|
|
695
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
591
696
|
};
|
|
592
697
|
fs.rmdirSync = function (path, options) {
|
|
593
|
-
|
|
698
|
+
var target = filePath(path);
|
|
699
|
+
bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
|
|
594
700
|
};
|
|
701
|
+
fs.constants = constants;
|
|
702
|
+
fs.Stats = Stats;
|
|
703
|
+
fs.Dirent = Dirent;
|
|
595
704
|
|
|
596
705
|
var callbackNames = [
|
|
597
706
|
['readFile', fs.readFileSync],
|
|
@@ -648,10 +757,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
648
757
|
});
|
|
649
758
|
};
|
|
650
759
|
})(promiseNames[index]);
|
|
651
|
-
fs.promises.access = function (path) {
|
|
760
|
+
fs.promises.access = function (path, mode) {
|
|
652
761
|
return new Promise(function (resolve, reject) {
|
|
653
|
-
|
|
654
|
-
|
|
762
|
+
Promise.resolve().then(function () {
|
|
763
|
+
fs.accessSync(path, mode);
|
|
764
|
+
}).then(resolve, reject);
|
|
655
765
|
});
|
|
656
766
|
};
|
|
657
767
|
|
|
@@ -849,15 +959,14 @@ async function createSandbox(layout, options) {
|
|
|
849
959
|
network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
|
|
850
960
|
javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
|
|
851
961
|
python: layout.python,
|
|
852
|
-
executionLimits:
|
|
962
|
+
executionLimits: sandboxExecutionLimits(layout)
|
|
853
963
|
});
|
|
854
964
|
return {
|
|
855
965
|
...layout,
|
|
856
966
|
fs: fs3,
|
|
857
967
|
async exec(command, signal) {
|
|
858
|
-
const timeout = AbortSignal.timeout(layout.timeoutMs);
|
|
859
968
|
try {
|
|
860
|
-
const result = await bash.exec(command, { signal
|
|
969
|
+
const result = await bash.exec(command, { signal });
|
|
861
970
|
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
|
|
862
971
|
} catch (error) {
|
|
863
972
|
return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
|
|
@@ -885,8 +994,12 @@ var import_node_path6 = __toESM(require("node:path"));
|
|
|
885
994
|
// src/skills.ts
|
|
886
995
|
var import_node_fs3 = require("node:fs");
|
|
887
996
|
var import_node_path5 = __toESM(require("node:path"));
|
|
997
|
+
var import_node_url = require("node:url");
|
|
888
998
|
var import_yaml = require("yaml");
|
|
999
|
+
var import_meta = {};
|
|
889
1000
|
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1001
|
+
var moduleDirectory = typeof __dirname === "string" ? __dirname : import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
1002
|
+
var BUILTIN_SKILLS_DIRECTORY = import_node_path5.default.resolve(moduleDirectory, "..", "resources", "skills");
|
|
890
1003
|
function discoverSkills(roots, logger) {
|
|
891
1004
|
const found = /* @__PURE__ */ new Map();
|
|
892
1005
|
for (const root of roots) {
|
|
@@ -961,7 +1074,10 @@ function defaultShell(configured) {
|
|
|
961
1074
|
__name(defaultShell, "defaultShell");
|
|
962
1075
|
function resolveLayout(options) {
|
|
963
1076
|
const { config } = options;
|
|
964
|
-
const skills = discoverSkills(
|
|
1077
|
+
const skills = discoverSkills(
|
|
1078
|
+
[BUILTIN_SKILLS_DIRECTORY, import_node_path6.default.join(options.dataPath, "skills"), ...config.skills.map((dir) => import_node_path6.default.resolve(options.root, dir))],
|
|
1079
|
+
options.logger
|
|
1080
|
+
);
|
|
965
1081
|
const workspace = import_node_path6.default.join(options.home, "workspace");
|
|
966
1082
|
(0, import_node_fs4.mkdirSync)(workspace, { recursive: true });
|
|
967
1083
|
const backend = config.backend;
|
|
@@ -1001,6 +1117,9 @@ Run a bash command inside the sandbox.
|
|
|
1001
1117
|
The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
|
|
1002
1118
|
{% endif %}
|
|
1003
1119
|
Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
|
|
1120
|
+
{% if not native %}
|
|
1121
|
+
/tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
1122
|
+
{% endif %}
|
|
1004
1123
|
{% if gitBashPaths %}
|
|
1005
1124
|
Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
|
|
1006
1125
|
Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
|
|
@@ -1064,7 +1183,7 @@ function createWorkspaceTools(sandbox) {
|
|
|
1064
1183
|
${output.stdout}`);
|
|
1065
1184
|
if (output.stderr.length > 0) sections.push(`stderr:
|
|
1066
1185
|
${output.stderr}`);
|
|
1067
|
-
return { type: "text", value: sections.join("\n\n") };
|
|
1186
|
+
return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
|
|
1068
1187
|
}, "toModelOutput"),
|
|
1069
1188
|
execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
|
|
1070
1189
|
}),
|
|
@@ -1159,6 +1278,7 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
|
|
|
1159
1278
|
__name(this, "WorkspaceExtension");
|
|
1160
1279
|
}
|
|
1161
1280
|
static inject = ["ishiki"];
|
|
1281
|
+
static usage = ``;
|
|
1162
1282
|
Schema = WorkspaceConfig;
|
|
1163
1283
|
dataPath;
|
|
1164
1284
|
constructor(ctx, config) {
|
|
@@ -1166,22 +1286,22 @@ var WorkspaceExtension = class extends import_koishi_plugin_ishiki4.Extension {
|
|
|
1166
1286
|
this.logger.level = config.logLevel;
|
|
1167
1287
|
this.dataPath = ctx.ishiki.dataPath;
|
|
1168
1288
|
}
|
|
1169
|
-
[import_koishi2.Service.invoke](
|
|
1289
|
+
[import_koishi2.Service.invoke](host, config) {
|
|
1170
1290
|
const shellOptions = {
|
|
1171
1291
|
config,
|
|
1172
|
-
home:
|
|
1173
|
-
root:
|
|
1292
|
+
home: host.home,
|
|
1293
|
+
root: host.root,
|
|
1174
1294
|
dataPath: this.dataPath,
|
|
1175
1295
|
logger: this.logger,
|
|
1176
|
-
resources:
|
|
1296
|
+
resources: host.resources
|
|
1177
1297
|
};
|
|
1178
1298
|
const layout = resolveLayout(shellOptions);
|
|
1179
1299
|
let shell;
|
|
1180
1300
|
let tools;
|
|
1181
1301
|
const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
|
|
1182
1302
|
const disposers = [];
|
|
1183
|
-
disposers.push(
|
|
1184
|
-
disposers.push(
|
|
1303
|
+
disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
|
|
1304
|
+
disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
|
|
1185
1305
|
return {
|
|
1186
1306
|
name: "ishiki.workspace",
|
|
1187
1307
|
extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
|
package/lib/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Schema, Service, type Context } from "koishi";
|
|
2
|
-
import { Extension, type
|
|
2
|
+
import { Extension, type ExtensionHost, type ToolSet } from "koishi-plugin-ishiki";
|
|
3
3
|
import { WorkspaceConfig } from "./config.js";
|
|
4
4
|
declare class WorkspaceExtension extends Extension<WorkspaceConfig> {
|
|
5
5
|
static inject: string[];
|
|
6
|
+
static readonly usage = "";
|
|
6
7
|
readonly Schema: Schema<WorkspaceConfig>;
|
|
7
8
|
private readonly dataPath;
|
|
8
9
|
constructor(ctx: Context, config: WorkspaceExtension.Config);
|
|
9
|
-
[Service.invoke](
|
|
10
|
+
[Service.invoke](host: ExtensionHost, config: WorkspaceConfig): {
|
|
10
11
|
name: string;
|
|
11
12
|
extendTools: () => Promise<ToolSet>;
|
|
12
13
|
extendInstructions: () => Promise<string>;
|
package/lib/index.js
CHANGED
|
@@ -7,18 +7,27 @@ import { Extension } from "koishi-plugin-ishiki";
|
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
9
|
import { Schema } from "koishi";
|
|
10
|
-
var WorkspaceConfig = Schema.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
10
|
+
var WorkspaceConfig = Schema.intersect([
|
|
11
|
+
Schema.object({
|
|
12
|
+
backend: Schema.union(["sandbox", "native"]).description("执行后端:sandbox 用 just-bash 隔离环境;native 直接 spawn 宿主机 shell").default("sandbox"),
|
|
13
|
+
timeoutMs: Schema.number().description("单条命令的总时限(毫秒),超时按退出码 124 返回").default(3e4),
|
|
14
|
+
maxExecutionOutputBytes: Schema.number().min(1).description("执行器输出安全上限;模型可见预览由 Ishiki 统一处理").default(32 * 1024 * 1024),
|
|
15
|
+
skills: Schema.array(Schema.string()).description("额外技能目录,相对 profile 目录解析;与内置技能和数据的 skills 目录合并,同名时依次覆盖,以这里优先").default([])
|
|
16
|
+
}),
|
|
17
|
+
Schema.union([
|
|
18
|
+
Schema.object({
|
|
19
|
+
backend: Schema.const("native").required(),
|
|
20
|
+
shell: Schema.string().description("native 模式的 shell 可执行文件路径,如 Git Bash 的 bash.exe 或 pwsh;留空时 Windows 优先 pwsh 兜底 powershell,其它平台用 /bin/bash").default("")
|
|
21
|
+
}).description("native 模式"),
|
|
22
|
+
Schema.object({
|
|
23
|
+
cwd: Schema.string().description("sandbox 模式的初始工作目录;应当是某个挂载点覆盖得到的虚拟路径。native 模式忽略,固定为 <runtime.home>/workspace").default("/home/workspace"),
|
|
24
|
+
network: Schema.boolean().description("仅 sandbox 模式:允许沙箱内访问网络;开启时仍拒绝私有地址。native 走宿主机真实网络").default(false),
|
|
25
|
+
javascript: Schema.boolean().description("仅 sandbox 模式:启用 js-exec 命令(QuickJS 里执行 JavaScript / TypeScript);默认关闭,开它会多一份代码执行面").default(false),
|
|
26
|
+
python: Schema.boolean().description("仅 sandbox 模式:启用 python3 / python 命令(CPython 编译成 WebAssembly);默认关闭,开它会多一份代码执行面").default(false),
|
|
27
|
+
mounts: Schema.array(Schema.string()).description("仅 sandbox 模式:宿主目录挂载,写法 `source:target[:ro]`;source 相对 profile 目录解析,省略 :ro 即可读写。native 模式无虚拟路径,不解析").default([])
|
|
28
|
+
}).description("sandbox 模式")
|
|
29
|
+
])
|
|
30
|
+
]);
|
|
22
31
|
|
|
23
32
|
// src/native.ts
|
|
24
33
|
import { execFile } from "node:child_process";
|
|
@@ -218,8 +227,9 @@ Use read_file to read a known file, write_file to replace a file, edit_file for
|
|
|
218
227
|
);
|
|
219
228
|
var SANDBOX_INSTRUCTIONS = new Template(
|
|
220
229
|
`## Workspace
|
|
221
|
-
You read and write files in a per-instance sandbox, not on the host: commands
|
|
230
|
+
You read and write files in a per-instance sandbox, not on the host: commands run in just-bash, only the mounted paths below exist, and host paths and commands are invisible.
|
|
222
231
|
The command set is not the host's.
|
|
232
|
+
Runtime details and API gaps are recorded in the bundled sandbox-environment skill; read it before writing JavaScript, TypeScript, Python, SQLite, or network-heavy commands.
|
|
223
233
|
Current working directory: {{ cwd }}
|
|
224
234
|
Mounts:
|
|
225
235
|
- {{ homeMount }}: read-only, this instance's data directory (writes fail)
|
|
@@ -237,11 +247,12 @@ Network: {{ network }}
|
|
|
237
247
|
Command timeout: {{ timeoutMs }} ms; a timeout returns exit code 124.
|
|
238
248
|
Executor output safety limit: {{ maxExecutionOutputBytes }} bytes. Inside codemode you receive the raw output; the runtime handles model-visible previews and archives. Return structured results instead of stringifying or slicing them yourself.
|
|
239
249
|
Shell state does not persist between calls: cd, aliases, functions, and exported variables are not retained. Put a directory change on the same line (cd <dir> && <command>).
|
|
250
|
+
/tmp belongs to this sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
240
251
|
{% if javascript %}
|
|
241
|
-
js-exec is available: run JavaScript / TypeScript in QuickJS
|
|
252
|
+
js-exec is available: run JavaScript / TypeScript in QuickJS with a partial Node.js module surface. Only files inside the sandbox are accessible.
|
|
242
253
|
{% endif %}
|
|
243
254
|
{% if python %}
|
|
244
|
-
python3 and python are available: CPython compiled to WebAssembly
|
|
255
|
+
python3 and python are available: CPython compiled to WebAssembly. Most of the standard library is available, but subprocess and the sqlite3 module are absent. The first call waits for it to load; reads and writes target this sandbox.
|
|
245
256
|
{% endif %}
|
|
246
257
|
You can also read a known file through read's workspace:///<sandbox-path> URL (e.g. workspace:///home/workspace/a.txt): virtual files, persistent directories, and mounts all resolve to the same sandbox file.
|
|
247
258
|
Use read_file to read a known file, write_file to replace a file, edit_file for a small edit, and bash for listing, searching, and pipelines.`
|
|
@@ -324,6 +335,18 @@ var WorkspaceScheme = class extends FileScheme {
|
|
|
324
335
|
// src/sandbox.ts
|
|
325
336
|
import path4 from "node:path";
|
|
326
337
|
|
|
338
|
+
// src/execution-limits.ts
|
|
339
|
+
function sandboxExecutionLimits(layout) {
|
|
340
|
+
return {
|
|
341
|
+
maxOutputSize: layout.maxExecutionOutputBytes,
|
|
342
|
+
maxExecutionTimeMs: layout.timeoutMs,
|
|
343
|
+
maxSqliteTimeoutMs: layout.timeoutMs,
|
|
344
|
+
maxPythonTimeoutMs: layout.timeoutMs,
|
|
345
|
+
maxJsTimeoutMs: layout.timeoutMs
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
__name(sandboxExecutionLimits, "sandboxExecutionLimits");
|
|
349
|
+
|
|
327
350
|
// src/js-bootstrap.ts
|
|
328
351
|
var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
329
352
|
(function () {
|
|
@@ -362,6 +385,27 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
362
385
|
ELOOP: -40
|
|
363
386
|
};
|
|
364
387
|
|
|
388
|
+
var constants = {
|
|
389
|
+
F_OK: 0,
|
|
390
|
+
R_OK: 4,
|
|
391
|
+
W_OK: 2,
|
|
392
|
+
X_OK: 1,
|
|
393
|
+
COPYFILE_EXCL: 1,
|
|
394
|
+
COPYFILE_FICLONE: 2,
|
|
395
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
396
|
+
S_IFMT: 61440,
|
|
397
|
+
S_IFREG: 32768,
|
|
398
|
+
S_IFDIR: 16384,
|
|
399
|
+
S_IFLNK: 40960,
|
|
400
|
+
O_RDONLY: 0,
|
|
401
|
+
O_WRONLY: 1,
|
|
402
|
+
O_RDWR: 2,
|
|
403
|
+
O_CREAT: 64,
|
|
404
|
+
O_EXCL: 128,
|
|
405
|
+
O_TRUNC: 512,
|
|
406
|
+
O_APPEND: 1024
|
|
407
|
+
};
|
|
408
|
+
|
|
365
409
|
function failure(error, syscall, target, destination) {
|
|
366
410
|
if (!(error instanceof Error)) error = new Error(String(error));
|
|
367
411
|
var matched = /^([A-Z][A-Z0-9]+):/.exec(error.message);
|
|
@@ -378,6 +422,22 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
378
422
|
return error;
|
|
379
423
|
}
|
|
380
424
|
|
|
425
|
+
function filePath(value) {
|
|
426
|
+
if (
|
|
427
|
+
typeof value !== 'object' ||
|
|
428
|
+
value === null ||
|
|
429
|
+
typeof value.href !== 'string' ||
|
|
430
|
+
typeof value.protocol !== 'string'
|
|
431
|
+
) return value;
|
|
432
|
+
if (value.protocol !== 'file:') {
|
|
433
|
+
throw failure(new Error('ERR_INVALID_URL_SCHEME: The URL must be of scheme file, received ' + value.protocol), 'open', value.href);
|
|
434
|
+
}
|
|
435
|
+
if (value.hostname && value.hostname !== 'localhost') {
|
|
436
|
+
throw failure(new Error('ERR_INVALID_FILE_URL_HOST: File URL host must be "localhost" or empty, received ' + value.hostname), 'open', value.href);
|
|
437
|
+
}
|
|
438
|
+
return decodeURIComponent(value.pathname);
|
|
439
|
+
}
|
|
440
|
+
|
|
381
441
|
function bridge(hostName, syscall, target, args, destination) {
|
|
382
442
|
try {
|
|
383
443
|
var operation = host[hostName];
|
|
@@ -398,29 +458,31 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
398
458
|
return String(value);
|
|
399
459
|
}
|
|
400
460
|
|
|
461
|
+
function Stats() {}
|
|
462
|
+
function Dirent() {}
|
|
463
|
+
|
|
401
464
|
function stats(value) {
|
|
402
465
|
var mtime = value.mtime instanceof Date ? value.mtime : new Date(value.mtime);
|
|
403
466
|
var size = Number(value.size);
|
|
404
|
-
var result =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
};
|
|
467
|
+
var result = Object.create(Stats.prototype);
|
|
468
|
+
result.dev = 0;
|
|
469
|
+
result.ino = 0;
|
|
470
|
+
result.mode = Number(value.mode);
|
|
471
|
+
result.nlink = value.isDirectory ? 2 : 1;
|
|
472
|
+
result.uid = 1000;
|
|
473
|
+
result.gid = 1000;
|
|
474
|
+
result.rdev = 0;
|
|
475
|
+
result.size = size;
|
|
476
|
+
result.blksize = 4096;
|
|
477
|
+
result.blocks = Math.ceil(size / 512);
|
|
478
|
+
result.atime = mtime;
|
|
479
|
+
result.mtime = mtime;
|
|
480
|
+
result.ctime = mtime;
|
|
481
|
+
result.birthtime = mtime;
|
|
482
|
+
result.atimeMs = mtime.getTime();
|
|
483
|
+
result.mtimeMs = mtime.getTime();
|
|
484
|
+
result.ctimeMs = mtime.getTime();
|
|
485
|
+
result.birthtimeMs = mtime.getTime();
|
|
424
486
|
result.isFile = function () { return value.isFile; };
|
|
425
487
|
result.isDirectory = function () { return value.isDirectory; };
|
|
426
488
|
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
@@ -432,7 +494,10 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
432
494
|
}
|
|
433
495
|
|
|
434
496
|
function dirent(name, parentPath, value) {
|
|
435
|
-
var result =
|
|
497
|
+
var result = Object.create(Dirent.prototype);
|
|
498
|
+
result.name = String(name);
|
|
499
|
+
result.parentPath = String(parentPath);
|
|
500
|
+
result.path = String(parentPath);
|
|
436
501
|
result.isFile = function () { return value.isFile; };
|
|
437
502
|
result.isDirectory = function () { return value.isDirectory; };
|
|
438
503
|
result.isSymbolicLink = function () { return value.isSymbolicLink; };
|
|
@@ -448,7 +513,8 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
448
513
|
}
|
|
449
514
|
|
|
450
515
|
function readStats(path, follow) {
|
|
451
|
-
|
|
516
|
+
var target = filePath(path);
|
|
517
|
+
return bridge('fsStat', follow ? 'stat' : 'lstat', target, [target, !follow]);
|
|
452
518
|
}
|
|
453
519
|
|
|
454
520
|
function collect(path, relative, options, output) {
|
|
@@ -464,10 +530,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
464
530
|
}
|
|
465
531
|
|
|
466
532
|
function readdir(path, options) {
|
|
533
|
+
var target = filePath(path);
|
|
467
534
|
if (typeof options === 'string') options = { encoding: options };
|
|
468
535
|
options = options || {};
|
|
469
536
|
var output = [];
|
|
470
|
-
collect(
|
|
537
|
+
collect(target, '', { recursive: Boolean(options.recursive), withFileTypes: Boolean(options.withFileTypes) }, output);
|
|
471
538
|
if (options.encoding === 'buffer') return output.map(function (entry) {
|
|
472
539
|
return typeof entry === 'string' ? Buffer.from(entry) : entry;
|
|
473
540
|
});
|
|
@@ -475,7 +542,7 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
475
542
|
}
|
|
476
543
|
|
|
477
544
|
function firstCreated(path) {
|
|
478
|
-
var text = String(path);
|
|
545
|
+
var text = String(filePath(path));
|
|
479
546
|
var absolute = text.charAt(0) === '/';
|
|
480
547
|
var parts = text.split('/');
|
|
481
548
|
var current = '';
|
|
@@ -494,24 +561,28 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
494
561
|
}
|
|
495
562
|
|
|
496
563
|
fs.readFileBuffer = function (path) {
|
|
564
|
+
var target = filePath(path);
|
|
497
565
|
try {
|
|
498
|
-
return raw.readFileBuffer(
|
|
566
|
+
return Buffer.from(raw.readFileBuffer(target));
|
|
499
567
|
} catch (error) {
|
|
500
|
-
throw failure(error, 'open',
|
|
568
|
+
throw failure(error, 'open', target);
|
|
501
569
|
}
|
|
502
570
|
};
|
|
503
571
|
fs.readFileSync = function (path, options) {
|
|
572
|
+
var target = filePath(path);
|
|
504
573
|
try {
|
|
505
|
-
return raw.readFileSync(
|
|
574
|
+
return raw.readFileSync(target, options);
|
|
506
575
|
} catch (error) {
|
|
507
|
-
throw failure(error, 'open',
|
|
576
|
+
throw failure(error, 'open', target);
|
|
508
577
|
}
|
|
509
578
|
};
|
|
510
579
|
fs.writeFileSync = function (path, data) {
|
|
511
|
-
|
|
580
|
+
var target = filePath(path);
|
|
581
|
+
bridge('fsWrite', 'open', target, [target, bytes(data)]);
|
|
512
582
|
};
|
|
513
583
|
fs.appendFileSync = function (path, data) {
|
|
514
|
-
|
|
584
|
+
var target = filePath(path);
|
|
585
|
+
bridge('fsAppend', 'open', target, [target, bytes(data)]);
|
|
515
586
|
};
|
|
516
587
|
fs.statSync = function (path) {
|
|
517
588
|
return stats(readStats(path, true));
|
|
@@ -521,45 +592,83 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
521
592
|
};
|
|
522
593
|
fs.readdirSync = readdir;
|
|
523
594
|
fs.mkdirSync = function (path, options) {
|
|
595
|
+
var target = filePath(path);
|
|
524
596
|
var recursive = Boolean(options && typeof options === 'object' && options.recursive);
|
|
525
|
-
var created = recursive ? firstCreated(
|
|
526
|
-
bridge('fsMkdir', 'mkdir',
|
|
597
|
+
var created = recursive ? firstCreated(target) : undefined;
|
|
598
|
+
bridge('fsMkdir', 'mkdir', target, [target, recursive]);
|
|
527
599
|
return created;
|
|
528
600
|
};
|
|
529
601
|
fs.rmSync = function (path, options) {
|
|
530
|
-
|
|
602
|
+
var target = filePath(path);
|
|
603
|
+
bridge('fsRm', 'rm', target, [target, Boolean(options && options.recursive), Boolean(options && options.force)]);
|
|
531
604
|
};
|
|
532
605
|
fs.existsSync = function (path) {
|
|
606
|
+
var target = filePath(path);
|
|
533
607
|
try {
|
|
534
|
-
return bridge('fsExists', 'access',
|
|
608
|
+
return bridge('fsExists', 'access', target, [target]);
|
|
535
609
|
} catch (error) {
|
|
536
610
|
return false;
|
|
537
611
|
}
|
|
538
612
|
};
|
|
613
|
+
fs.accessSync = function (path, mode) {
|
|
614
|
+
var target = filePath(path);
|
|
615
|
+
if (!bridge('fsExists', 'access', target, [target])) {
|
|
616
|
+
throw failure(new Error("ENOENT: no such file or directory, access '" + target + "'"), 'access', target);
|
|
617
|
+
}
|
|
618
|
+
var flags = mode === undefined ? 0 : Number(mode);
|
|
619
|
+
if ((flags & ~7) !== 0) {
|
|
620
|
+
throw failure(new Error("EINVAL: invalid mode argument, access '" + target + "'"), 'access', target);
|
|
621
|
+
}
|
|
622
|
+
if (flags === 0) return;
|
|
623
|
+
var permissions = stats(readStats(target, true)).mode & 511;
|
|
624
|
+
var required = 0;
|
|
625
|
+
if (flags & constants.R_OK) required |= 256;
|
|
626
|
+
if (flags & constants.W_OK) required |= 128;
|
|
627
|
+
if (flags & constants.X_OK) required |= 64;
|
|
628
|
+
if ((permissions & required) !== required) {
|
|
629
|
+
throw failure(new Error("EACCES: permission denied, access '" + target + "'"), 'access', target);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
539
632
|
fs.symlinkSync = function (target, path) {
|
|
540
|
-
|
|
633
|
+
var link = filePath(path);
|
|
634
|
+
bridge('fsSymlink', 'symlink', link, [filePath(target), link]);
|
|
541
635
|
};
|
|
542
636
|
fs.readlinkSync = function (path) {
|
|
543
|
-
|
|
637
|
+
var target = filePath(path);
|
|
638
|
+
return bridge('fsReadlink', 'readlink', target, [target]);
|
|
544
639
|
};
|
|
545
640
|
fs.chmodSync = function (path, mode) {
|
|
546
|
-
|
|
641
|
+
var target = filePath(path);
|
|
642
|
+
bridge('fsChmod', 'chmod', target, [target, Number(mode)]);
|
|
547
643
|
};
|
|
548
644
|
fs.realpathSync = function (path) {
|
|
549
|
-
|
|
645
|
+
var target = filePath(path);
|
|
646
|
+
return bridge('fsRealpath', 'realpath', target, [target]);
|
|
550
647
|
};
|
|
551
648
|
fs.renameSync = function (from, to) {
|
|
552
|
-
|
|
649
|
+
var source = filePath(from);
|
|
650
|
+
var destination = filePath(to);
|
|
651
|
+
bridge('fsRename', 'rename', source, [source, destination], destination);
|
|
553
652
|
};
|
|
554
|
-
fs.copyFileSync = function (from, to) {
|
|
555
|
-
|
|
653
|
+
fs.copyFileSync = function (from, to, mode) {
|
|
654
|
+
var source = filePath(from);
|
|
655
|
+
var destination = filePath(to);
|
|
656
|
+
if ((Number(mode) || 0) & constants.COPYFILE_EXCL && fs.existsSync(destination)) {
|
|
657
|
+
throw failure(new Error("EEXIST: file already exists, copyfile '" + destination + "'"), 'copyfile', source, destination);
|
|
658
|
+
}
|
|
659
|
+
bridge('fsCopy', 'copyfile', source, [source, destination], destination);
|
|
556
660
|
};
|
|
557
661
|
fs.unlinkSync = function (path) {
|
|
558
|
-
|
|
662
|
+
var target = filePath(path);
|
|
663
|
+
bridge('fsRm', 'unlink', target, [target, false, false]);
|
|
559
664
|
};
|
|
560
665
|
fs.rmdirSync = function (path, options) {
|
|
561
|
-
|
|
666
|
+
var target = filePath(path);
|
|
667
|
+
bridge('fsRm', 'rmdir', target, [target, Boolean(options && options.recursive), false]);
|
|
562
668
|
};
|
|
669
|
+
fs.constants = constants;
|
|
670
|
+
fs.Stats = Stats;
|
|
671
|
+
fs.Dirent = Dirent;
|
|
563
672
|
|
|
564
673
|
var callbackNames = [
|
|
565
674
|
['readFile', fs.readFileSync],
|
|
@@ -616,10 +725,11 @@ var JAVASCRIPT_FS_BOOTSTRAP = String.raw`
|
|
|
616
725
|
});
|
|
617
726
|
};
|
|
618
727
|
})(promiseNames[index]);
|
|
619
|
-
fs.promises.access = function (path) {
|
|
728
|
+
fs.promises.access = function (path, mode) {
|
|
620
729
|
return new Promise(function (resolve, reject) {
|
|
621
|
-
|
|
622
|
-
|
|
730
|
+
Promise.resolve().then(function () {
|
|
731
|
+
fs.accessSync(path, mode);
|
|
732
|
+
}).then(resolve, reject);
|
|
623
733
|
});
|
|
624
734
|
};
|
|
625
735
|
|
|
@@ -817,15 +927,14 @@ async function createSandbox(layout, options) {
|
|
|
817
927
|
network: layout.network ? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true } : void 0,
|
|
818
928
|
javascript: layout.javascript ? { bootstrap: JAVASCRIPT_FS_BOOTSTRAP } : false,
|
|
819
929
|
python: layout.python,
|
|
820
|
-
executionLimits:
|
|
930
|
+
executionLimits: sandboxExecutionLimits(layout)
|
|
821
931
|
});
|
|
822
932
|
return {
|
|
823
933
|
...layout,
|
|
824
934
|
fs: fs3,
|
|
825
935
|
async exec(command, signal) {
|
|
826
|
-
const timeout = AbortSignal.timeout(layout.timeoutMs);
|
|
827
936
|
try {
|
|
828
|
-
const result = await bash.exec(command, { signal
|
|
937
|
+
const result = await bash.exec(command, { signal });
|
|
829
938
|
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode };
|
|
830
939
|
} catch (error) {
|
|
831
940
|
return { stdout: "", stderr: `bash: ${error instanceof Error ? error.message : String(error)}`, exitCode: 1 };
|
|
@@ -853,8 +962,11 @@ import path6 from "node:path";
|
|
|
853
962
|
// src/skills.ts
|
|
854
963
|
import { readdirSync, readFileSync, statSync as statSync2 } from "node:fs";
|
|
855
964
|
import path5 from "node:path";
|
|
965
|
+
import { fileURLToPath } from "node:url";
|
|
856
966
|
import { parse as parseYaml } from "yaml";
|
|
857
967
|
var NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
968
|
+
var moduleDirectory = typeof __dirname === "string" ? __dirname : path5.dirname(fileURLToPath(import.meta.url));
|
|
969
|
+
var BUILTIN_SKILLS_DIRECTORY = path5.resolve(moduleDirectory, "..", "resources", "skills");
|
|
858
970
|
function discoverSkills(roots, logger) {
|
|
859
971
|
const found = /* @__PURE__ */ new Map();
|
|
860
972
|
for (const root of roots) {
|
|
@@ -929,7 +1041,10 @@ function defaultShell(configured) {
|
|
|
929
1041
|
__name(defaultShell, "defaultShell");
|
|
930
1042
|
function resolveLayout(options) {
|
|
931
1043
|
const { config } = options;
|
|
932
|
-
const skills = discoverSkills(
|
|
1044
|
+
const skills = discoverSkills(
|
|
1045
|
+
[BUILTIN_SKILLS_DIRECTORY, path6.join(options.dataPath, "skills"), ...config.skills.map((dir) => path6.resolve(options.root, dir))],
|
|
1046
|
+
options.logger
|
|
1047
|
+
);
|
|
933
1048
|
const workspace = path6.join(options.home, "workspace");
|
|
934
1049
|
mkdirSync2(workspace, { recursive: true });
|
|
935
1050
|
const backend = config.backend;
|
|
@@ -969,6 +1084,9 @@ Run a bash command inside the sandbox.
|
|
|
969
1084
|
The sandbox is a JavaScript-interpreted virtual environment, not a host shell: only files under the mounts are visible, and the command set is not the host's.
|
|
970
1085
|
{% endif %}
|
|
971
1086
|
Write paths outside the current working directory in full. cd, aliases, functions, and exported variables do not persist between calls.
|
|
1087
|
+
{% if not native %}
|
|
1088
|
+
/tmp belongs to the current sandbox lifetime. Read a /tmp file only after the same or an earlier successful call created it; a failed or timed-out call may leave nothing behind. Write results that must survive to the workspace or an artifact.
|
|
1089
|
+
{% endif %}
|
|
972
1090
|
{% if gitBashPaths %}
|
|
973
1091
|
Command text follows git-bash path syntax: /d/work means D:\\work. A path inside a string that only gets handed to another program (a JavaScript string passed to node, a config value) is not converted, so write D:/... there.
|
|
974
1092
|
Write scratch files under the working directory rather than /tmp: the shell, a program it starts, and the file tools may resolve /tmp to different folders.
|
|
@@ -1032,7 +1150,7 @@ function createWorkspaceTools(sandbox) {
|
|
|
1032
1150
|
${output.stdout}`);
|
|
1033
1151
|
if (output.stderr.length > 0) sections.push(`stderr:
|
|
1034
1152
|
${output.stderr}`);
|
|
1035
|
-
return { type: "text", value: sections.join("\n\n") };
|
|
1153
|
+
return { type: output.exitCode === 0 ? "text" : "error-text", value: sections.join("\n\n") };
|
|
1036
1154
|
}, "toModelOutput"),
|
|
1037
1155
|
execute: /* @__PURE__ */ __name((input, options) => sandbox.exec(input.command, options.abortSignal), "execute")
|
|
1038
1156
|
}),
|
|
@@ -1127,6 +1245,7 @@ var WorkspaceExtension = class extends Extension {
|
|
|
1127
1245
|
__name(this, "WorkspaceExtension");
|
|
1128
1246
|
}
|
|
1129
1247
|
static inject = ["ishiki"];
|
|
1248
|
+
static usage = ``;
|
|
1130
1249
|
Schema = WorkspaceConfig;
|
|
1131
1250
|
dataPath;
|
|
1132
1251
|
constructor(ctx, config) {
|
|
@@ -1134,22 +1253,22 @@ var WorkspaceExtension = class extends Extension {
|
|
|
1134
1253
|
this.logger.level = config.logLevel;
|
|
1135
1254
|
this.dataPath = ctx.ishiki.dataPath;
|
|
1136
1255
|
}
|
|
1137
|
-
[Service.invoke](
|
|
1256
|
+
[Service.invoke](host, config) {
|
|
1138
1257
|
const shellOptions = {
|
|
1139
1258
|
config,
|
|
1140
|
-
home:
|
|
1141
|
-
root:
|
|
1259
|
+
home: host.home,
|
|
1260
|
+
root: host.root,
|
|
1142
1261
|
dataPath: this.dataPath,
|
|
1143
1262
|
logger: this.logger,
|
|
1144
|
-
resources:
|
|
1263
|
+
resources: host.resources
|
|
1145
1264
|
};
|
|
1146
1265
|
const layout = resolveLayout(shellOptions);
|
|
1147
1266
|
let shell;
|
|
1148
1267
|
let tools;
|
|
1149
1268
|
const box = /* @__PURE__ */ __name(() => shell ??= layout.backend === "native" ? createNativeShell(layout) : createSandbox(layout, shellOptions), "box");
|
|
1150
1269
|
const disposers = [];
|
|
1151
|
-
disposers.push(
|
|
1152
|
-
disposers.push(
|
|
1270
|
+
disposers.push(host.resources?.attach(new SkillScheme(layout.skills)));
|
|
1271
|
+
disposers.push(host.resources?.attach(new WorkspaceScheme(async () => (await box()).fs)));
|
|
1153
1272
|
return {
|
|
1154
1273
|
name: "ishiki.workspace",
|
|
1155
1274
|
extendTools: /* @__PURE__ */ __name(async () => tools ??= createWorkspaceTools(await box()), "extendTools"),
|
package/lib/skills.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export interface Skill {
|
|
|
4
4
|
readonly description: string;
|
|
5
5
|
readonly directory: string;
|
|
6
6
|
}
|
|
7
|
+
export declare const BUILTIN_SKILLS_DIRECTORY: string;
|
|
7
8
|
export declare function discoverSkills(roots: readonly string[], logger: Logger): Skill[];
|
|
8
9
|
export declare function skillCatalog(skills: readonly Skill[], native: boolean): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-ishiki-workspace",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "workspace tools for Ishiki",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
7
7
|
"chatbot",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"directory": "packages/workspace"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"lib"
|
|
24
|
+
"lib",
|
|
25
|
+
"resources"
|
|
25
26
|
],
|
|
26
27
|
"main": "./lib/index.cjs",
|
|
27
28
|
"module": "./lib/index.js",
|
|
@@ -44,16 +45,16 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"koishi": "^4.18.11",
|
|
47
|
-
"koishi-plugin-ishiki": ">=0.0.
|
|
48
|
+
"koishi-plugin-ishiki": ">=0.0.6 <0.1.0"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
50
51
|
"koishi": "^4.18.11",
|
|
51
|
-
"koishi-plugin-ishiki": ">=0.0.
|
|
52
|
+
"koishi-plugin-ishiki": ">=0.0.6 <0.1.0"
|
|
52
53
|
},
|
|
53
54
|
"koishi": {
|
|
54
55
|
"description": {
|
|
55
|
-
"zh": "
|
|
56
|
-
"en": "
|
|
56
|
+
"zh": "Ishiki 的工作区扩展,提供 bash 沙箱和文件读写能力",
|
|
57
|
+
"en": "Ishiki workspace extension with bash sandbox and file read/write"
|
|
57
58
|
},
|
|
58
59
|
"service": {
|
|
59
60
|
"required": [
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sandbox-environment
|
|
3
|
+
description: Use before running or debugging shell commands, JavaScript, TypeScript, Python, SQLite, networking, or file operations in the Ishiki sandbox; it routes work to the supported runtime and records the environment's API gaps.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sandbox Environment
|
|
7
|
+
|
|
8
|
+
The workspace is a just-bash environment with optional QuickJS and WebAssembly Python runtimes. Choose the executor before writing runtime-specific code.
|
|
9
|
+
|
|
10
|
+
## Choose the executor
|
|
11
|
+
|
|
12
|
+
- Use the POSIX shell for file discovery, text pipelines, archive commands, and the SQLite command-line tool.
|
|
13
|
+
- Use `js-exec <file>` for JavaScript or TypeScript. The `node` command is a usage notice, so it does not execute scripts.
|
|
14
|
+
- Use `python3` or `python` for Python code. The first invocation waits for the WebAssembly runtime to load.
|
|
15
|
+
|
|
16
|
+
Read the matching reference before using runtime-specific APIs:
|
|
17
|
+
|
|
18
|
+
- Shell commands, mounts, SQLite, and networking: [references/shell.md](references/shell.md)
|
|
19
|
+
- JavaScript modules, globals, `fs`, and async callbacks: [references/javascript.md](references/javascript.md)
|
|
20
|
+
- Python modules, subprocesses, and persistence: [references/python.md](references/python.md)
|
|
21
|
+
|
|
22
|
+
## Shared paths
|
|
23
|
+
|
|
24
|
+
- `/home` is read-only and contains this instance's data directory.
|
|
25
|
+
- `/home/workspace` is the writable working directory. Put scripts and generated files here.
|
|
26
|
+
- `/home/skills` is read-only. Read a skill, then run only scripts that the task needs.
|
|
27
|
+
|
|
28
|
+
Each shell call is isolated by the configured timeout. Environment variables, functions, aliases, and the current directory return to their defaults on the next call.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# JavaScript Runtime
|
|
2
|
+
|
|
3
|
+
Run JavaScript and TypeScript with `js-exec <file>`. It uses QuickJS and supports ESM imports, top-level await, TypeScript syntax, `fetch`, and `node:child_process`. The `node` command does not run code.
|
|
4
|
+
|
|
5
|
+
## Modules and globals
|
|
6
|
+
|
|
7
|
+
The runtime exposes a partial Node.js surface. These modules are unavailable:
|
|
8
|
+
|
|
9
|
+
`node:zlib`, `node:crypto`, `node:net`, `node:http`, `node:https`, `node:dns`, `node:tls`, `node:readline`, `node:vm`, `node:v8`, `node:worker_threads`, `node:module`, `node:timers`, `node:tty`, and `node:sqlite3`.
|
|
10
|
+
|
|
11
|
+
These browser and JavaScript globals are unavailable:
|
|
12
|
+
|
|
13
|
+
`TextEncoder`, `TextDecoder`, `atob`, `btoa`, `setTimeout`, `setInterval`, `queueMicrotask`, `structuredClone`, `performance`, `WebAssembly`, and `crypto`.
|
|
14
|
+
|
|
15
|
+
Use the synchronous filesystem API or `fs.promises` for file work. Use `Buffer` for UTF-8 and base64 conversion.
|
|
16
|
+
|
|
17
|
+
## Filesystem API
|
|
18
|
+
|
|
19
|
+
The workspace bootstrap supplies common Node `fs` behavior, including:
|
|
20
|
+
|
|
21
|
+
- `Stats` and `Dirent` instances, `fs.constants`, `fs.accessSync`, and recursive `readdir`
|
|
22
|
+
- callback, promise, and synchronous forms for common file and directory methods
|
|
23
|
+
- file URL path arguments and `Buffer` results from `fs.readFileBuffer`
|
|
24
|
+
- Node-shaped error fields such as `code`, `errno`, `syscall`, `path`, and `dest`
|
|
25
|
+
|
|
26
|
+
The `node:fs` module loader publishes a fixed set of named exports. Use the default import when accessing bootstrap additions: `import fs from 'node:fs'`. Namespace imports expose the upstream method list, so additions such as `fs.constants`, `fs.Stats`, `fs.Dirent`, and `fs.accessSync` are absent from `import * as fs`.
|
|
27
|
+
|
|
28
|
+
The sandbox does not expose file descriptors or streams. `openSync`, `readSync`, `writeSync`, `createReadStream`, and `createWriteStream` are absent. `cp`, `truncate`, `utimes`, `mkdtemp`, `watch`, and `opendir` are also absent. Use whole-file reads and writes, or run a shell command for unsupported operations.
|
|
29
|
+
|
|
30
|
+
## Async callbacks
|
|
31
|
+
|
|
32
|
+
QuickJS stops when the script's synchronous top level finishes. Keep the script alive with top-level `await` until callback work completes, or use `fs.promises`:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
fs.readFile("/home/workspace/input.txt", "utf8", (error, text) => {
|
|
37
|
+
if (error) reject(error);
|
|
38
|
+
else {
|
|
39
|
+
console.log(text);
|
|
40
|
+
resolve();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Python Runtime
|
|
2
|
+
|
|
3
|
+
`python3` and `python` run CPython 3.13 through Emscripten. The first call waits for the runtime to load. Scripts read and write the same sandbox paths as the shell.
|
|
4
|
+
|
|
5
|
+
## Available modules
|
|
6
|
+
|
|
7
|
+
Most pure-Python standard library modules are available. Native integration is limited:
|
|
8
|
+
|
|
9
|
+
- `subprocess` is unavailable.
|
|
10
|
+
- `_sqlite3` and the `sqlite3` module are unavailable.
|
|
11
|
+
- Third-party packages cannot be installed.
|
|
12
|
+
- Long-running threads, sockets, and process management are unavailable.
|
|
13
|
+
|
|
14
|
+
Use `/home/workspace` for scripts and generated files. For SQLite work, use the shell's `sqlite3` command. To start Python from JavaScript, use `node:child_process`; Python cannot start shell commands or other programs.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Shell Runtime
|
|
2
|
+
|
|
3
|
+
just-bash provides a POSIX-style command set, not a Linux host. Shell state does not persist between calls, and each call runs under the configured timeout.
|
|
4
|
+
|
|
5
|
+
## Files and processes
|
|
6
|
+
|
|
7
|
+
- Use `/home/workspace` for writable files.
|
|
8
|
+
- `/home` and `/home/skills` are read-only mounts.
|
|
9
|
+
- Start a process and consume its output in the same call. Background processes do not outlive the call.
|
|
10
|
+
- Use `bash` pipelines for listing, searching, text transforms, and archive work that the JavaScript filesystem bridge does not expose.
|
|
11
|
+
|
|
12
|
+
## SQLite
|
|
13
|
+
|
|
14
|
+
The `sqlite3` command is available:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
sqlite3 /home/workspace/data.db "CREATE TABLE notes (body TEXT); INSERT INTO notes VALUES ('hello');"
|
|
18
|
+
sqlite3 /home/workspace/data.db "SELECT body FROM notes;"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The command does not support dot commands such as `.tables` or `.version`. Pass SQL as the command argument. Python's `sqlite3` module is unavailable, so use this command-line tool for database work.
|
|
22
|
+
|
|
23
|
+
## Network
|
|
24
|
+
|
|
25
|
+
When networking is enabled, `curl` and JavaScript `fetch` can reach public addresses. Private and loopback addresses are rejected. Check the task's configured network access before choosing a remote API.
|