@capybara-hq/plugin-sdk 0.1.0-rc.1 → 0.1.0-rc.3

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/agent-docs/agent-templates.md +47 -0
  3. package/agent-docs/concepts.md +58 -0
  4. package/agent-docs/connector-templates.md +47 -0
  5. package/agent-docs/extension-points/channel.md +15 -0
  6. package/agent-docs/extension-points/config-source.md +15 -0
  7. package/agent-docs/extension-points/connector.md +15 -0
  8. package/agent-docs/extension-points/counter.md +15 -0
  9. package/agent-docs/extension-points/directory.md +15 -0
  10. package/agent-docs/extension-points/file-preview.md +15 -0
  11. package/agent-docs/extension-points/identity-source.md +15 -0
  12. package/agent-docs/extension-points/model-provider.md +15 -0
  13. package/agent-docs/extension-points/object-store.md +15 -0
  14. package/agent-docs/extension-points/sandbox.md +15 -0
  15. package/agent-docs/extension-points/secret-store.md +15 -0
  16. package/agent-docs/extension-points/web-fetch.md +15 -0
  17. package/agent-docs/extension-points/web-search.md +15 -0
  18. package/agent-docs/extensions.md +63 -0
  19. package/agent-docs/plugin-format.md +60 -0
  20. package/agent-docs/skills.md +44 -0
  21. package/agent-docs/tool-naming.md +21 -0
  22. package/dist/agent-template.d.ts +85 -0
  23. package/dist/agent-template.js +361 -2
  24. package/dist/config-ui.d.ts +131 -0
  25. package/dist/config-ui.js +602 -0
  26. package/dist/ctx.d.ts +3 -1
  27. package/dist/directory.d.ts +11 -2
  28. package/dist/extension-point.d.ts +9 -0
  29. package/dist/extension-point.js +17 -12
  30. package/dist/file-preview.d.ts +8 -0
  31. package/dist/hooks.d.ts +14 -4
  32. package/dist/index.d.ts +4 -1
  33. package/dist/index.js +4 -1
  34. package/dist/marketplace.d.ts +29 -2
  35. package/dist/marketplace.js +30 -2
  36. package/dist/org-access.d.ts +27 -0
  37. package/dist/org-access.js +35 -0
  38. package/dist/plugin-manifest.d.ts +25 -2
  39. package/dist/plugin-manifest.js +20 -1
  40. package/dist/sandbox.d.ts +140 -60
  41. package/dist/sandbox.js +30 -6
  42. package/dist/secret.d.ts +4 -1
  43. package/dist/secret.js +5 -2
  44. package/dist/skill-invocation.d.ts +8 -5
  45. package/dist/skill-invocation.js +17 -8
  46. package/dist/skill.d.ts +20 -3
  47. package/dist/skill.js +28 -6
  48. package/dist/tool.d.ts +8 -0
  49. package/dist/ui.d.ts +8 -0
  50. package/dist/web.d.ts +105 -0
  51. package/dist/web.js +135 -0
  52. package/package.json +4 -2
  53. package/schemas/marketplace.v1.schema.json +36 -0
  54. package/schemas/plugin.v1.schema.json +49 -4
package/dist/sandbox.d.ts CHANGED
@@ -1,30 +1,44 @@
1
1
  /**
2
- * Sandbox contract `sandbox@1`: a single-select, brokered extension point (extensions may not
2
+ * Sandbox contract `sandbox@2`: a single-select, brokered extension point (extensions may not
3
3
  * `requires` it; the kernel's `SandboxService` consumes it). The provider implements only the
4
4
  * primitives below; the kernel builds `glob` / `grep` / `edit` from `exec` plus `readFile` /
5
5
  * `writeFile`.
6
+ *
7
+ * `@2` separates an environment's **file system** from its **instance**: the file system
8
+ * (`createFileSystem`) lives until the environment is deleted (`destroyFileSystem`); instances
9
+ * (`create`) attach to it and come and go (`destroy` drops the instance and keeps the files).
6
10
  */
7
11
  /** Sandbox extension point ref. */
8
- export declare const SANDBOX_EXTENSION_POINT = "sandbox@1";
9
- /** 沙箱内允许模型访问的根目录。 */
12
+ export declare const SANDBOX_EXTENSION_POINT = "sandbox@2";
13
+ /** The roots inside the sandbox the model may access. */
10
14
  export declare const SANDBOX_ROOTS: readonly ["/workspace", "/skills", "/tmp"];
11
- /** 沙箱资源与出站限制。 */
15
+ /** Sandbox resource and egress limits. */
12
16
  export interface SandboxLimits {
13
17
  cpu: number;
14
18
  memoryMb: number;
15
19
  diskMb: number;
16
20
  egress: "none" | "allowlist" | "open";
17
- /** 出站白名单(`egress` 为 `allowlist` 时生效);每项须通过 {@link validateAllowHost}。 */
21
+ /** Egress allowlist (used when `egress` is `allowlist`); each entry passes {@link validateAllowHost}. */
18
22
  allowHosts: string[];
19
23
  }
20
- /** 提供方的实例句柄。 */
24
+ /** The provider's instance handle. */
21
25
  export interface SandboxHandle {
22
- /** 提供方的实例 id。 */
26
+ /** The provider's instance id. */
27
+ id: string;
28
+ }
29
+ /** The provider's file system handle (an environment's files, independent of any instance). */
30
+ export interface FileSystemHandle {
31
+ /** The provider's file system id. */
23
32
  id: string;
24
33
  }
