@webskill/sdk 0.1.3 → 0.1.4
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/README.md +5 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +54 -22
- package/dist/{dist-Cm_j6Jol.js → dist-BVXZF7H_.js} +138 -52
- package/dist/{dist-DZobLFh6.js → dist-DAn07zHu.js} +24 -6
- package/dist/{dist-DS1sfgHa.js → dist-DvoBsChX.js} +13 -1
- package/dist/{dist-DxGyAznR.js → dist-MTc2KB03.js} +46 -3
- package/dist/governance.d.ts +7 -4
- package/dist/governance.js +19 -4
- package/dist/{index-CqMuvcb2.d.ts → index-BSS3EWY-.d.ts} +1 -1
- package/dist/{index-CPuwsnmB.d.ts → index-npFMt2Zw.d.ts} +16 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/sandboxWorkerEntry.d.ts +1 -0
- package/dist/sandboxWorkerEntry.js +214 -0
- package/dist/{testing-CbM6rJ-E.js → testing-S8SN9Lc6.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CgNC-oQu-Dq_A1yA-.d.ts → types-CgNC-oQu-DYrxhD3z.d.ts} +11 -1
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ governance.
|
|
|
16
16
|
(missing-parameter forms, confirmations, authorization prompts), and
|
|
17
17
|
resumable interrupted runs.
|
|
18
18
|
- **Script sandbox** — `worker_threads` sandbox for Node, Web Worker sandbox for
|
|
19
|
-
the browser. Deny-by-default network policy,
|
|
20
|
-
timeouts kill runaway scripts.
|
|
19
|
+
the browser. Deny-by-default network policy, deny-by-default builtin-module
|
|
20
|
+
allowlist, per-capability forced approval, timeouts kill runaway scripts.
|
|
21
|
+
**Honest note:** these executors reduce the capability surface — they are NOT
|
|
22
|
+
a security boundary. Scripts share the host process/origin and bypasses
|
|
23
|
+
exist; never rely on them to isolate untrusted scripts.
|
|
21
24
|
- **`navigator.webskill`** — a browser facade (`discover` / `read` / `validate` /
|
|
22
25
|
`run` / `install` / `uninstall`) assembled explicitly in one call.
|
|
23
26
|
- **MCP** — call page-provided tools and consume page-declared dynamic skills
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, x as FileStat } from "./types-CgNC-oQu-
|
|
2
|
-
import { $ as ToolDefinition, N as NetworkPolicy, X as ScriptExecutionContext, Z as ScriptExecutor, d as BridgeCapabilities, m as BridgeResponse, p as BridgeRequest, st as WebSkillApi, tt as ToolResult, u as ApprovalScope, ut as bridgeError, v as ExternalSkillProvider, xt as parseBridgeRequest, y as ExternalToolSource } from "./index-
|
|
1
|
+
import { H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, x as FileStat } from "./types-CgNC-oQu-DYrxhD3z.js";
|
|
2
|
+
import { $ as ToolDefinition, N as NetworkPolicy, X as ScriptExecutionContext, Z as ScriptExecutor, d as BridgeCapabilities, m as BridgeResponse, p as BridgeRequest, st as WebSkillApi, tt as ToolResult, u as ApprovalScope, ut as bridgeError, v as ExternalSkillProvider, xt as parseBridgeRequest, y as ExternalToolSource } from "./index-BSS3EWY-.js";
|
|
3
3
|
//#region ../browser/dist/index.d.ts
|
|
4
4
|
//#region src/fs/featureDetection.d.ts
|
|
5
5
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
package/dist/browser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-
|
|
3
|
-
import { n as MockLlmClient } from "./testing-
|
|
1
|
+
import { D as verifyManifest, E as validateSkills, S as parseSkillPackManifest, T as resolveInsideRoot, _ as exportSkills, d as buildCatalog, f as buildManifest, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, s as SkillDiscovery, v as isValidSkillName, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
|
|
2
|
+
import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DAn07zHu.js";
|
|
3
|
+
import { n as MockLlmClient } from "./testing-S8SN9Lc6.js";
|
|
4
4
|
import { unzipSync } from "fflate";
|
|
5
5
|
|
|
6
6
|
//#region ../browser/dist/index.js
|
|
@@ -508,19 +508,31 @@ var BrowserSkillManager = class {
|
|
|
508
508
|
const errors = report.issues.filter((i) => i.severity === "error");
|
|
509
509
|
throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
510
510
|
}
|
|
511
|
-
|
|
512
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
513
|
-
await copyDir(fs, finalDir, targetDir);
|
|
514
|
-
const manifest = await this.#createManifest(targetDir, {
|
|
511
|
+
const manifest = await this.#createManifest(finalDir, {
|
|
515
512
|
name,
|
|
516
513
|
...version ? { version } : {},
|
|
517
514
|
source
|
|
518
515
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
516
|
+
targetDir = `${this.#managedRoot}/${name}`;
|
|
517
|
+
const backupDir = `${stagingRoot}/backup`;
|
|
518
|
+
const hadPrevious = await fs.exists(targetDir);
|
|
519
|
+
if (hadPrevious) await copyDir(fs, targetDir, backupDir);
|
|
520
|
+
let committed = false;
|
|
521
|
+
try {
|
|
522
|
+
if (hadPrevious) await fs.remove(targetDir, { recursive: true });
|
|
523
|
+
await copyDir(fs, finalDir, targetDir);
|
|
524
|
+
committed = true;
|
|
525
|
+
await this.#upsertLockEntry(name, {
|
|
526
|
+
digest: manifest.integrity.digest,
|
|
527
|
+
installedAt: manifest.installedAt,
|
|
528
|
+
source
|
|
529
|
+
});
|
|
530
|
+
} catch (e) {
|
|
531
|
+
if (committed) await removeDirQuiet(fs, targetDir);
|
|
532
|
+
if (hadPrevious) await copyDir(fs, backupDir, targetDir);
|
|
533
|
+
targetDir = void 0;
|
|
534
|
+
throw e;
|
|
535
|
+
}
|
|
524
536
|
targetDir = void 0;
|
|
525
537
|
return manifest;
|
|
526
538
|
} catch (e) {
|
|
@@ -562,11 +574,7 @@ var BrowserSkillManager = class {
|
|
|
562
574
|
}
|
|
563
575
|
const manifests = [];
|
|
564
576
|
for (const entry of pack.manifest.skills) {
|
|
565
|
-
const
|
|
566
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
567
|
-
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
568
|
-
installed.push(targetDir);
|
|
569
|
-
const manifest = await this.#createManifest(targetDir, {
|
|
577
|
+
const manifest = await this.#createManifest(`${finalRoot}/${entry.name}`, {
|
|
570
578
|
name: entry.name,
|
|
571
579
|
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
572
580
|
source
|
|
@@ -574,11 +582,35 @@ var BrowserSkillManager = class {
|
|
|
574
582
|
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
575
583
|
manifests.push(manifest);
|
|
576
584
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
585
|
+
const swapped = [];
|
|
586
|
+
try {
|
|
587
|
+
for (const manifest of manifests) {
|
|
588
|
+
const targetDir = `${this.#managedRoot}/${manifest.name}`;
|
|
589
|
+
const backupDir = `${stagingRoot}/backup/${manifest.name}`;
|
|
590
|
+
if (await fs.exists(targetDir)) {
|
|
591
|
+
await copyDir(fs, targetDir, backupDir);
|
|
592
|
+
await fs.remove(targetDir, { recursive: true });
|
|
593
|
+
}
|
|
594
|
+
await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
|
|
595
|
+
installed.push(targetDir);
|
|
596
|
+
swapped.push({
|
|
597
|
+
targetDir,
|
|
598
|
+
backupDir
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
|
|
602
|
+
digest: manifest.integrity.digest,
|
|
603
|
+
installedAt: manifest.installedAt,
|
|
604
|
+
source
|
|
605
|
+
});
|
|
606
|
+
} catch (e) {
|
|
607
|
+
for (const { targetDir, backupDir } of swapped) {
|
|
608
|
+
await removeDirQuiet(fs, targetDir);
|
|
609
|
+
if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
|
|
610
|
+
}
|
|
611
|
+
installed.length = 0;
|
|
612
|
+
throw e;
|
|
613
|
+
}
|
|
582
614
|
return manifests;
|
|
583
615
|
} catch (e) {
|
|
584
616
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, c as FsArtifactStore, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-
|
|
1
|
+
import { D as verifyManifest, E as validateSkills, S as parseSkillPackManifest, T as resolveInsideRoot, _ as exportSkills, f as buildManifest, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, v as isValidSkillName, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
|
|
2
|
+
import { M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, c as FsArtifactStore, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-DAn07zHu.js";
|
|
3
3
|
import { unzipSync, zipSync } from "fflate";
|
|
4
4
|
import { existsSync, promises, readFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
@@ -15,39 +15,73 @@ import { execFile } from "node:child_process";
|
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
|
|
17
17
|
//#region ../node/dist/index.js
|
|
18
|
-
const toPlatform$
|
|
18
|
+
const toPlatform$4 = (p) => p.split("/").join(path.sep);
|
|
19
19
|
const toPosix = (p) => p.split(path.sep).join("/");
|
|
20
20
|
const isEnoent = (e) => typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
21
|
-
/**
|
|
21
|
+
/** realpath 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
|
|
22
|
+
async function realpathNearest(platformPath) {
|
|
23
|
+
let current = platformPath;
|
|
24
|
+
const rest = [];
|
|
25
|
+
for (;;) try {
|
|
26
|
+
const real = await promises.realpath(current);
|
|
27
|
+
return rest.length === 0 ? real : [real, ...rest].join(path.sep);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
if (!isEnoent(e)) throw e;
|
|
30
|
+
rest.unshift(path.basename(current));
|
|
31
|
+
const parent = path.dirname(current);
|
|
32
|
+
if (parent === current) return platformPath;
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 基于 node:fs/promises 的 FileSystemProvider;接受 `/` 风格路径,写入自动创建父目录。
|
|
38
|
+
* 可选 root 模式:构造传入 root 后,read/write 操作先做 realpath 包含校验
|
|
39
|
+
* (root 与目标都 realpath 后前缀比对),经符号链接逃逸 root → FS_PATH_OUTSIDE_ROOT。
|
|
40
|
+
*/
|
|
22
41
|
var NodeFS = class {
|
|
23
42
|
kind = "node";
|
|
43
|
+
#root;
|
|
44
|
+
constructor(deps = {}) {
|
|
45
|
+
this.#root = deps.root;
|
|
46
|
+
}
|
|
47
|
+
/** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
|
|
48
|
+
async #assertContained(p) {
|
|
49
|
+
if (!this.#root) return;
|
|
50
|
+
const rootReal = await promises.realpath(toPlatform$4(this.#root));
|
|
51
|
+
const targetReal = await realpathNearest(toPlatform$4(p));
|
|
52
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + path.sep)) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path escapes root via symbolic link: ${p} (resolves outside ${this.#root})`);
|
|
53
|
+
}
|
|
24
54
|
async readText(p) {
|
|
55
|
+
await this.#assertContained(p);
|
|
25
56
|
try {
|
|
26
|
-
return await promises.readFile(toPlatform$
|
|
57
|
+
return await promises.readFile(toPlatform$4(p), "utf8");
|
|
27
58
|
} catch (e) {
|
|
28
59
|
throw this.#mapError(e, p);
|
|
29
60
|
}
|
|
30
61
|
}
|
|
31
62
|
async writeText(p, content) {
|
|
32
|
-
|
|
63
|
+
await this.#assertContained(p);
|
|
64
|
+
const target = toPlatform$4(p);
|
|
33
65
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
34
66
|
await promises.writeFile(target, content, "utf8");
|
|
35
67
|
}
|
|
36
68
|
async readBinary(p) {
|
|
69
|
+
await this.#assertContained(p);
|
|
37
70
|
try {
|
|
38
|
-
return await promises.readFile(toPlatform$
|
|
71
|
+
return await promises.readFile(toPlatform$4(p));
|
|
39
72
|
} catch (e) {
|
|
40
73
|
throw this.#mapError(e, p);
|
|
41
74
|
}
|
|
42
75
|
}
|
|
43
76
|
async writeBinary(p, content) {
|
|
44
|
-
|
|
77
|
+
await this.#assertContained(p);
|
|
78
|
+
const target = toPlatform$4(p);
|
|
45
79
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
46
80
|
await promises.writeFile(target, content);
|
|
47
81
|
}
|
|
48
82
|
async exists(p) {
|
|
49
83
|
try {
|
|
50
|
-
await promises.access(toPlatform$
|
|
84
|
+
await promises.access(toPlatform$4(p));
|
|
51
85
|
return true;
|
|
52
86
|
} catch {
|
|
53
87
|
return false;
|
|
@@ -55,7 +89,7 @@ var NodeFS = class {
|
|
|
55
89
|
}
|
|
56
90
|
async stat(p) {
|
|
57
91
|
try {
|
|
58
|
-
const s = await promises.stat(toPlatform$
|
|
92
|
+
const s = await promises.stat(toPlatform$4(p));
|
|
59
93
|
return {
|
|
60
94
|
path: toPosix(p),
|
|
61
95
|
type: s.isDirectory() ? "directory" : "file",
|
|
@@ -69,21 +103,21 @@ var NodeFS = class {
|
|
|
69
103
|
async list(p) {
|
|
70
104
|
let dirents;
|
|
71
105
|
try {
|
|
72
|
-
dirents = await promises.readdir(toPlatform$
|
|
106
|
+
dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
|
|
73
107
|
} catch (e) {
|
|
74
108
|
throw this.#mapError(e, p);
|
|
75
109
|
}
|
|
76
110
|
return dirents.map((d) => ({
|
|
77
|
-
path: toPosix(path.join(toPlatform$
|
|
111
|
+
path: toPosix(path.join(toPlatform$4(p), d.name)),
|
|
78
112
|
type: d.isDirectory() ? "directory" : "file"
|
|
79
113
|
}));
|
|
80
114
|
}
|
|
81
115
|
async mkdir(p) {
|
|
82
|
-
await promises.mkdir(toPlatform$
|
|
116
|
+
await promises.mkdir(toPlatform$4(p), { recursive: true });
|
|
83
117
|
}
|
|
84
118
|
async remove(p, options) {
|
|
85
119
|
try {
|
|
86
|
-
await promises.rm(toPlatform$
|
|
120
|
+
await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
|
|
87
121
|
} catch (e) {
|
|
88
122
|
throw this.#mapError(e, p);
|
|
89
123
|
}
|
|
@@ -94,7 +128,7 @@ var NodeFS = class {
|
|
|
94
128
|
}
|
|
95
129
|
};
|
|
96
130
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
97
|
-
const toPlatform$
|
|
131
|
+
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
98
132
|
/**
|
|
99
133
|
* 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
|
|
100
134
|
* worker_threads 沙箱见 deferred-items D1。
|
|
@@ -114,7 +148,7 @@ var NodeScriptExecutor = class {
|
|
|
114
148
|
return hasTs ? tsPath : jsPath;
|
|
115
149
|
}
|
|
116
150
|
async #loadModule(scriptPath) {
|
|
117
|
-
return await import(`${pathToFileURL(toPlatform$
|
|
151
|
+
return await import(`${pathToFileURL(toPlatform$3(scriptPath)).href}?t=${Date.now()}`);
|
|
118
152
|
}
|
|
119
153
|
async loadDefinition(skillRoot, scriptName) {
|
|
120
154
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
@@ -223,14 +257,14 @@ var NodeScriptExecutor = class {
|
|
|
223
257
|
}
|
|
224
258
|
};
|
|
225
259
|
const baseName = (p) => p.split("/").pop() ?? p;
|
|
226
|
-
const toPlatform$
|
|
260
|
+
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
227
261
|
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
228
262
|
/**
|
|
229
263
|
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
230
264
|
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
231
265
|
*/
|
|
232
266
|
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
233
|
-
/** Worker 入口文件:src 为同目录 .ts;dist 为 dist/executor
|
|
267
|
+
/** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
|
|
234
268
|
function workerEntryPath() {
|
|
235
269
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
236
270
|
const candidates = [
|
|
@@ -239,13 +273,17 @@ function workerEntryPath() {
|
|
|
239
273
|
path.join(dir, "executor", "sandboxWorkerEntry.js")
|
|
240
274
|
];
|
|
241
275
|
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
242
|
-
|
|
276
|
+
throw new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker entry not found (tried: ${candidates.join(", ")})`);
|
|
243
277
|
}
|
|
244
278
|
/**
|
|
245
279
|
* D1:worker_threads 沙箱脚本执行器。
|
|
246
280
|
* 每次执行独立 Worker(resourceLimits + env 白名单),loadDefinition 同样在
|
|
247
281
|
* Worker 内完成(禁止主进程 import 不可信脚本);超时 terminate(可杀同步死循环)。
|
|
248
282
|
* 能力桥协议与浏览器同一来源(runtime/sandbox/bridgeProtocol)。
|
|
283
|
+
*
|
|
284
|
+
* 诚实标注:本执行器做的是**能力面收敛**(网络策略、模块 allowlist、资源限额、
|
|
285
|
+
* 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程,仍有绕过手段;
|
|
286
|
+
* 禁止假定其可隔离不可信脚本。
|
|
249
287
|
*/
|
|
250
288
|
var SandboxedScriptExecutor = class {
|
|
251
289
|
#fs;
|
|
@@ -280,7 +318,7 @@ var SandboxedScriptExecutor = class {
|
|
|
280
318
|
const skillName = baseName(skillRoot);
|
|
281
319
|
const result = await this.#runWorker({
|
|
282
320
|
mode: "load",
|
|
283
|
-
scriptPath: toPlatform$
|
|
321
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
284
322
|
scriptName
|
|
285
323
|
});
|
|
286
324
|
if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
|
|
@@ -310,7 +348,7 @@ var SandboxedScriptExecutor = class {
|
|
|
310
348
|
try {
|
|
311
349
|
const result = await this.#runWorker({
|
|
312
350
|
mode: "execute",
|
|
313
|
-
scriptPath: toPlatform$
|
|
351
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
314
352
|
scriptName,
|
|
315
353
|
skillName: context.skillName,
|
|
316
354
|
runId: context.runId,
|
|
@@ -356,7 +394,8 @@ var SandboxedScriptExecutor = class {
|
|
|
356
394
|
workerData: {
|
|
357
395
|
...workerData,
|
|
358
396
|
networkPolicy: this.#networkPolicy,
|
|
359
|
-
networkPolicyLib: NETWORK_POLICY_LIB
|
|
397
|
+
networkPolicyLib: NETWORK_POLICY_LIB,
|
|
398
|
+
allowedModules: this.#options.allowedModules ?? []
|
|
360
399
|
},
|
|
361
400
|
env,
|
|
362
401
|
resourceLimits: this.#options.resourceLimits ?? {
|
|
@@ -776,7 +815,7 @@ var CliUiBridge = class {
|
|
|
776
815
|
}
|
|
777
816
|
};
|
|
778
817
|
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
779
|
-
const toPlatform = (p) => p.split("/").join(path.sep);
|
|
818
|
+
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
780
819
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
781
820
|
/**
|
|
782
821
|
* zip 解包(fflate)。每个条目路径过 resolveInsideRoot:
|
|
@@ -803,15 +842,15 @@ async function extractTarFile(archivePath, destRoot) {
|
|
|
803
842
|
try {
|
|
804
843
|
const entryPaths = [];
|
|
805
844
|
await tar.t({
|
|
806
|
-
file: toPlatform(archivePath),
|
|
845
|
+
file: toPlatform$1(archivePath),
|
|
807
846
|
onentry: (entry) => {
|
|
808
847
|
entryPaths.push(entry.path);
|
|
809
848
|
}
|
|
810
849
|
});
|
|
811
850
|
for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
|
|
812
851
|
await tar.x({
|
|
813
|
-
file: toPlatform(archivePath),
|
|
814
|
-
cwd: toPlatform(destRoot),
|
|
852
|
+
file: toPlatform$1(archivePath),
|
|
853
|
+
cwd: toPlatform$1(destRoot),
|
|
815
854
|
filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
|
|
816
855
|
});
|
|
817
856
|
} catch (e) {
|
|
@@ -830,8 +869,23 @@ async function listFiles(fs, root, prefix = "") {
|
|
|
830
869
|
}
|
|
831
870
|
return out;
|
|
832
871
|
}
|
|
833
|
-
|
|
872
|
+
const toPlatform = (p) => p.split("/").join(path.sep);
|
|
873
|
+
/** lstat 扫描拒绝符号链接条目(与 tar 解包 filter 策略一致;虚拟 fs 路径不存在于真实 fs 时跳过) */
|
|
874
|
+
async function assertNoSymlinks(platformRoot) {
|
|
875
|
+
let entries;
|
|
876
|
+
try {
|
|
877
|
+
entries = await promises.readdir(platformRoot, { withFileTypes: true });
|
|
878
|
+
} catch {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
for (const entry of entries) {
|
|
882
|
+
if (entry.isSymbolicLink()) throw new WebSkillError("INSTALL_FAILED", `Refusing to copy symbolic link entry: ${platformRoot}${path.sep}${entry.name}`);
|
|
883
|
+
if (entry.isDirectory()) await assertNoSymlinks(`${platformRoot}${path.sep}${entry.name}`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/** 递归拷贝目录(含子目录与二进制文件);真实 fs 上先做符号链接扫描 */
|
|
834
887
|
async function copyDir(fs, from, to) {
|
|
888
|
+
await assertNoSymlinks(toPlatform(from));
|
|
835
889
|
await fs.mkdir(to);
|
|
836
890
|
for (const entry of await fs.list(from)) {
|
|
837
891
|
const name = entry.path.split("/").pop() ?? "";
|
|
@@ -854,8 +908,8 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
854
908
|
for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
|
|
855
909
|
await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
|
|
856
910
|
} else await tar.c({
|
|
857
|
-
file: toPlatform(options.outPath),
|
|
858
|
-
cwd: toPlatform(skillRoot)
|
|
911
|
+
file: toPlatform$1(options.outPath),
|
|
912
|
+
cwd: toPlatform$1(skillRoot)
|
|
859
913
|
}, ["."]);
|
|
860
914
|
return options.outPath;
|
|
861
915
|
} catch (e) {
|
|
@@ -876,7 +930,7 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
876
930
|
let manifestText;
|
|
877
931
|
try {
|
|
878
932
|
await tar.t({
|
|
879
|
-
file: toPlatform(archivePath),
|
|
933
|
+
file: toPlatform$1(archivePath),
|
|
880
934
|
onentry: (entry) => {
|
|
881
935
|
if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
|
|
882
936
|
const chunks = [];
|
|
@@ -970,7 +1024,7 @@ async function stageHttp(source, ctx, expectedSha256) {
|
|
|
970
1024
|
else {
|
|
971
1025
|
const archivePath = `${ctx.stagingRoot}/archive.tar`;
|
|
972
1026
|
await ctx.fs.writeBinary(archivePath, data);
|
|
973
|
-
await extractTarFile(toPlatform(archivePath), toPlatform(contentDir));
|
|
1027
|
+
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir));
|
|
974
1028
|
}
|
|
975
1029
|
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
976
1030
|
if (await ctx.fs.exists(packFilePath)) return {
|
|
@@ -1021,7 +1075,7 @@ async function stageNpm(source, ctx) {
|
|
|
1021
1075
|
if (!tarball || !await ctx.fs.exists(tgzPath)) throw new WebSkillError("INSTALL_FAILED", `npm pack output not found in staging directory: ${tarball ?? "(empty output)"}`);
|
|
1022
1076
|
const pkgDir = `${ctx.stagingRoot}/pkg`;
|
|
1023
1077
|
await ctx.fs.mkdir(pkgDir);
|
|
1024
|
-
await extractTarFile(toPlatform(tgzPath), toPlatform(pkgDir));
|
|
1078
|
+
await extractTarFile(toPlatform$1(tgzPath), toPlatform$1(pkgDir));
|
|
1025
1079
|
const pkgRoot = `${pkgDir}/package`;
|
|
1026
1080
|
const skillDir = await ctx.fs.exists(`${pkgRoot}/skill/SKILL.md`) ? `${pkgRoot}/skill` : pkgRoot;
|
|
1027
1081
|
let packageName = source.packageName;
|
|
@@ -1187,20 +1241,32 @@ var SkillManager = class {
|
|
|
1187
1241
|
const errors = report.issues.filter((i) => i.severity === "error");
|
|
1188
1242
|
throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
1189
1243
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
await copyDir(fs, finalDir, targetDir);
|
|
1193
|
-
await this.#inferSkillSchemas(targetDir);
|
|
1194
|
-
const manifest = await createManifest(fs, targetDir, {
|
|
1244
|
+
await this.#inferSkillSchemas(finalDir);
|
|
1245
|
+
const manifest = await createManifest(fs, finalDir, {
|
|
1195
1246
|
name,
|
|
1196
1247
|
...version ? { version } : {},
|
|
1197
1248
|
source: staged.source
|
|
1198
1249
|
});
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1250
|
+
targetDir = `${this.#managedRoot}/${name}`;
|
|
1251
|
+
const backupDir = `${stagingRoot}/backup`;
|
|
1252
|
+
const hadPrevious = await fs.exists(targetDir);
|
|
1253
|
+
if (hadPrevious) await copyDir(fs, targetDir, backupDir);
|
|
1254
|
+
let committed = false;
|
|
1255
|
+
try {
|
|
1256
|
+
if (hadPrevious) await fs.remove(targetDir, { recursive: true });
|
|
1257
|
+
await copyDir(fs, finalDir, targetDir);
|
|
1258
|
+
committed = true;
|
|
1259
|
+
await upsertLockEntry(fs, this.#managedRoot, name, {
|
|
1260
|
+
digest: manifest.integrity.digest,
|
|
1261
|
+
installedAt: manifest.installedAt,
|
|
1262
|
+
source: staged.source
|
|
1263
|
+
});
|
|
1264
|
+
} catch (e) {
|
|
1265
|
+
if (committed) await removeDirQuiet(fs, targetDir);
|
|
1266
|
+
if (hadPrevious) await copyDir(fs, backupDir, targetDir);
|
|
1267
|
+
targetDir = void 0;
|
|
1268
|
+
throw e;
|
|
1269
|
+
}
|
|
1204
1270
|
targetDir = void 0;
|
|
1205
1271
|
return manifest;
|
|
1206
1272
|
} catch (e) {
|
|
@@ -1243,11 +1309,7 @@ var SkillManager = class {
|
|
|
1243
1309
|
}
|
|
1244
1310
|
const manifests = [];
|
|
1245
1311
|
for (const entry of pack.manifest.skills) {
|
|
1246
|
-
const
|
|
1247
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
1248
|
-
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
1249
|
-
installed.push(targetDir);
|
|
1250
|
-
const manifest = await createManifest(fs, targetDir, {
|
|
1312
|
+
const manifest = await createManifest(fs, `${finalRoot}/${entry.name}`, {
|
|
1251
1313
|
name: entry.name,
|
|
1252
1314
|
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
1253
1315
|
source
|
|
@@ -1255,11 +1317,35 @@ var SkillManager = class {
|
|
|
1255
1317
|
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
1256
1318
|
manifests.push(manifest);
|
|
1257
1319
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1320
|
+
const swapped = [];
|
|
1321
|
+
try {
|
|
1322
|
+
for (const manifest of manifests) {
|
|
1323
|
+
const targetDir = `${this.#managedRoot}/${manifest.name}`;
|
|
1324
|
+
const backupDir = `${stagingRoot}/backup/${manifest.name}`;
|
|
1325
|
+
if (await fs.exists(targetDir)) {
|
|
1326
|
+
await copyDir(fs, targetDir, backupDir);
|
|
1327
|
+
await fs.remove(targetDir, { recursive: true });
|
|
1328
|
+
}
|
|
1329
|
+
await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
|
|
1330
|
+
installed.push(targetDir);
|
|
1331
|
+
swapped.push({
|
|
1332
|
+
targetDir,
|
|
1333
|
+
backupDir
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
|
|
1337
|
+
digest: manifest.integrity.digest,
|
|
1338
|
+
installedAt: manifest.installedAt,
|
|
1339
|
+
source
|
|
1340
|
+
});
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
for (const { targetDir, backupDir } of swapped) {
|
|
1343
|
+
await removeDirQuiet(fs, targetDir);
|
|
1344
|
+
if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
|
|
1345
|
+
}
|
|
1346
|
+
installed.length = 0;
|
|
1347
|
+
throw e;
|
|
1348
|
+
}
|
|
1263
1349
|
return manifests;
|
|
1264
1350
|
} catch (e) {
|
|
1265
1351
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillReader, d as buildCatalog, l as WebSkillError, s as SkillDiscovery, u as assertSafePathSegment, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
|
|
2
2
|
import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
|
|
3
3
|
|
|
4
4
|
//#region ../runtime/dist/index.js
|
|
@@ -1577,6 +1577,7 @@ var AgentLoop = class {
|
|
|
1577
1577
|
}
|
|
1578
1578
|
/** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
|
|
1579
1579
|
async #handleReadExternalSkill(skillKey, pathArg, state) {
|
|
1580
|
+
const failures = [];
|
|
1580
1581
|
for (const provider of this.#deps.skillProviders ?? []) try {
|
|
1581
1582
|
let name = skillKey;
|
|
1582
1583
|
if (skillKey.startsWith("mcp://")) {
|
|
@@ -1610,10 +1611,12 @@ var AgentLoop = class {
|
|
|
1610
1611
|
text: await provider.readFile(name, pathArg)
|
|
1611
1612
|
}]
|
|
1612
1613
|
};
|
|
1613
|
-
} catch {
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
failures.push(messageOf$1(e));
|
|
1614
1616
|
continue;
|
|
1615
1617
|
}
|
|
1616
|
-
|
|
1618
|
+
const detail = failures.length > 0 ? ` (provider errors: ${failures.join("; ")})` : "";
|
|
1619
|
+
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
1617
1620
|
}
|
|
1618
1621
|
/** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
|
|
1619
1622
|
async #activateSkill(skillName, state, via, skillMdText) {
|
|
@@ -1635,7 +1638,9 @@ var AgentLoop = class {
|
|
|
1635
1638
|
const rawAllowed = metadata["allowed-tools"];
|
|
1636
1639
|
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) allowedTools = rawAllowed.filter((e) => typeof e === "string");
|
|
1637
1640
|
else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
1638
|
-
} catch {
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf$1(e)}` });
|
|
1643
|
+
}
|
|
1639
1644
|
const executor = this.#deps.executor;
|
|
1640
1645
|
const loaded = [];
|
|
1641
1646
|
if (executor) {
|
|
@@ -1654,7 +1659,9 @@ var AgentLoop = class {
|
|
|
1654
1659
|
await this.#enrichDefinition(root, match[1], def, state);
|
|
1655
1660
|
state.activatedTools.set(def.name, def);
|
|
1656
1661
|
loaded.push(def.name);
|
|
1657
|
-
} catch {
|
|
1662
|
+
} catch (e) {
|
|
1663
|
+
state.trace.record("run.warning", { message: `Failed to load definition of script "${match[1]}" for skill "${skillName}": ${messageOf$1(e)}` });
|
|
1664
|
+
}
|
|
1658
1665
|
}
|
|
1659
1666
|
}
|
|
1660
1667
|
let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
|
|
@@ -1924,10 +1931,12 @@ var WebSkillRuntime = class {
|
|
|
1924
1931
|
if (!this.#catalogCache) await this.discover();
|
|
1925
1932
|
const cache = this.#catalogCache;
|
|
1926
1933
|
if (!cache) throw new Error("discover() did not populate the catalog cache");
|
|
1934
|
+
const providerFailures = [];
|
|
1927
1935
|
const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
|
|
1928
1936
|
try {
|
|
1929
1937
|
return await p.listSkills();
|
|
1930
|
-
} catch {
|
|
1938
|
+
} catch (e) {
|
|
1939
|
+
providerFailures.push(e instanceof Error ? e.message : String(e));
|
|
1931
1940
|
return [];
|
|
1932
1941
|
}
|
|
1933
1942
|
}))).flat();
|
|
@@ -1957,6 +1966,13 @@ var WebSkillRuntime = class {
|
|
|
1957
1966
|
userPrompt,
|
|
1958
1967
|
route
|
|
1959
1968
|
});
|
|
1969
|
+
for (const failure of providerFailures) result.run.trace.push({
|
|
1970
|
+
id: `evt-provider-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1971
|
+
runId: result.run.id,
|
|
1972
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1973
|
+
type: "run.warning",
|
|
1974
|
+
message: `Skill provider listSkills() failed: ${failure}`
|
|
1975
|
+
});
|
|
1960
1976
|
if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
|
|
1961
1977
|
prompt: userPrompt,
|
|
1962
1978
|
run: result.run
|
|
@@ -2279,6 +2295,7 @@ var FsArtifactStore = class {
|
|
|
2279
2295
|
});
|
|
2280
2296
|
}
|
|
2281
2297
|
async listArtifacts(runId) {
|
|
2298
|
+
assertSafePathSegment(runId, "runId");
|
|
2282
2299
|
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
2283
2300
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
2284
2301
|
const raw = await this.#fs.readText(indexPath);
|
|
@@ -2290,6 +2307,7 @@ var FsArtifactStore = class {
|
|
|
2290
2307
|
}
|
|
2291
2308
|
}
|
|
2292
2309
|
#artifactPath(runId, artifactPath) {
|
|
2310
|
+
assertSafePathSegment(runId, "runId");
|
|
2293
2311
|
return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
|
|
2294
2312
|
}
|
|
2295
2313
|
async #persist(input) {
|
|
@@ -174,8 +174,20 @@ function normalizePath(path) {
|
|
|
174
174
|
return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
175
175
|
}
|
|
176
176
|
/**
|
|
177
|
+
* 外部标识(runId、candidateId、skillName、versionId 等)作为单一路径段使用前的统一校验:
|
|
178
|
+
* 拒绝空串、`.`、`..`、含 `/` 或 `\`、含 `:`(Windows 盘符/ADS)。
|
|
179
|
+
* 违规抛 FS_PATH_OUTSIDE_ROOT(kind 用于错误消息定位,如 "runId")。
|
|
180
|
+
*/
|
|
181
|
+
function assertSafePathSegment(segment, kind) {
|
|
182
|
+
if (segment === "" || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes(":")) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Invalid ${kind} (must be a single safe path segment): ${JSON.stringify(segment)}`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
177
185
|
* 将相对路径安全地解析到 root 之内。
|
|
178
186
|
* 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
|
|
187
|
+
*
|
|
188
|
+
* 注意:本函数仅做**词法**校验,不解析符号链接——root 内经符号链接指向
|
|
189
|
+
* 外部的目标无法被词法检查拦截(realpath 防护见 NodeFS root 模式与
|
|
190
|
+
* 安装管线的 lstat 符号链接扫描)。
|
|
179
191
|
*/
|
|
180
192
|
function resolveInsideRoot(root, relativePath) {
|
|
181
193
|
const fail = (reason) => {
|
|
@@ -587,4 +599,4 @@ const xmlRenderer = {
|
|
|
587
599
|
};
|
|
588
600
|
|
|
589
601
|
//#endregion
|
|
590
|
-
export {
|
|
602
|
+
export { renderAvailableSkillsXml as C, verifyManifest as D, validateSkills as E, xmlRenderer as O, parseSkillPackManifest as S, resolveInsideRoot as T, exportSkills as _, SKILL_NAME_PATTERN as a, normalizePath as b, SkillReader as c, buildCatalog as d, buildManifest as f, escapeXml as g, computeDigest as h, SKILL_NAME_MAX_LENGTH as i, WebSkillError as l, checkSkillRules as m, SKILLS_LOCKFILE as n, SKILL_PACK_FILE as o, checkDependencyCycles as p, SKILL_MANIFEST_FILE as r, SkillDiscovery as s, MemoryFS as t, assertSafePathSegment as u, isValidSkillName as v, renderCatalogJson as w, parseSkillMarkdown as x, jsonRenderer as y };
|