@capybara-hq/plugin-sdk 0.1.0-rc.2 → 0.1.0-rc.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 +1 -1
- package/agent-docs/agent-templates.md +47 -0
- package/agent-docs/concepts.md +58 -0
- package/agent-docs/connector-templates.md +47 -0
- package/agent-docs/extension-points/channel.md +15 -0
- package/agent-docs/extension-points/config-source.md +15 -0
- package/agent-docs/extension-points/connector.md +15 -0
- package/agent-docs/extension-points/counter.md +15 -0
- package/agent-docs/extension-points/directory.md +15 -0
- package/agent-docs/extension-points/file-preview.md +15 -0
- package/agent-docs/extension-points/identity-source.md +15 -0
- package/agent-docs/extension-points/model-provider.md +15 -0
- package/agent-docs/extension-points/object-store.md +15 -0
- package/agent-docs/extension-points/sandbox.md +15 -0
- package/agent-docs/extension-points/secret-store.md +15 -0
- package/agent-docs/extension-points/web-fetch.md +15 -0
- package/agent-docs/extension-points/web-search.md +15 -0
- package/agent-docs/extensions.md +63 -0
- package/agent-docs/plugin-format.md +60 -0
- package/agent-docs/skills.md +44 -0
- package/agent-docs/tool-naming.md +21 -0
- package/dist/agent-template.d.ts +85 -0
- package/dist/agent-template.js +361 -2
- package/dist/config-ui.d.ts +2 -1
- package/dist/config-ui.js +25 -7
- package/dist/ctx.d.ts +65 -1
- package/dist/directory.d.ts +11 -2
- package/dist/extension-point.d.ts +4 -0
- package/dist/extension-point.js +16 -14
- package/dist/file-preview.d.ts +8 -0
- package/dist/hooks.d.ts +73 -14
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/marketplace.d.ts +29 -2
- package/dist/marketplace.js +30 -2
- package/dist/model.d.ts +60 -2
- package/dist/model.js +22 -0
- package/dist/org-access.d.ts +27 -0
- package/dist/org-access.js +35 -0
- package/dist/permissions.d.ts +4 -1
- package/dist/plugin-manifest.d.ts +15 -2
- package/dist/plugin-manifest.js +11 -1
- package/dist/sandbox.d.ts +140 -60
- package/dist/sandbox.js +30 -6
- package/dist/skill-invocation.d.ts +8 -5
- package/dist/skill-invocation.js +17 -8
- package/dist/skill.d.ts +20 -3
- package/dist/skill.js +28 -6
- package/dist/tool.d.ts +8 -0
- package/dist/ui.d.ts +8 -0
- package/dist/web.d.ts +30 -5
- package/package.json +4 -2
- package/schemas/marketplace.v1.schema.json +36 -0
- package/schemas/plugin.v1.schema.json +29 -4
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sandbox contract `sandbox@
|
|
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@
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
36
|
-
*
|
|
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
|
-
/**
|
|
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@
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
*
|
|
59
|
-
* `
|
|
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
|
-
*
|
|
62
|
-
* `
|
|
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
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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 (
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
82
|
-
* (
|
|
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
|
|
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
|
-
*
|
|
100
|
-
*
|
|
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
|
|
174
|
+
destroyByName(name: string, signal: AbortSignal): Promise<void>;
|
|
103
175
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
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
|
|
183
|
+
killAll(h: SandboxHandle, signal: AbortSignal): Promise<void>;
|
|
109
184
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
|
191
|
+
killTagged(h: SandboxHandle, tags: string[], signal: AbortSignal): Promise<void>;
|
|
118
192
|
/**
|
|
119
|
-
*
|
|
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.
|
|
205
|
+
* {@link SandboxProvider.killTagged} can find it.
|
|
131
206
|
*/
|
|
132
|
-
tag
|
|
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
|
-
/**
|
|
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@
|
|
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@
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
@@ -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
|
|
30
|
-
* `/skills/<skillName>` (a qualified `<plugin>:<name>`
|
|
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
|
|
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;
|
package/dist/skill-invocation.js
CHANGED
|
@@ -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
|
|
109
|
-
* `/skills/<skillName>` (a qualified `<plugin>:<name>`
|
|
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
|
|
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, "&").replace(/"/g, """).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 ${
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
38
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|