25
- /** 命令执行结果。 */
34
+ /** Who runs an `exec` (informational in phase 1: audit and attribution). */
35
+ export interface SandboxActor {
36
+ accountId: string;
37
+ conversationId: string;
38
+ }
39
+ /** Result of a command. */
26
40
  export interface ExecResult {
27
- /** 超时或在执行中被中止为 null(此时 `timedOut` 分别为 true / false)。 */
41
+ /** null on timeout or when aborted while running (`timedOut` is then true / false). */
28
42
  exitCode: number | null;
29
43
  stdout: string;
30
44
  stderr: string;
@@ -32,8 +46,10 @@ export interface ExecResult {
32
46
  timedOut: boolean;
33
47
  }
34
48
  /**
35
- * 实例在提供方那边确定已经不在了(容器被删、E2B 沙箱过期)。`exec` / `readFile` / `writeFile` / `stat`
36
- * 发现实例不在时必须抛它(而不是普通 Error),内核据此重建沙箱并重试一次;只是暂时连不上时抛普通错误。
49
+ * The instance is definitely gone on the provider's side (container removed, E2B sandbox expired).
50
+ * `exec` / `readFile` / `writeFile` / `stat` must throw it (not a plain Error) when they find the
51
+ * instance gone; the kernel then rebuilds the instance and retries once. A transient connection
52
+ * failure throws a plain error.
37
53
  */
38
54
  export declare class SandboxGoneError extends Error {
39
55
  readonly sandboxGone: true;
@@ -41,8 +57,21 @@ export declare class SandboxGoneError extends Error {
41
57
  cause?: unknown;
42
58
  });
43
59
  }
44
- /** 鸭子类型判断(不用 instanceof),兼容插件自带另一份 plugin-sdk 副本的情况。 */
60
+ /** Duck-typed check (not instanceof), so a plugin bundling another plugin-sdk copy still matches. */
45
61
  export declare function isSandboxGoneError(e: unknown): e is SandboxGoneError;
62
+ /**
63
+ * The file system is definitely gone on the provider's side (volume removed, E2B sandbox killed).
64
+ * `create` throws it instead of attaching the instance to a new, empty file system; the kernel then
65
+ * reports the environment's files as lost rather than handing out a silently empty workspace.
66
+ */
67
+ export declare class FileSystemGoneError extends Error {
68
+ readonly fileSystemGone: true;
69
+ constructor(message: string, opts?: {
70
+ cause?: unknown;
71
+ });
72
+ }
73
+ /** Duck-typed check (not instanceof), so a plugin bundling another plugin-sdk copy still matches. */
74
+ export declare function isFileSystemGoneError(e: unknown): e is FileSystemGoneError;
46
75
  /**
47
76
  * A holder tag (`exec` argument `tag`, {@link SandboxProvider.killTagged}): the kernel passes its
48
77
  * lease tokens (UUIDs). Providers check tags against it before putting them in a command line or a
@@ -50,73 +79,119 @@ export declare function isSandboxGoneError(e: unknown): e is SandboxGoneError;
50
79
  */
51
80
  export declare const SANDBOX_TAG_RE: RegExp;
