@webskill/sdk 0.2.2 → 0.2.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 CHANGED
@@ -16,6 +16,7 @@ governance.
16
16
  (missing-parameter forms, confirmations, authorization prompts), and
17
17
  resumable interrupted runs.
18
18
  - **Script sandbox** — two tiers. Isolation-grade: Node `ProcessSandboxExecutor`
19
+ (fork + `--permission`, real process isolation).
19
20
  (`child_process.fork` + `--permission`, experimental; fs scoped to the skill
20
21
  root, workers/child processes/addons denied by default — note the permission
21
22
  model has NO network dimension, so `fetch`/`WebSocket` stay patch-enforced and
@@ -25,6 +26,9 @@ governance.
25
26
  tier (NOT a security boundary): `SandboxedScriptExecutor` /
26
27
  `BrowserWorkerScriptExecutor` in blob-Worker mode, with deny-by-default
27
28
  network policy, builtin-module allowlist, forced approval, timeouts.
29
+ Known proactive escape hatches (`process.binding`, `dlopen`, `abort`, …) are
30
+ stripped in the worker entry (0.2.3); other host-shared surfaces are NOT
31
+ defended — use `ProcessSandboxExecutor` for untrusted skills.
28
32
  - **`navigator.webskill`** — a browser facade (`discover` / `read` / `validate` /
29
33
  `run` / `install` / `uninstall`) assembled explicitly in one call.
30
34
  - **MCP** — call page-provided tools and consume page-declared dynamic skills
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as FileStat, I as SkillInstallSource, K as VerifyResult, W as SkillsLockfile, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CKm5G_eQ-8W8FnP4u.js";
2
- import { N as NetworkPolicy, Q as ScriptExecutor, Z as ScriptExecutionContext, d as BridgeCapabilities, it as ToolResult, m as BridgeResponse, nt as ToolDefinition, p as BridgeRequest, pt as bridgeError, u as ApprovalScope, ut as WebSkillApi, v as ExternalSkillProvider, wt as parseBridgeRequest, y as ExternalToolSource } from "./index-gjFuBevI.js";
1
+ import { C as FileStat, I as SkillInstallSource, K as VerifyResult, W as SkillsLockfile, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CKm5G_eQ-BqyXnvoR.js";
2
+ import { Et as parseBridgeRequest, N as NetworkPolicy, Q as ScriptExecutor, Z as ScriptExecutionContext, d as BridgeCapabilities, it as ToolResult, m as BridgeResponse, nt as ToolDefinition, p as BridgeRequest, pt as bridgeError, u as ApprovalScope, ut as WebSkillApi, v as ExternalSkillProvider, y as ExternalToolSource } from "./index-DZShzhon.js";
3
3
  //#region ../browser/dist/index.d.ts
4
4
  //#region src/fs/featureDetection.d.ts
5
5
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -15,6 +15,7 @@ declare class OpfsProvider implements FileSystemProvider {
15
15
  });
16
16
  readText(p: string): Promise<string>;
17
17
  writeText(p: string, content: string): Promise<void>;
18
+ appendText(p: string, content: string): Promise<void>;
18
19
  readBinary(p: string): Promise<Uint8Array>;
19
20
  writeBinary(p: string, content: Uint8Array): Promise<void>;
20
21
  exists(p: string): Promise<boolean>;
@@ -164,14 +165,16 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
164
165
  //#region src/executor/iframeSandbox.d.ts
165
166
  /**
166
167
  * opaque origin 沙箱 iframe(sandbox="allow-scripts" srcdoc):
167
- * iframe 内建 module Worker(Blob URL),iframe window postMessage 中继。
168
- * opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS(独立 storage 分区)。
169
- * 就绪握手:iframe 加载完成发 sandbox-ready 后才放行出站消息(防丢失)。
168
+ * iframe 内建 classic Worker(Blob URL;module Worker opaque origin 不可加载,实证),
169
+ * iframe window 作 postMessage 中继。opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS
170
+ * 通道鉴别(P0-C):每实例 channelId 随消息携带,宿主校验 event.source 与 channelId,
171
+ * 同页伪造消息一律丢弃;terminate 摘除监听器。
170
172
  */
171
173
  declare class IframeWorkerLike implements WorkerLike {
172
174
  #private;
173
- constructor(iframe: HTMLIFrameElement);
175
+ constructor(iframe: HTMLIFrameElement, hostWindow: Window, channelId: string);
174
176
  get ready(): boolean;
177
+ get channelId(): string;
175
178
  markReady(): void;
176
179
  postMessage(data: unknown): void;
177
180
  addEventListener(_type: 'message', listener: (event: {
@@ -181,7 +184,7 @@ declare class IframeWorkerLike implements WorkerLike {
181
184
  terminate(): void;
182
185
  }
183
186
  /**
184
- * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 module Worker。
187
+ * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 classic Worker。
185
188
  * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
186
189
  */
187
190
  declare function createIframeWorker(bootstrapSource: string, doc?: Document): IframeWorkerLike;
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
- import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, T as readResponseWithLimit, b as isValidSkillName, c as SkillDiscovery, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, p as buildCatalog, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D0saNPi_.js";
2
- import { A as isNetworkAllowed, C as bridgeError, E as createWebSkillApi, M as networkUrlHost, N as normalizeToolContent, P as parseBridgeRequest, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as mergeCatalogEntries, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DrySSQ5R.js";
3
- import { n as MockLlmClient } from "./testing-B4pq6JYa.js";
1
+ import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, T as readResponseWithLimit, b as isValidSkillName, c as SkillDiscovery, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, p as buildCatalog, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D7MsoMPx.js";
2
+ import { A as isNetworkAllowed, C as bridgeError, E as createWebSkillApi, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as mergeCatalogEntries, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-CV64gN62.js";
3
+ import { n as MockLlmClient } from "./testing-BN18eqbD.js";
4
4
 
5
5
  //#region ../browser/dist/index.js
6
6
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
@@ -57,6 +57,22 @@ var OpfsProvider = class {
57
57
  async writeText(p, content) {
58
58
  await this.writeBinary(p, new TextEncoder().encode(content));
59
59
  }
60
+ async appendText(p, content) {
61
+ await this.#wrap(p, async () => {
62
+ const segments = this.#segments(p);
63
+ const name = segments.pop();
64
+ if (!name) throw new WebSkillError("FS_NOT_FOUND", `Invalid file path: ${p}`);
65
+ const handle = await (await this.#walkDir(`/${segments.join("/")}`, true)).getFileHandle(name, { create: true });
66
+ const size = (await handle.getFile()).size;
67
+ const writable = await handle.createWritable({ keepExistingData: true });
68
+ await writable.write({
69
+ type: "write",
70
+ position: size,
71
+ data: content
72
+ });
73
+ await writable.close();
74
+ });
75
+ }
60
76
  async readBinary(p) {
61
77
  return this.#wrap(p, async () => {
62
78
  const handle = await this.#walkFile(p);
@@ -232,6 +248,16 @@ function loadModule(source) {
232
248
  return import(url);
233
249
  }
234
250
 
251
+ function assertSerializable(value) {
252
+ try {
253
+ structuredClone(value);
254
+ } catch (e) {
255
+ var err = new Error('Script returned a non-serializable value: ' + String((e && e.message) || e));
256
+ err.code = 'TOOL_EXECUTION_FAILED';
257
+ throw err;
258
+ }
259
+ }
260
+
235
261
  function postError(type, id, code, message, extra) {
236
262
  var msg = { type: type, id: id, ok: false, error: { code: code, message: message } };
237
263
  if (extra) Object.assign(msg, extra);
@@ -326,6 +352,7 @@ self.onmessage = async function (event) {
326
352
  throw new Error('Script does not export a run function');
327
353
  }
328
354
  var value = await mod2.run(msg.args, makeContext(msg));
355
+ assertSerializable(value);
329
356
  self.postMessage({
330
357
  type: 'execute-result', id: msg.id, ok: true,
331
358
  value: value === undefined ? null : value,
@@ -714,22 +741,38 @@ var BrowserSkillManager = class {
714
741
  }
715
742
  };
716
743
  const ENVELOPE = "__webskill_sandbox__";
744
+ let channelSeq = 0;
745
+ /** 每实例 channelId(防同页伪造消息注入;crypto.randomUUID 优先,无则随机回退) */
746
+ function nextChannelId() {
747
+ return globalThis.crypto?.randomUUID?.() ?? `ch-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${++channelSeq}`;
748
+ }
717
749
  /**
718
750
  * opaque origin 沙箱 iframe(sandbox="allow-scripts" srcdoc):
719
- * iframe 内建 module Worker(Blob URL),iframe window postMessage 中继。
720
- * opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS(独立 storage 分区)。
721
- * 就绪握手:iframe 加载完成发 sandbox-ready 后才放行出站消息(防丢失)。
751
+ * iframe 内建 classic Worker(Blob URL;module Worker opaque origin 不可加载,实证),
752
+ * iframe window 作 postMessage 中继。opaque origin:沙箱内摸不到页面 DOM/localStorage/OPFS
753
+ * 通道鉴别(P0-C):每实例 channelId 随消息携带,宿主校验 event.source 与 channelId,
754
+ * 同页伪造消息一律丢弃;terminate 摘除监听器。
722
755
  */
723
756
  var IframeWorkerLike = class {
724
757
  #iframe;
725
758
  #ready = false;
726
759
  #queue = [];
727
- constructor(iframe) {
760
+ #channelId;
761
+ #hostWindow;
762
+ #hostListener;
763
+ constructor(iframe, hostWindow, channelId) {
728
764
  this.#iframe = iframe;
765
+ this.#hostWindow = hostWindow;
766
+ this.#channelId = channelId;
767
+ this.#hostListener = (event) => this.#onMessage(event);
768
+ this.#hostWindow.addEventListener("message", this.#hostListener);
729
769
  }
730
770
  get ready() {
731
771
  return this.#ready;
732
772
  }
773
+ get channelId() {
774
+ return this.#channelId;
775
+ }
733
776
  markReady() {
734
777
  this.#ready = true;
735
778
  for (const payload of this.#queue) this.#post(payload);
@@ -738,9 +781,21 @@ var IframeWorkerLike = class {
738
781
  #post(payload) {
739
782
  this.#iframe.contentWindow?.postMessage({
740
783
  [ENVELOPE]: true,
784
+ channelId: this.#channelId,
741
785
  payload
742
786
  }, "*");
743
787
  }
788
+ /** 通道鉴别:source 必须是本 iframe 的 contentWindow 且 channelId 匹配,否则丢弃 */
789
+ #onMessage(event) {
790
+ if (event.source !== this.#iframe.contentWindow) return;
791
+ const data = event.data;
792
+ if (!data || data[ENVELOPE] !== true || data.channelId !== this.#channelId) return;
793
+ if (data.payload && data.payload.type === "sandbox-ready") {
794
+ this.markReady();
795
+ return;
796
+ }
797
+ this.emit(data.payload);
798
+ }
744
799
  postMessage(data) {
745
800
  if (this.#ready) this.#post(data);
746
801
  else this.#queue.push(data);
@@ -753,15 +808,17 @@ var IframeWorkerLike = class {
753
808
  for (const listener of this.#listeners) listener({ data });
754
809
  }
755
810
  terminate() {
811
+ this.#hostWindow.removeEventListener("message", this.#hostListener);
756
812
  this.#iframe.remove();
757
813
  }
758
814
  };
759
815
  /** srcdoc 文档:内嵌 Worker 引导源码 + 双向中继(BOOTSTRAP 经 JSON 字符串注入,防 <\/script> 截断) */
760
- function sandboxDocument(bootstrapSource) {
816
+ function sandboxDocument(bootstrapSource, channelId) {
761
817
  const bootstrapJson = JSON.stringify(bootstrapSource).replace(/<\//g, "<\\/");
762
818
  return `<!doctype html>
763
819
  <html><head><meta charset="utf-8"></head><body><script>
764
820
  const ENVELOPE = ${JSON.stringify(ENVELOPE)};
821
+ const CHANNEL_ID = ${JSON.stringify(channelId)};
765
822
  const blob = new Blob([${bootstrapJson}], { type: 'text/javascript' });
766
823
  const url = URL.createObjectURL(blob);
767
824
  // 注意:opaque origin 下 module Worker 无法加载(实证),classic Worker + data: URL 动态导入可用
@@ -769,36 +826,29 @@ const worker = new Worker(url);
769
826
  URL.revokeObjectURL(url);
770
827
  window.addEventListener('message', (event) => {
771
828
  const data = event.data;
772
- if (data && data[ENVELOPE]) worker.postMessage(data.payload);
829
+ // 中继同样校验 channelId(同页脚本无法伪造实例 id)
830
+ if (data && data[ENVELOPE] && data.channelId === CHANNEL_ID) worker.postMessage(data.payload);
773
831
  });
774
832
  worker.addEventListener('message', (event) => {
775
- parent.postMessage({ [ENVELOPE]: true, payload: event.data }, '*');
833
+ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: event.data }, '*');
776
834
  });
777
835
  worker.addEventListener('error', (event) => {
778
- parent.postMessage({ [ENVELOPE]: true, payload: { type: 'sandbox-worker-error', message: event.message } }, '*');
836
+ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: { type: 'sandbox-worker-error', message: event.message } }, '*');
779
837
  });
780
- parent.postMessage({ [ENVELOPE]: true, payload: { type: 'sandbox-ready' } }, '*');
838
+ parent.postMessage({ [ENVELOPE]: true, channelId: CHANNEL_ID, payload: { type: 'sandbox-ready' } }, '*');
781
839
  <\/script></body></html>`;
782
840
  }
783
841
  /**
784
- * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 module Worker。
842
+ * 创建 opaque iframe 沙箱 Worker:sandbox="allow-scripts" srcdoc iframe 承载 classic Worker。
785
843
  * 返回 IframeWorkerLike(协议与 blob Worker 完全一致:load/execute/bridge/network-blocked)。
786
844
  */
787
845
  function createIframeWorker(bootstrapSource, doc = document) {
846
+ const channelId = nextChannelId();
788
847
  const iframe = doc.createElement("iframe");
789
848
  iframe.setAttribute("sandbox", "allow-scripts");
790
849
  iframe.style.display = "none";
791
- iframe.srcdoc = sandboxDocument(bootstrapSource);
792
- const worker = new IframeWorkerLike(iframe);
793
- (doc.defaultView ?? window).addEventListener("message", (event) => {
794
- const data = event.data;
795
- if (!data || data[ENVELOPE] !== true) return;
796
- if (data.payload && data.payload.type === "sandbox-ready") {
797
- worker.markReady();
798
- return;
799
- }
800
- worker.emit(data.payload);
801
- });
850
+ iframe.srcdoc = sandboxDocument(bootstrapSource, channelId);
851
+ const worker = new IframeWorkerLike(iframe, doc.defaultView ?? window, channelId);
802
852
  doc.body.appendChild(iframe);
803
853
  return worker;
804
854
  }
@@ -827,6 +877,8 @@ var BrowserWorkerScriptExecutor = class {
827
877
  #networkPolicy;
828
878
  #approval;
829
879
  #transpiler;
880
+ /** 消息 id 唯一递增(多实例/多执行不串) */
881
+ #messageSeq = 0;
830
882
  constructor(deps) {
831
883
  this.#fs = deps.fs;
832
884
  this.#workerFactory = deps.workerFactory ?? resolveWorkerFactory(deps.sandbox ?? "auto");
@@ -871,7 +923,7 @@ var BrowserWorkerScriptExecutor = class {
871
923
  try {
872
924
  const data = await this.#request(worker, {
873
925
  type: "load",
874
- id: "load-1",
926
+ id: `load-${++this.#messageSeq}`,
875
927
  source: script.source,
876
928
  networkPolicy: this.#networkPolicy
877
929
  });
@@ -906,7 +958,7 @@ var BrowserWorkerScriptExecutor = class {
906
958
  try {
907
959
  const response = await this.#request(worker, {
908
960
  type: "execute",
909
- id: "exec-1",
961
+ id: `exec-${++this.#messageSeq}`,
910
962
  skillName: context.skillName,
911
963
  runId: context.runId,
912
964
  source: script.source,
@@ -915,12 +967,13 @@ var BrowserWorkerScriptExecutor = class {
915
967
  }, timeoutMs, (bridgeRequest) => this.#handleBridge(bridgeRequest, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
916
968
  if (!response.ok) {
917
969
  const stderrSummary = response.stderr?.length ? ` | stderr: ${response.stderr.join(" | ").slice(0, 500)}` : "";
970
+ const normalized = normalizeToolError(response.error?.code, response.error?.message ?? "script execution failed");
918
971
  return {
919
972
  ok: false,
920
973
  content: [],
921
974
  error: {
922
- code: response.error?.code ?? "TOOL_EXECUTION_FAILED",
923
- message: `${response.error?.message ?? "script execution failed"}${stderrSummary}`
975
+ code: normalized.code,
976
+ message: `${normalized.message}${stderrSummary}`
924
977
  }
925
978
  };
926
979
  }
@@ -1,4 +1,4 @@
1
- import { C as parseSkillMarkdown, E as renderAvailableSkillsXml, c as SkillDiscovery, d as assertSafePathSegment, j as validateSkills, k as resolveInsideRoot, l as SkillReader, p as buildCatalog, u as WebSkillError } from "./dist-D0saNPi_.js";
1
+ import { C as parseSkillMarkdown, E as renderAvailableSkillsXml, c as SkillDiscovery, d as assertSafePathSegment, j as validateSkills, k as resolveInsideRoot, l as SkillReader, p as buildCatalog, u as WebSkillError } from "./dist-D7MsoMPx.js";
2
2
  import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
@@ -272,7 +272,7 @@ var AnthropicClient = class {
272
272
  "content-type": "application/json",
273
273
  "x-api-key": this.#config.apiKey,
274
274
  "anthropic-version": ANTHROPIC_VERSION,
275
- "anthropic-dangerous-direct-browser-access": "true"
275
+ ...this.#config.dangerouslyAllowDirectBrowserAccess ? { "anthropic-dangerous-direct-browser-access": "true" } : {}
276
276
  };
277
277
  }
278
278
  async complete(input) {
@@ -571,7 +571,7 @@ var GoogleGenAiClient = class {
571
571
  }
572
572
  async #post(input, stream) {
573
573
  const model = input.model ?? this.#config.model;
574
- const action = stream ? `:streamGenerateContent?alt=sse&key=${encodeURIComponent(this.#config.apiKey)}` : `:generateContent?key=${encodeURIComponent(this.#config.apiKey)}`;
574
+ const action = stream ? ":streamGenerateContent?alt=sse" : ":generateContent";
575
575
  const { systemInstruction, contents } = toGenAiContents(input.messages);
576
576
  const body = { contents };
577
577
  if (systemInstruction) body["systemInstruction"] = systemInstruction;
@@ -581,7 +581,10 @@ var GoogleGenAiClient = class {
581
581
  try {
582
582
  res = await this.#fetch(`${this.#baseUrl()}/v1beta/models/${encodeURIComponent(model)}${action}`, {
583
583
  method: "POST",
584
- headers: { "content-type": "application/json" },
584
+ headers: {
585
+ "content-type": "application/json",
586
+ "x-goog-api-key": this.#config.apiKey
587
+ },
585
588
  body: JSON.stringify(body),
586
589
  signal: input.signal ?? (this.#config.requestTimeoutMs ? AbortSignal.timeout(this.#config.requestTimeoutMs) : null)
587
590
  });
@@ -597,7 +600,7 @@ var GoogleGenAiClient = class {
597
600
  /** 轻量探测(GET /v1beta/models),集成测试据此决定 skip */
598
601
  async checkAvailability() {
599
602
  try {
600
- return (await this.#fetch(`${this.#baseUrl()}/v1beta/models?key=${encodeURIComponent(this.#config.apiKey)}`)).ok;
603
+ return (await this.#fetch(`${this.#baseUrl()}/v1beta/models`, { headers: { "x-goog-api-key": this.#config.apiKey } })).ok;
601
604
  } catch {
602
605
  return false;
603
606
  }
@@ -846,11 +849,12 @@ const ASK_USER_TOOL = {
846
849
  */
847
850
  function createScriptContext(deps) {
848
851
  const { fs, artifactStore, skillName, skillRoot, runId, confirm, onWarning, onArtifactCreated } = deps;
852
+ const readFs = fs.withRoot?.(skillRoot) ?? fs;
849
853
  return {
850
854
  skillName,
851
855
  runId,
852
856
  async readReference(relativePath) {
853
- return fs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
857
+ return readFs.readText(resolveInsideRoot(skillRoot, `references/${relativePath}`));
854
858
  },
855
859
  async writeArtifact(path, content, options) {
856
860
  const artifact = typeof content === "string" ? await artifactStore.createTextArtifact({
@@ -1402,7 +1406,7 @@ var AgentLoop = class {
1402
1406
  state.messages.push({
1403
1407
  role: "tool",
1404
1408
  toolCallId: pendingCall.id,
1405
- content: JSON.stringify(result)
1409
+ content: await this.#serializeToolResult(pendingCall, result, state)
1406
1410
  });
1407
1411
  } else if (pending.type === "ask" && pendingCall) {
1408
1412
  const value = await this.#interact(state, pending, {
@@ -1423,14 +1427,24 @@ var AgentLoop = class {
1423
1427
  state.messages.push({
1424
1428
  role: "tool",
1425
1429
  toolCallId: pendingCall.id,
1426
- content: JSON.stringify(result)
1430
+ content: await this.#serializeToolResult(pendingCall, result, state)
1427
1431
  });
1428
1432
  } else if (pendingCall) {
1429
1433
  const result = await this.#executeCall(pendingCall, state);
1430
1434
  state.messages.push({
1431
1435
  role: "tool",
1432
1436
  toolCallId: pendingCall.id,
1433
- content: JSON.stringify(result)
1437
+ content: await this.#serializeToolResult(pendingCall, result, state)
1438
+ });
1439
+ }
1440
+ for (;;) {
1441
+ const next = this.#findPendingToolCall(state.messages);
1442
+ if (!next) break;
1443
+ const result = await this.#executeCall(next, state);
1444
+ state.messages.push({
1445
+ role: "tool",
1446
+ toolCallId: next.id,
1447
+ content: await this.#serializeToolResult(next, result, state)
1434
1448
  });
1435
1449
  }
1436
1450
  } catch (e) {
@@ -1724,14 +1738,20 @@ var AgentLoop = class {
1724
1738
  const text = JSON.stringify(result);
1725
1739
  const max = this.#config.toolResultMaxBytes;
1726
1740
  if (text.length <= max) return text;
1727
- const artifact = await this.#deps.artifactStore.createTextArtifact({
1728
- runId: state.runId,
1729
- path: `tool-results/${call.id}.json`,
1730
- content: text
1731
- });
1741
+ let note;
1742
+ try {
1743
+ note = `full tool result saved as artifact "${(await this.#deps.artifactStore.createTextArtifact({
1744
+ runId: state.runId,
1745
+ path: `tool-results/${call.id}.json`,
1746
+ content: text
1747
+ })).id}"`;
1748
+ } catch (e) {
1749
+ state.trace.record("run.warning", { message: `Failed to persist oversized tool result artifact for "${call.name}": ${messageOf$1(e)}` });
1750
+ note = "full result discarded (artifact store unavailable)";
1751
+ }
1732
1752
  const head = text.slice(0, Math.floor(max * .6));
1733
1753
  const tail = text.slice(-Math.floor(max * .3));
1734
- return `${head}\n...[truncated ${text.length - head.length - tail.length} chars; full tool result saved as artifact "${artifact.id}"]...\n${tail}`;
1754
+ return `${head}\n...[truncated ${text.length - head.length - tail.length} chars; ${note}]...\n${tail}`;
1735
1755
  }
1736
1756
  /** SkillStateGuard 判定(无注入默认全放行;仅显式 false 拦截) */
1737
1757
  async #guardDenied(kind, skillName) {
@@ -2627,6 +2647,67 @@ function networkUrlHost(url) {
2627
2647
  return "(unparseable-url)";
2628
2648
  }
2629
2649
  }
2650
+ /** WebSkillErrorCode 全量白名单(错误码归一用;与 core errors.ts 保持同步) */
2651
+ const WHITELIST = /* @__PURE__ */ new Set([
2652
+ "FS_NOT_FOUND",
2653
+ "FS_PATH_OUTSIDE_ROOT",
2654
+ "SKILL_NOT_FOUND",
2655
+ "SKILL_INVALID_METADATA",
2656
+ "SKILL_INVALID_NAME",
2657
+ "SKILL_DUPLICATE_NAME",
2658
+ "SKILL_UNSUPPORTED_SCRIPT",
2659
+ "VALIDATION_FAILED",
2660
+ "TOOL_NOT_FOUND",
2661
+ "TOOL_EXECUTION_FAILED",
2662
+ "NETWORK_BLOCKED",
2663
+ "TOOL_UNSUPPORTED",
2664
+ "TOOL_SCHEMA_UNAVAILABLE",
2665
+ "RUN_TIMEOUT",
2666
+ "RUN_MAX_TURNS_EXCEEDED",
2667
+ "RUN_FAILED",
2668
+ "RUN_CANCELLED",
2669
+ "RUN_INTERACTION_TIMEOUT",
2670
+ "UI_UNAVAILABLE",
2671
+ "LLM_UNAVAILABLE",
2672
+ "LLM_REQUEST_FAILED",
2673
+ "INSTALL_FAILED",
2674
+ "UNINSTALL_FAILED",
2675
+ "EXPORT_FAILED",
2676
+ "INTEGRITY_FAILED",
2677
+ "FS_PERMISSION_DENIED",
2678
+ "MCP_ENDPOINT_UNAVAILABLE",
2679
+ "MCP_TOOL_NOT_FOUND",
2680
+ "CANDIDATE_INVALID",
2681
+ "APPROVAL_REQUIRED",
2682
+ "SKILL_QUARANTINED",
2683
+ "SKILL_DISABLED",
2684
+ "SKILL_UNKNOWN_ALLOWED_TOOL",
2685
+ "SKILL_UNKNOWN_DEPENDENCY",
2686
+ "SKILL_CIRCULAR_DEPENDENCY",
2687
+ "GOVERNANCE_FAILED",
2688
+ "RUN_SNAPSHOT_NOT_FOUND",
2689
+ "RUN_SNAPSHOT_EXPIRED",
2690
+ "RUN_SNAPSHOT_INCOMPATIBLE"
2691
+ ]);
2692
+ /**
2693
+ * 错误码白名单归一:沙箱/桥消息里出现的非白名单码(DOMException 数值码、
2694
+ * Node 任意 ERR_* 码等)一律归为 TOOL_EXECUTION_FAILED。
2695
+ */
2696
+ function normalizeErrorCode(code) {
2697
+ return typeof code === "string" && WHITELIST.has(code) ? code : "TOOL_EXECUTION_FAILED";
2698
+ }
2699
+ /** 归一化后的 (code, message):非白名单码保留在 message 尾部([original code: X]) */
2700
+ function normalizeToolError(code, message) {
2701
+ const normalized = normalizeErrorCode(code);
2702
+ if (typeof code === "string" && normalized === code) return {
2703
+ code: normalized,
2704
+ message
2705
+ };
2706
+ return {
2707
+ code: normalized,
2708
+ message: `${message} [original code: ${String(code)}]`
2709
+ };
2710
+ }
2630
2711
  /**
2631
2712
  * Per-capability 强制授权判定(宿主侧,browser/node 执行器共用单一来源)。
2632
2713
  * 'require-approval' 模式下经注入的 UiBridge 发 authorize 交互;
@@ -2651,6 +2732,7 @@ var CapabilityApproval = class CapabilityApproval {
2651
2732
  async authorize(input) {
2652
2733
  const { runId, capability, mode } = input;
2653
2734
  if (mode === false) return "disabled";
2735
+ if (mode !== true && mode !== "require-approval") return "disabled";
2654
2736
  if (mode !== "require-approval") return "allowed";
2655
2737
  if (this.#scope === "once-per-run" && this.#approved.get(runId)?.has(capability)) return "allowed";
2656
2738
  if (!this.#uiBridge) return "denied";
@@ -2678,4 +2760,4 @@ var CapabilityApproval = class CapabilityApproval {
2678
2760
  };
2679
2761
 
2680
2762
  //#endregion
2681
- export { isNetworkAllowed as A, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, resolveToolName as F, schemaToForm as I, toLlmToolSpec as L, networkUrlHost as M, normalizeToolContent as N, fromVercelResult as O, parseBridgeRequest as P, toVercelToolSpecs as R, WebSkillRuntime as S, createScriptContext as T, READ_SKILL_FILE_TOOL as _, AnthropicClient as a, SerializingMemoryStore as b, FsArtifactStore as c, FullDisclosureRouter as d, GoogleGenAiClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, mergeCatalogEntries as j, fromVercelStreamPart as k, FsMemoryStore as l, OpenAiCompatibleClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, HookRunner as p, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_TOOL_NAME as v, buildRenderResult as w, TraceRecorder as x, RUN_SNAPSHOT_SCHEMA_VERSION as y };
2763
+ export { isNetworkAllowed as A, toVercelToolSpecs as B, bridgeError as C, extractChartSpec as D, createWebSkillApi as E, normalizeToolError as F, parseBridgeRequest as I, resolveToolName as L, networkUrlHost as M, normalizeErrorCode as N, fromVercelResult as O, normalizeToolContent as P, schemaToForm as R, WebSkillRuntime as S, createScriptContext as T, READ_SKILL_FILE_TOOL as _, AnthropicClient as a, SerializingMemoryStore as b, FsArtifactStore as c, FullDisclosureRouter as d, GoogleGenAiClient as f, READ_SKILL_FILE_INPUT_SCHEMA as g, ProgressiveRouter as h, AgentLoop as i, mergeCatalogEntries as j, fromVercelStreamPart as k, FsMemoryStore as l, OpenAiCompatibleClient as m, ASK_USER_TOOL as n, CapabilityApproval as o, HookRunner as p, ASK_USER_TOOL_NAME as r, EventBus as s, ASK_USER_INPUT_SCHEMA as t, FsRunSnapshotStore as u, READ_SKILL_FILE_TOOL_NAME as v, buildRenderResult as w, TraceRecorder as x, RUN_SNAPSHOT_SCHEMA_VERSION as y, toLlmToolSpec as z };
@@ -1,4 +1,4 @@
1
- import { u as WebSkillError } from "./dist-D0saNPi_.js";
1
+ import { u as WebSkillError } from "./dist-D7MsoMPx.js";
2
2
 
3
3
  //#region ../ui/dist/index.js
4
4
  /** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
@@ -1,5 +1,5 @@
1
- import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, O as resolveArchiveLimits, T as readResponseWithLimit, b as isValidSkillName, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D0saNPi_.js";
2
- import { A as isNetworkAllowed, C as bridgeError, M as networkUrlHost, N as normalizeToolContent, P as parseBridgeRequest, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-DrySSQ5R.js";
1
+ import { A as unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, O as resolveArchiveLimits, T as readResponseWithLimit, b as isValidSkillName, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D7MsoMPx.js";
2
+ import { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-CV64gN62.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { unzipSync, zipSync } from "fflate";
5
5
  import { existsSync, promises, realpathSync } from "node:fs";
@@ -37,13 +37,16 @@ async function realpathNearest(platformPath) {
37
37
  * 可选 root 模式:构造传入 root 后,read/write 操作先做 realpath 包含校验
38
38
  * (root 与目标都 realpath 后前缀比对),经符号链接逃逸 root → FS_PATH_OUTSIDE_ROOT。
39
39
  */
40
- var NodeFS = class {
40
+ var NodeFS = class NodeFS {
41
41
  kind = "node";
42
42
  #root;
43
43
  constructor(deps = {}) {
44
44
  this.#root = deps.root;
45
45
  }
46
- /** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
46
+ /** root 模式:全部方法(read/write/exists/stat/list/mkdir/remove/rename)目标 realpath 必须落在 root realpath 前缀内 */
47
+ withRoot(root) {
48
+ return new NodeFS({ root });
49
+ }
47
50
  async #assertContained(p) {
48
51
  if (!this.#root) return;
49
52
  const rootReal = await promises.realpath(toPlatform$4(this.#root));
@@ -64,6 +67,12 @@ var NodeFS = class {
64
67
  await promises.mkdir(path.dirname(target), { recursive: true });
65
68
  await promises.writeFile(target, content, "utf8");
66
69
  }
70
+ async appendText(p, content) {
71
+ await this.#assertContained(p);
72
+ const target = toPlatform$4(p);
73
+ await promises.mkdir(path.dirname(target), { recursive: true });
74
+ await promises.appendFile(target, content, "utf8");
75
+ }
67
76
  async readBinary(p) {
68
77
  await this.#assertContained(p);
69
78
  try {
@@ -79,6 +88,7 @@ var NodeFS = class {
79
88
  await promises.writeFile(target, content);
80
89
  }
81
90
  async exists(p) {
91
+ await this.#assertContained(p);
82
92
  try {
83
93
  await promises.access(toPlatform$4(p));
84
94
  return true;
@@ -87,6 +97,7 @@ var NodeFS = class {
87
97
  }
88
98
  }
89
99
  async stat(p) {
100
+ await this.#assertContained(p);
90
101
  try {
91
102
  const s = await promises.stat(toPlatform$4(p));
92
103
  return {
@@ -100,6 +111,7 @@ var NodeFS = class {
100
111
  }
101
112
  }
102
113
  async list(p) {
114
+ await this.#assertContained(p);
103
115
  let dirents;
104
116
  try {
105
117
  dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
@@ -112,9 +124,11 @@ var NodeFS = class {
112
124
  }));
113
125
  }
114
126
  async mkdir(p) {
127
+ await this.#assertContained(p);
115
128
  await promises.mkdir(toPlatform$4(p), { recursive: true });
116
129
  }
117
130
  async remove(p, options) {
131
+ await this.#assertContained(p);
118
132
  try {
119
133
  await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
120
134
  } catch (e) {
@@ -122,6 +136,8 @@ var NodeFS = class {
122
136
  }
123
137
  }
124
138
  async rename(from, to) {
139
+ await this.#assertContained(from);
140
+ await this.#assertContained(to);
125
141
  await promises.mkdir(path.dirname(toPlatform$4(to)), { recursive: true });
126
142
  try {
127
143
  await promises.rename(toPlatform$4(from), toPlatform$4(to));
@@ -303,8 +319,10 @@ function workerEntryPath() {
303
319
  * 能力桥协议与浏览器同一来源(runtime/sandbox/bridgeProtocol)。
304
320
  *
305
321
  * 诚实标注:本执行器做的是**能力面收敛**(网络策略、模块 allowlist、资源限额、
306
- * 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程,仍有绕过手段;
307
- * 禁止假定其可隔离不可信脚本。
322
+ * 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程;
323
+ * 已知主动逃逸面(process.binding/_linkedBinding/dlopen/openStdin/reallyExit/abort)
324
+ * 已在入口删除(0.2.3),常规 import 拦截之外不承诺防御其它宿主共享面;
325
+ * 禁止假定其可隔离不可信脚本(隔离级需求用 ProcessSandboxExecutor)。
308
326
  */
309
327
  var SandboxedScriptExecutor = class {
310
328
  #fs;
@@ -379,12 +397,13 @@ var SandboxedScriptExecutor = class {
379
397
  }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
380
398
  if (!result.ok) {
381
399
  const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
400
+ const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
382
401
  return {
383
402
  ok: false,
384
403
  content: [],
385
404
  error: {
386
- code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
387
- message: `${result.error?.message ?? "script execution failed"}${stderrSummary}`
405
+ code: normalized.code,
406
+ message: `${normalized.message}${stderrSummary}`
388
407
  }
389
408
  };
390
409
  }
@@ -461,7 +480,7 @@ var SandboxedScriptExecutor = class {
461
480
  });
462
481
  worker.on("error", (e) => done(() => reject(e)));
463
482
  worker.on("exit", (code) => {
464
- if (code !== 0) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker exited with code ${code}`)));
483
+ if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
465
484
  });
466
485
  });
467
486
  }
@@ -644,8 +663,11 @@ var ProcessSandboxExecutor = class {
644
663
  ...readAllow(artifactDir),
645
664
  ...writeAllow(artifactDir)
646
665
  ],
647
- silent: false
666
+ env: Object.fromEntries((this.#options.envWhitelist ?? []).filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]])),
667
+ silent: true
648
668
  });
669
+ child.stdout?.resume();
670
+ child.stderr?.resume();
649
671
  child.unref();
650
672
  child.channel?.unref();
651
673
  return {
@@ -708,12 +730,13 @@ var ProcessSandboxExecutor = class {
708
730
  }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
709
731
  if (!result.ok) {
710
732
  const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
733
+ const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
711
734
  return {
712
735
  ok: false,
713
736
  content: [],
714
737
  error: {
715
- code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
716
- message: `${result.error?.message ?? "script execution failed"}${stderrSummary}`
738
+ code: normalized.code,
739
+ message: `${normalized.message}${stderrSummary}`
717
740
  }
718
741
  };
719
742
  }