52
81
  /**
53
- * `sandbox@1` 的实现(由沙箱插件提供,如 Docker、E2B)。
54
- * 实例确定不在时:`resume` 返回 `gone`;`exec` / `readFile` / `writeFile` / `stat` 抛 {@link SandboxGoneError};
55
- * `suspend` / `destroy` 照常成功。
82
+ * The `sandbox@2` implementation (provided by a sandbox plugin such as Docker or E2B).
83
+ *
84
+ * **File systems and instances.** `createFileSystem` makes an environment's files; they live until
85
+ * `destroyFileSystem` (the environment is deleted). `create` makes an instance attached to an
86
+ * existing file system; `destroy` / `destroyByName` drop the instance only and keep the files, and
87
+ * `suspend` keeps them too. When the file system is gone, `create` must throw
88
+ * {@link FileSystemGoneError} and never create an empty one in its place (a Docker provider checks
89
+ * its volumes exist before mounting them, because Docker would otherwise create empty volumes).
90
+ *
91
+ * **Gone instances.** When the instance is definitely gone: `resume` returns `gone`; `exec` /
92
+ * `readFile` / `writeFile` / `stat` / `killAll` / `killTagged` throw {@link SandboxGoneError};
93
+ * `suspend` / `destroy` succeed as usual.
56
94
  *
57
- * **自动启动**:实例还在、但已停止 / 暂停(被内核之外的原因停掉:宿主机重启、守护进程重启、沙箱里的进程把
58
- * 自己的 init 弄停了、休眠扫描停了实例却没来得及记下 `suspended`……)时,`exec` / `readFile` / `writeFile` /
59
- * `stat` 必须先透明地把它启动起来(同 `resume`,文件保留)再执行,而不是报错——内核只信自己记的 `running`。
95
+ * **Auto-start**: when the instance still exists but is stopped or paused (for a reason outside the
96
+ * kernel: host or daemon restart, a process in the sandbox stopping its init, an idle sweep that
97
+ * stopped the instance before recording `suspended`...), `exec` / `readFile` / `writeFile` /
98
+ * `stat` / `killAll` / `killTagged` must start it transparently first (like `resume`, files kept)
99
+ * and then run, never fail: the kernel trusts only the `running` it recorded itself. Auto-start
100
+ * must be idempotent under concurrent calls: two calls that both find the instance stopped both
101
+ * start it without failing.
60
102
  *
61
- * **命令时限在沙箱里也要生效**:`exec` 除了在提供方这边按 `timeoutMs` 停掉命令,还必须让沙箱里的命令自己在
62
- * `timeoutMs` 之后不久(几秒的余量)被杀掉。内核副本崩溃时提供方这边的计时随之消失,断开连接也不会结束命令;
63
- * 沙箱里的时限保证它不会无限期地与下一个持锁方的命令同时运行。
103
+ * **Time limits apply inside the sandbox too**: besides stopping a command after `timeoutMs` on the
104
+ * provider's side, `exec` must make the command in the sandbox kill itself shortly (a few seconds)
105
+ * after `timeoutMs`. When a kernel replica crashes, the provider-side timer goes with it, and a
106
+ * dropped connection does not end the command; the in-sandbox limit keeps it from running
107
+ * indefinitely next to the next holder's commands.
64
108
  *
65
- * **中止**:每个方法都必须及时响应 `signal`(已中止就立即失败,进行中被中止就尽快以中止原因结束)——内核在锁丢失、
66
- * 运行被中止或超时时靠它停下,不响应就可能与新的持锁方同时操作同一实例。`exec` 被中止时必须杀掉正在跑的命令
67
- * (连同它派生的进程),而不只是不再等待它的输出;命令启动后才被中止的 `exec` 以 `exitCode: null`、
68
- * `timedOut: false` 正常 resolve(带上已收集的输出),不 reject。调用时 signal 已中止则立即 reject。
109
+ * **Abort**: every method must respond to `signal` promptly (fail at once when already aborted, end
110
+ * with the abort reason as soon as possible when aborted mid-way): the kernel relies on it to stop
111
+ * on lock loss, run abort or timeout, and ignoring it may let two holders operate on one instance.
112
+ * An aborted `exec` must kill its running command (and the processes it spawned), not just stop
113
+ * waiting for its output; an `exec` aborted after its command started resolves normally with
114
+ * `exitCode: null`, `timedOut: false` (and the output collected so far), it does not reject. An
115
+ * `exec` called with an already aborted signal rejects at once.
69
116
  *
70
- * **Concurrent use (optional)**: a provider that declares {@link SandboxProvider.concurrentExec}
71
- * lets the kernel call `exec`, `readFile`, `writeFile` and `stat` concurrently on one instance
72
- * (commands of parallel sub-agents sharing their parent conversation's sandbox). Then those four
73
- * methods must be safe to call concurrently, and **auto-start must be idempotent under concurrent
74
- * calls**: two calls that both find the instance stopped both start it without failing. Each
75
- * `exec` labels its command with its `tag`, and an aborted `exec` kills only its own command.
76
- * `concurrentExec` requires {@link SandboxProvider.killTagged}; a provider that declares one
77
- * without the other, or neither, is used one call at a time (the behavior without them).
117
+ * **Concurrent use** (required in `@2`): the sessions of one environment share its instance, so the
118
+ * kernel calls `exec`, `readFile`, `writeFile` and `stat` concurrently on one instance. Those four
119
+ * methods must be safe to call concurrently. Each `exec` labels its command with its `tag`, and an
120
+ * aborted `exec` kills only its own command.
78
121
  */
79
122
  export interface SandboxProvider {
123
+ /** `exec` / `readFile` / `writeFile` / `stat` may run concurrently on one instance (required). */
124
+ readonly concurrentExec: true;
80
125
  /**
81
- * Declares that `exec` / `readFile` / `writeFile` / `stat` may run concurrently on one instance
82
- * (see the contract above). Requires {@link SandboxProvider.killTagged}.
126
+ * Whether the provider isolates the sessions of one environment from each other inside the
127
+ * instance (a declared capability; absent means false).
128
+ */
129
+ readonly sessionIsolation?: boolean;
130
+ /**
131
+ * Creates an environment's file system. `name` looks like `cap-env-<environmentId>`. If a file
132
+ * system with this name already exists, return it.
133
+ */
134
+ createFileSystem(args: {
135
+ name: string;
136
+ limits: SandboxLimits;
137
+ signal: AbortSignal;
138
+ }): Promise<FileSystemHandle>;
139
+ /** Deletes the file system and its files. Succeeds when it does not exist. */
140
+ destroyFileSystem(fs: FileSystemHandle, signal: AbortSignal): Promise<void>;
141
+ /**
142
+ * Deletes the file system made under `name` by `createFileSystem`; succeeds when there is none.
143
+ * The kernel uses it when it has no handle (`createFileSystem` finished on the provider's side
144
+ * but the kernel stopped before recording the handle), and in its one-off cleanup of
145
+ * per-conversation sandboxes with `cap-<conversationId>` names that a `sandbox@1` provider's
146
+ * `create` made together with their files (Docker: the `cap-<conversationId>-workspace` and
147
+ * `-skills` volumes); a provider removes those under the same call.
148
+ */
149
+ destroyFileSystemByName(name: string, signal: AbortSignal): Promise<void>;
150
+ /**
151
+ * Creates an instance attached to `fileSystem`. Without `image` the provider uses its own
152
+ * published image. Throws {@link FileSystemGoneError} when the file system is gone.
83
153
  */
84
- readonly concurrentExec?: boolean;
85
- /** `name` 形如 `cap-<conversationId>`。 */
86
154
  create(args: {
87
155
  name: string;
88
- image: string;
156
+ image?: string;
89
157
  limits: SandboxLimits;
158
+ fileSystem: FileSystemHandle;
90
159
  signal: AbortSignal;
91
160
  }): Promise<SandboxHandle>;
92
- /** `gone` 表示提供方那边已经没有这个实例。 */
161
+ /** `gone` means the provider no longer has this instance. */
93
162
  resume(h: SandboxHandle, signal: AbortSignal): Promise<"resumed" | "gone">;
94
- /** 文件必须保留。实例不存在也算成功。 */
163
+ /** Files must be kept. Succeeds when the instance does not exist. */
95
164
  suspend(h: SandboxHandle, signal: AbortSignal): Promise<void>;
96
- /** 不存在也算成功。 */
165
+ /** Drops the instance only; the file system and its files are kept. Succeeds when it does not exist. */
97
166
  destroy(h: SandboxHandle, signal: AbortSignal): Promise<void>;
98
167
  /**
99
- * 可选:按 `create` 时的 `name`(`cap-<conversationId>`)销毁实例,不存在也算成功。
100
- * 内核在拿不到句柄时用它清理(例如 `create` 已在提供方那边完成、内核却在记下句柄前退出)。
168
+ * Drops the instance made under `name` by `create` (instance only, files kept); succeeds when
169
+ * there is none. The kernel uses it when it has no handle (`create` finished on the provider's
170
+ * side but the kernel stopped before recording the handle), and in its one-off cleanup of
171
+ * per-conversation sandboxes with `cap-<conversationId>` names made by a `sandbox@1` provider's
172
+ * `create`.
101
173
  */
102
- destroyByName?(name: string, signal: AbortSignal): Promise<void>;
174
+ destroyByName(name: string, signal: AbortSignal): Promise<void>;
103
175
  /**
104
- * 可选:杀掉这个实例里经 `exec` 启动、仍在运行的全部命令(连同它们派生的、没有自己另起会话的进程)。
105
- * 内核在接手一个前任持锁方没有正常结束的沙箱(前任崩溃)时、第一次执行前调用,清掉前任遗留的命令;
106
- * 命令自己放到后台、另起会话的进程不保证被杀。实例已停止时同样先启动;不在时抛 {@link SandboxGoneError}。
176
+ * Kills every running command started through `exec` in this instance (and the processes they
177
+ * spawned that did not start their own session). The kernel calls it only when no other holder
178
+ * can be running commands (an instance just resumed from `suspended`, or under the exclusive
179
+ * lease), never while another session may be; to clean up after one crashed holder it uses
180
+ * {@link SandboxProvider.killTagged}. Processes a command put in the background in their own
181
+ * session are not guaranteed to be killed.
107
182
  */
108
- killAll?(h: SandboxHandle, signal: AbortSignal): Promise<void>;
183
+ killAll(h: SandboxHandle, signal: AbortSignal): Promise<void>;
109
184
  /**
110
- * Optional: kill every running `exec` command labelled with one of `tags` (and its process
111
- * tree), leaving other commands running. The kernel calls it for the tags of holders that
112
- * crashed while live holders keep running commands. Tags match {@link SANDBOX_TAG_RE}. Match
113
- * whole tags: one tag may be a prefix of another (`ab` of `ab-cd`), and killing `ab` must not
114
- * touch `ab-cd`'s commands. Starts a stopped instance first; throws {@link SandboxGoneError}
115
- * when the instance is gone.
185
+ * Kills every running `exec` command labelled with one of `tags` (and its process tree), leaving
186
+ * other commands running. The kernel calls it for the tags of holders that crashed while live
187
+ * holders keep running commands. Tags match {@link SANDBOX_TAG_RE}. Match whole tags: one tag
188
+ * may be a prefix of another (`ab` of `ab-cd`), and killing `ab` must not touch `ab-cd`'s
189
+ * commands.
116
190
  */
117
- killTagged?(h: SandboxHandle, tags: string[], signal: AbortSignal): Promise<void>;
191
+ killTagged(h: SandboxHandle, tags: string[], signal: AbortSignal): Promise<void>;
118
192
  /**
119
- * 执行一条命令。`maxOutputBytes` 是 stdout 与 stderr **合计**最多收集的字节,超出的丢弃、`truncated` 为 true。
193
+ * Runs a command. `maxOutputBytes` caps stdout and stderr **combined**; the rest is dropped and
194
+ * `truncated` is true.
120
195
  */
121
196
  exec(h: SandboxHandle, args: {
122
197
  command: string;
@@ -127,11 +202,16 @@ export interface SandboxProvider {
127
202
  signal: AbortSignal;
128
203
  /**
129
204
  * The holder's tag ({@link SANDBOX_TAG_RE}); label the command with it so
130
- * {@link SandboxProvider.killTagged} can find it. Absent from kernels before phase C.
205
+ * {@link SandboxProvider.killTagged} can find it.
131
206
  */
132
- tag?: string;
207
+ tag: string;
208
+ /** Who runs the command; informational in phase 1 (record it, do not act on it). */
209
+ actor: SandboxActor;
133
210
  }): Promise<ExecResult>;
134
- /** 读文件(最多 `maxBytes` 字节,更长的截断、`truncated` 为 true)。不存在、或是目录时返回 null。 */
211
+ /**
212
+ * Reads a file (at most `maxBytes`; longer files are truncated and `truncated` is true). Returns
213
+ * null when it does not exist or is a directory.
214
+ */
135
215
  readFile(h: SandboxHandle, path: string, opts: {
136
216
  maxBytes: number;
137
217
  signal: AbortSignal;
@@ -139,7 +219,7 @@ export interface SandboxProvider {
139
219
  data: Uint8Array;
140
220
  truncated: boolean;
141
221
  } | null>;
142
- /** 父目录自动创建。 */
222
+ /** Parent directories are created. */
143
223
  writeFile(h: SandboxHandle, path: string, data: Uint8Array, signal: AbortSignal): Promise<void>;
144
224
  stat(h: SandboxHandle, path: string, signal: AbortSignal): Promise<{
145
225
  size: number;
package/dist/sandbox.js CHANGED
@@ -1,17 +1,23 @@
1
1
  /**
2
- * Sandbox contract `sandbox@1`: a single-select, brokered extension point (extensions may not
2
+ * Sandbox contract `sandbox@2`: a single-select, brokered extension point (extensions may not
3
3
  * `requires` it; the kernel's `SandboxService` consumes it). The provider implements only the
4
4
  * primitives below; the kernel builds `glob` / `grep` / `edit` from `exec` plus `readFile` /
5
5
  * `writeFile`.
6
+ *
7
+ * `@2` separates an environment's **file system** from its **instance**: the file system
8
+ * (`createFileSystem`) lives until the environment is deleted (`destroyFileSystem`); instances
9
+ * (`create`) attach to it and come and go (`destroy` drops the instance and keeps the files).
6
10
  */
7
11
  /** Sandbox extension point ref. */
8
- export const SANDBOX_EXTENSION_POINT = "sandbox@1";
9
- /** 沙箱内允许模型访问的根目录。 */
12
+ export const SANDBOX_EXTENSION_POINT = "sandbox@2";
13
+ /** The roots inside the sandbox the model may access. */
10
14
  export const SANDBOX_ROOTS = ["/workspace", "/skills", "/tmp"];
11
15
  const MAX_PATH_LENGTH = 4096;
12
16
  /**
13
- * 实例在提供方那边确定已经不在了(容器被删、E2B 沙箱过期)。`exec` / `readFile` / `writeFile` / `stat`
14
- * 发现实例不在时必须抛它(而不是普通 Error),内核据此重建沙箱并重试一次;只是暂时连不上时抛普通错误。
17
+ * The instance is definitely gone on the provider's side (container removed, E2B sandbox expired).
18
+ * `exec` / `readFile` / `writeFile` / `stat` must throw it (not a plain Error) when they find the
19
+ * instance gone; the kernel then rebuilds the instance and retries once. A transient connection
20
+ * failure throws a plain error.
15
21
  */
16
22
  export class SandboxGoneError extends Error {
17
23
  sandboxGone = true;
@@ -20,12 +26,30 @@ export class SandboxGoneError extends Error {
20
26
  this.name = "SandboxGoneError";
21
27
  }
22
28
  }
23
- /** 鸭子类型判断(不用 instanceof),兼容插件自带另一份 plugin-sdk 副本的情况。 */
29
+ /** Duck-typed check (not instanceof), so a plugin bundling another plugin-sdk copy still matches. */
24
30
  export function isSandboxGoneError(e) {
25
31
  return (e instanceof Error &&
26
32
  e.name === "SandboxGoneError" &&
27
33
  e.sandboxGone === true);
28
34
  }
35
+ /**
36
+ * The file system is definitely gone on the provider's side (volume removed, E2B sandbox killed).
37
+ * `create` throws it instead of attaching the instance to a new, empty file system; the kernel then
38
+ * reports the environment's files as lost rather than handing out a silently empty workspace.
39
+ */
40
+ export class FileSystemGoneError extends Error {
41
+ fileSystemGone = true;
42
+ constructor(message, opts) {
43
+ super(message, opts && "cause" in opts ? { cause: opts.cause } : undefined);
44
+ this.name = "FileSystemGoneError";
45
+ }
46
+ }
47
+ /** Duck-typed check (not instanceof), so a plugin bundling another plugin-sdk copy still matches. */
48
+ export function isFileSystemGoneError(e) {
49
+ return (e instanceof Error &&
50
+ e.name === "FileSystemGoneError" &&
51
+ e.fileSystemGone === true);
52
+ }
29
53
  /**
30
54
  * A holder tag (`exec` argument `tag`, {@link SandboxProvider.killTagged}): the kernel passes its
31
55
  * lease tokens (UUIDs). Providers check tags against it before putting them in a command line or a
package/dist/secret.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { z } from "zod";
2
- /** 标记一个 schema 为敏感字段(Zod 4 meta `{ secret: true }`)。 */
2
+ /**
3
+ * Marks a schema as a secret field (Zod 4 meta `{ secret: true }`), merging with existing meta
4
+ * (`ui` hints, `hot`), so `secret(ui(x, …))` and `ui(secret(x), …)` are the same.
5
+ */
3
6
  export declare function secret<T extends z.ZodTypeAny>(schema: T): T;
4
7
  /** 读取 schema 的 meta,判断是否敏感;内核 `meta({secret:true})` 同样可识别。 */
5
8
  export declare function isSecret(schema: z.ZodTypeAny): boolean;
package/dist/secret.js CHANGED
@@ -1,6 +1,9 @@
1
- /** 标记一个 schema 为敏感字段(Zod 4 meta `{ secret: true }`)。 */
1
+ /**
2
+ * Marks a schema as a secret field (Zod 4 meta `{ secret: true }`), merging with existing meta
3
+ * (`ui` hints, `hot`), so `secret(ui(x, …))` and `ui(secret(x), …)` are the same.
4
+ */
2
5
  export function secret(schema) {
3
- return schema.meta({ secret: true });
6
+ return schema.meta({ ...(schema.meta() ?? {}), secret: true });
4
7
  }
5
8
  /** 读取 schema 的 meta,判断是否敏感;内核 `meta({secret:true})` 同样可识别。 */
6
9
  export function isSecret(schema) {
@@ -26,13 +26,14 @@ export declare function skillInvocation(frontmatter: Readonly<Record<string, unk
26
26
  export declare function parseSkillArguments(args: string): string[];
27
27
  /**
28
28
  * A skill body with its arguments substituted (see the file header), without the wrapper.
29
- * `skillName` is the resolved skill's bare name: `${CLAUDE_SKILL_DIR}` becomes
30
- * `/skills/<skillName>` (a qualified `<plugin>:<name>` throws a `TypeError`). `args` is trimmed;
29
+ * `skillName` is the resolved skill's bare name and `digest` its version's `skillDigest`:
30
+ * `${CLAUDE_SKILL_DIR}` becomes `/skills/<skillName>@<digest>` (a qualified `<plugin>:<name>` or a
31
+ * digest that is not 12 lowercase hex digits throws a `TypeError`). `args` is trimmed;
31
32
  * more than {@link SKILL_INVOCATION_LIMITS.argumentsMaxChars} characters throw a `RangeError`, and
32
33
  * so does a result longer than {@link SKILL_INVOCATION_LIMITS.renderedMaxChars} (checked while
33
34
  * substituting, before the long string is built).
34
35
  */
35
- export declare function renderSkillBody(body: string, invocation: SkillInvocation, args: string, skillName: string): string;
36
+ export declare function renderSkillBody(body: string, invocation: SkillInvocation, args: string, skillName: string, digest: string): string;
36
37
  /** Input of {@link renderSkillInvocation}. */
37
38
  export interface RenderSkillInvocationInput {
38
39
  /**
@@ -41,10 +42,12 @@ export interface RenderSkillInvocationInput {
41
42
  */
42
43
  name: string;
43
44
  /**
44
- * The resolved skill's bare name (never `<plugin>:<name>`): its sandbox directory
45
- * `/skills/<skillName>/`, used for `${CLAUDE_SKILL_DIR}` and the files line.
45
+ * The resolved skill's bare name (never `<plugin>:<name>`): with `digest`, its sandbox
46
+ * directory `/skills/<skillName>@<digest>/`, used for `${CLAUDE_SKILL_DIR}` and the files line.
46
47
  */
47
48
  skillName: string;
49
+ /** The version's `skillDigest` (12 lowercase hex digits). */
50
+ digest: string;
48
51
  version: number;
49
52
  /** The SKILL.md body (after the frontmatter). */
50
53
  body: string;
@@ -6,12 +6,12 @@
6
6
  * The argument rules follow Claude Code (re-checked 2026-10-27): `$ARGUMENTS` is the whole argument
7
7
  * string, `$ARGUMENTS[N]` / `$N` are 0-based positional values (a missing one stays as written),
8
8
  * `$name` takes its value from frontmatter `arguments` (a missing one becomes empty), `\$` is a
9
- * literal `$`, `${CLAUDE_SKILL_DIR}` is `/skills/<name>` and every other `${…}` stays as written.
9
+ * literal `$`, `${CLAUDE_SKILL_DIR}` is `/skills/<name>@<digest>` and every other `${…}` stays as written.
10
10
  * When the body has no argument placeholder and the arguments are not empty, they are appended as
11
11
  * `ARGUMENTS: <args>`. Substituted text is never scanned again. Nothing is executed: shell
12
12
  * injections (`` !`cmd` ``) stay text, and the model is told how to run them.
13
13
  */
14
- import { isSkillName, SKILL_INVOCATION_LIMITS, SKILLS_SANDBOX_ROOT } from "./skill.js";
14
+ import { isSkillName, SKILL_INVOCATION_LIMITS, SKILLS_SANDBOX_ROOT, skillSandboxDir, } from "./skill.js";
15
15
  import { argumentHintText, SKILL_ARGUMENT_NAME_RE } from "./skill-keys.js";
16
16
  /**
17
17
  * The invocation metadata of a SKILL.md frontmatter. Values of the wrong type are ignored here
@@ -102,17 +102,26 @@ const checkSkillName = (skillName) => {
102
102
  throw new TypeError(`skillName must be a bare skill name (its directory under ${SKILLS_SANDBOX_ROOT}), got "${skillName}"`);
103
103
  }
104
104
  };
105
+ const DIGEST_RE = /^[0-9a-f]{12}$/;
106
+ const checkDigest = (digest) => {
107
+ if (typeof digest !== "string" || !DIGEST_RE.test(digest)) {
108
+ throw new TypeError(`digest must be 12 lowercase hex digits (skillDigest), got "${digest}"`);
109
+ }
110
+ };
105
111
  const tooLong = () => new RangeError(`the rendered skill must be at most ${SKILL_INVOCATION_LIMITS.renderedMaxChars} characters`);
106
112
  /**
107
113
  * A skill body with its arguments substituted (see the file header), without the wrapper.
108
- * `skillName` is the resolved skill's bare name: `${CLAUDE_SKILL_DIR}` becomes
109
- * `/skills/<skillName>` (a qualified `<plugin>:<name>` throws a `TypeError`). `args` is trimmed;
114
+ * `skillName` is the resolved skill's bare name and `digest` its version's `skillDigest`:
115
+ * `${CLAUDE_SKILL_DIR}` becomes `/skills/<skillName>@<digest>` (a qualified `<plugin>:<name>` or a
116
+ * digest that is not 12 lowercase hex digits throws a `TypeError`). `args` is trimmed;
110
117
  * more than {@link SKILL_INVOCATION_LIMITS.argumentsMaxChars} characters throw a `RangeError`, and
111
118
  * so does a result longer than {@link SKILL_INVOCATION_LIMITS.renderedMaxChars} (checked while
112
119
  * substituting, before the long string is built).
113
120
  */
114
- export function renderSkillBody(body, invocation, args, skillName) {
121
+ export function renderSkillBody(body, invocation, args, skillName, digest) {
115
122
  checkSkillName(skillName);
123
+ checkDigest(digest);
124
+ const dir = skillSandboxDir(skillName, digest);
116
125
  const all = trimmedArgs(args);
117
126
  const values = parseSkillArguments(all);
118
127
  const named = invocation.arguments ?? [];
@@ -125,7 +134,7 @@ export function renderSkillBody(body, invocation, args, skillName) {
125
134
  if (m === "\\$")
126
135
  return "$";
127
136
  if (m === "${CLAUDE_SKILL_DIR}")
128
- return `${SKILLS_SANDBOX_ROOT}/${skillName}`;
137
+ return dir;
129
138
  if (m === "$ARGUMENTS") {
130
139
  placeholder = true;
131
140
  return all;
@@ -173,14 +182,14 @@ const escapeAttr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").repla
173
182
  */
174
183
  export function renderSkillInvocation(i) {
175
184
  const args = trimmedArgs(i.args);
176
- const body = renderSkillBody(i.body, i.invocation, args, i.skillName).replace(CLOSE_TAG_RE, "<\\/$1");
185
+ const body = renderSkillBody(i.body, i.invocation, args, i.skillName, i.digest).replace(CLOSE_TAG_RE, "<\\/$1");
177
186
  const parts = [];
178
187
  if (i.lead !== false) {
179
188
  parts.push(escapeTags(`The user invoked the skill "${i.name}" with /${i.name}${args === "" ? "" : ` ${args}`}. Follow its instructions for this request.`));
180
189
  }
181
190
  parts.push(`<skill_invocation name="${escapeAttr(i.name)}" version="${i.version}">\n${body}\n</skill_invocation>`);
182
191
  if (i.lines.files === "load") {
183
- parts.push(`Its files are not in the sandbox yet: call Skill with "${i.name}" to copy them to ${SKILLS_SANDBOX_ROOT}/${i.skillName}/ before running its scripts.`);
192
+ parts.push(`Its files are not in the sandbox yet: call Skill with "${i.name}" to copy them to ${skillSandboxDir(i.skillName, i.digest)}/ before running its scripts.`);
184
193
  }
185
194
  else if (i.lines.files === "unavailable") {
186
195
  parts.push("Its other files are not available in this conversation.");
package/dist/skill.d.ts CHANGED
@@ -30,12 +30,29 @@ export declare const SKILL_INVOCATION_LIMITS: {
30
30
  };
31
31
  /** 技能入口文件(包根目录,大小写敏感)。 */
32
32
  export declare const SKILL_ENTRY_FILE = "SKILL.md";
33
- /** 沙箱里放技能的根目录:每个技能在 `/skills/<name>/` 下(只读复制)。 */
33
+ /**
34
+ * The sandbox root for skills: each skill version is copied (read-only to commands) to
35
+ * `/skills/<name>@<digest>/` ({@link skillSandboxDir}).
36
+ */
34
37
  export declare const SKILLS_SANDBOX_ROOT = "/skills";
35
38
  /** 是否为合法的技能名(见文件头的规范说明)。 */
36
39
  export declare function isSkillName(s: unknown): s is string;
37
- /** 技能文件在沙箱里的路径:`/skills/<name>/<path>`。 */
38
- export declare function skillSandboxPath(name: string, path: string): string;
40
+ /**
41
+ * The digest of a skill version's file manifest: the first 12 hex digits of the sha256 of its
42
+ * files sorted by path (JavaScript string order), one `${path} ${size} ${sha256}\n` line each. Two
43
+ * versions with the same files get the same digest, so their sandbox directories coincide; any
44
+ * other change gives a different directory, so two versions of one skill never overwrite each
45
+ * other in a shared file system.
46
+ */
47
+ export declare function skillDigest(files: {
48
+ path: string;
49
+ size: number;
50
+ sha256: string;
51
+ }[]): string;
52
+ /** A skill version's directory in the sandbox: `/skills/<name>@<digest>` ({@link skillDigest}). */
53
+ export declare function skillSandboxDir(name: string, digest: string): string;
54
+ /** A skill file's path in the sandbox: `/skills/<name>@<digest>/<path>`. */
55
+ export declare function skillSandboxPath(name: string, digest: string, path: string): string;
39
56
  /** {@link parseSkillMd} 成功时的结果。 */
40
57
  export interface ParsedSkillMd {
41
58
  name: string;
package/dist/skill.js CHANGED
@@ -16,8 +16,9 @@
16
16
  * whose types are checked because the kernel uses them (`skillInvocation`, phase D). Matching the
17
17
  * directory name is not enforced: an uploaded zip's outer directory often has another name (GitHub's
18
18
  * `repo-main/`); the skill's name is always the frontmatter `name`, and its sandbox directory is
19
- * `/skills/<name>/`.
19
+ * `/skills/<name>@<digest>/` ({@link skillSandboxDir}, {@link skillDigest}).
20
20
  */
21
+ import { createHash } from "node:crypto";
21
22
  import { readFrontmatter } from "./frontmatter.js";
22
23
  import { hasControlChars } from "./model.js";
23
24
  import { validateSandboxPath } from "./sandbox.js";
@@ -54,7 +55,10 @@ export const SKILL_INVOCATION_LIMITS = {
54
55
  };
55
56
  /** 技能入口文件(包根目录,大小写敏感)。 */
56
57
  export const SKILL_ENTRY_FILE = "SKILL.md";
57
- /** 沙箱里放技能的根目录:每个技能在 `/skills/<name>/` 下(只读复制)。 */
58
+ /**
59
+ * The sandbox root for skills: each skill version is copied (read-only to commands) to
60
+ * `/skills/<name>@<digest>/` ({@link skillSandboxDir}).
61
+ */
58
62
  export const SKILLS_SANDBOX_ROOT = "/skills";
59
63
  const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
60
64
  const XML_TAG_RE = /<\/?[A-Za-z][^<>]*>/;
@@ -63,9 +67,27 @@ const DRIVE_RE = /^[A-Za-z]:/;
63
67
  export function isSkillName(s) {
64
68
  return typeof s === "string" && s.length <= SKILL_LIMITS.nameMaxChars && NAME_RE.test(s);
65
69
  }
66
- /** 技能文件在沙箱里的路径:`/skills/<name>/<path>`。 */
67
- export function skillSandboxPath(name, path) {
68
- return `${SKILLS_SANDBOX_ROOT}/${name}/${path}`;
70
+ /**
71
+ * The digest of a skill version's file manifest: the first 12 hex digits of the sha256 of its
72
+ * files sorted by path (JavaScript string order), one `${path} ${size} ${sha256}\n` line each. Two
73
+ * versions with the same files get the same digest, so their sandbox directories coincide; any
74
+ * other change gives a different directory, so two versions of one skill never overwrite each
75
+ * other in a shared file system.
76
+ */
77
+ export function skillDigest(files) {
78
+ const lines = [...files]
79
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
80
+ .map((f) => `${f.path} ${f.size} ${f.sha256}\n`)
81
+ .join("");
82
+ return createHash("sha256").update(lines).digest("hex").slice(0, 12);
83
+ }
84
+ /** A skill version's directory in the sandbox: `/skills/<name>@<digest>` ({@link skillDigest}). */
85
+ export function skillSandboxDir(name, digest) {
86
+ return `${SKILLS_SANDBOX_ROOT}/${name}@${digest}`;
87
+ }
88
+ /** A skill file's path in the sandbox: `/skills/<name>@<digest>/<path>`. */
89
+ export function skillSandboxPath(name, digest, path) {
90
+ return `${skillSandboxDir(name, digest)}/${path}`;
69
91
  }
70
92
  const chars = (s) => [...s].length;
71
93
  /**
@@ -179,7 +201,7 @@ function pathProblem(p) {
179
201
  return "path must not contain '.' or '..' segments";
180
202
  }
181
203
  // 再按沙箱路径规则核一遍(百分号编码的 `..` 等)
182
- const errs = validateSandboxPath(skillSandboxPath("skill", p));
204
+ const errs = validateSandboxPath(skillSandboxPath("skill", "000000000000", p));
183
205
  return errs[0] ?? null;
184
206
  }
185
207
  /**
package/dist/tool.d.ts CHANGED
@@ -129,6 +129,14 @@ export interface ToolCallContext {
129
129
  state: unknown;
130
130
  answer: FormAnswer | null;
131
131
  };
132
+ /**
133
+ * The extension's effective deployment config for this org: the platform's deployment config
134
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
135
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
136
+ * for work done for this org. Optional in the type so older extensions compile; the kernel
137
+ * always passes it.
138
+ */
139
+ config?: unknown;
132
140
  }
133
141
  export type ToolHandler = (input: unknown, ctx: ToolCallContext) => Promise<ToolResult | InputRequired>;
134
142
  /**
package/dist/ui.d.ts CHANGED
@@ -175,6 +175,14 @@ export interface UiRenderArgs {
175
175
  callId: string;
176
176
  key?: string;
177
177
  };
178
+ /**
179
+ * The extension's effective deployment config for this org: the platform's deployment config
180
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
181
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
182
+ * for work done for this org. Optional in the type so older extensions compile; the kernel
183
+ * always passes it.
184
+ */
185
+ config?: unknown;
178
186
  }
179
187
  /** 视图里的按钮或表单提交:`actionId` 来自块;表单提交时带 `answer`。 */
180
188
  export interface UiActionArgs extends UiRenderArgs {