@capybara-hq/plugin-sdk 0.1.0-rc.2 → 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 (51) 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 +2 -1
  25. package/dist/config-ui.js +25 -7
  26. package/dist/ctx.d.ts +3 -1
  27. package/dist/directory.d.ts +11 -2
  28. package/dist/extension-point.d.ts +4 -0
  29. package/dist/extension-point.js +16 -14
  30. package/dist/file-preview.d.ts +8 -0
  31. package/dist/hooks.d.ts +11 -2
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +2 -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 +15 -2
  39. package/dist/plugin-manifest.js +11 -1
  40. package/dist/sandbox.d.ts +140 -60
  41. package/dist/sandbox.js +30 -6
  42. package/dist/skill-invocation.d.ts +8 -5
  43. package/dist/skill-invocation.js +17 -8
  44. package/dist/skill.d.ts +20 -3
  45. package/dist/skill.js +28 -6
  46. package/dist/tool.d.ts +8 -0
  47. package/dist/ui.d.ts +8 -0
  48. package/dist/web.d.ts +30 -5
  49. package/package.json +4 -2
  50. package/schemas/marketplace.v1.schema.json +36 -0
  51. package/schemas/plugin.v1.schema.json +29 -4
@@ -53,6 +53,14 @@ export interface FilePreviewArgs {
53
53
  * rejects a file over the previewer's `maxBytes`. The extension never gets a storage key or URL.
54
54
  */
55
55
  read(): Promise<Uint8Array>;
56
+ /**
57
+ * The extension's effective deployment config for this org: the platform's deployment config
58
+ * with the org's copy applied to the overridable fields (`orgAccess(…, "override")`), or the
59
+ * platform's config (equal to `ctx.config`) while the org follows it. Use it, not `ctx.config`,
60
+ * for work done for this org. Optional in the type so older extensions compile; the kernel
61
+ * always passes it.
62
+ */
63
+ config?: unknown;
56
64
  }
57
65
  /** Checks a `file-preview@1` provider; returns the problems (empty = valid). */
58
66
  export declare function validateFilePreviewProvider(p: unknown): string[];
package/dist/hooks.d.ts CHANGED
@@ -169,9 +169,18 @@ export type HookVerdict = void | {
169
169
  };
170
170
  /**
171
171
  * A hook handler. The payload is a deep-frozen copy (mutating it throws). `signal` aborts when the
172
- * hook runs past `hooks.timeoutMs` or the call it observes is aborted.
172
+ * hook runs past `hooks.timeoutMs` or the call it observes is aborted. `call.config` is the
173
+ * extension's effective deployment config for the payload's org (the platform's config with the
174
+ * org's copy applied to the overridable fields, or `ctx.config` while the org follows it); the
175
+ * kernel always passes it, and never calls a hook for an org where that config is not valid
176
+ * (observe hooks are skipped, policy hooks deny).
173
177
  */
174
- export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal) => Promise<HookVerdict> | HookVerdict;
178
+ export type HookHandler<K extends AvailableHook> = (payload: HookPayloads[K], signal: AbortSignal, call?: HookCall) => Promise<HookVerdict> | HookVerdict;
179
+ /** The per-call context of a hook handler (see {@link HookHandler}). */
180
+ export interface HookCall {
181
+ /** The extension's effective deployment config for the payload's org. */
182
+ config: unknown;
183
+ }
175
184
  /**
176
185
  * - `policy` (only on {@link POLICY_HOOKS}): policy hooks run serially by plugin name (registration
177
186
  * order within a plugin) before the call; the first `{ deny }`, error or timeout blocks the call and
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const SDK_VERSION = "0.1.0-rc.2";
1
+ export declare const SDK_VERSION = "0.1.0-rc.3";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -16,6 +16,7 @@ export * from "./identity.js";
16
16
  export * from "./marketplace.js";
17
17
  export * from "./model.js";
18
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
19
20
  export * from "./permissions.js";
20
21
  export * from "./plugin-manifest.js";
21
22
  export * from "./plugin-package.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export const SDK_VERSION = "0.1.0-rc.2";
1
+ export const SDK_VERSION = "0.1.0-rc.3";
2
2
  export * from "./agent-template.js";
3
3
  export * from "./bootstrap.js";
4
4
  export * from "./cards.js";
@@ -16,6 +16,7 @@ export * from "./identity.js";
16
16
  export * from "./marketplace.js";
17
17
  export * from "./model.js";
18
18
  export * from "./model-params.js";
19
+ export * from "./org-access.js";
19
20
  export * from "./permissions.js";
20
21
  export * from "./plugin-manifest.js";
21
22
  export * from "./plugin-package.js";
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  /**
3
3
  * The marketplace manifest `marketplace.json` (package ecosystem spec §5.2): a list of plugins,
4
- * each with one source. One entry is one version: a marketplace that offers several versions lists
5
- * the plugin once per version, so `(name, marketplaceEntryVersion)` is unique in a manifest.
4
+ * each with one source. One entry is one version. `capybara-plugin marketplace --write` lists one
5
+ * entry per plugin, its current version (plugin component management spec §13.2); readers still
6
+ * accept several versions of a plugin (marketplaces written in the older form), so
7
+ * `(name, marketplaceEntryVersion)` is unique in a manifest.
6
8
  *
7
9
  * Sources: a same-repo `path`, a `git` repo, an `npm` package (exact version + sha512, downloads
8
10
  * are verified against it, never npm metadata) or an `archive` URL (sha512 or sha256 SRI).
@@ -19,6 +21,10 @@ export declare const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1;
19
21
  export declare const MARKETPLACE_LIMITS: {
20
22
  readonly nameChars: 100;
21
23
  readonly summaryChars: 280;
24
+ /** Characters of each `displayName` language (as `plugin.json`'s). */
25
+ readonly displayNameChars: 100;
26
+ /** Characters of a `deprecated.reason`. */
27
+ readonly deprecationReasonChars: 280;
22
28
  readonly plugins: 5000;
23
29
  readonly permissionKeys: 500;
24
30
  readonly urlChars: 2000;
@@ -53,6 +59,11 @@ export interface MarketplaceEntry {
53
59
  en: string;
54
60
  zh?: string;
55
61
  };
62
+ /** Display name (the plugin's `plugin.json` `displayName`); lists show it, else the name. */
63
+ displayName?: {
64
+ zh: string;
65
+ en: string;
66
+ };
56
67
  /** Exact semver; defaults to the npm source's version, otherwise the plugin's own. */
57
68
  version?: string;
58
69
  /** Exact semver. */
@@ -64,6 +75,15 @@ export interface MarketplaceEntry {
64
75
  };
65
76
  /** https URL. */
66
77
  homepage?: string;
78
+ /**
79
+ * This version is deprecated (`true`, or with a reason shown to admins): browsing lists it under
80
+ * the plugin's other versions, never as the one to import, and the kernel refuses importing it
81
+ * (`marketplace.entry_deprecated`). Set by hand on the entry by the marketplace maintainer;
82
+ * `capybara-plugin marketplace --write` keeps it. `false` is the same as absent.
83
+ */
84
+ deprecated?: boolean | {
85
+ reason?: string;
86
+ };
67
87
  source: MarketplaceSource;
68
88
  }
69
89
  /** `marketplace.json`. */
@@ -97,3 +117,10 @@ export declare function parseMarketplaceManifest(json: unknown, opts?: Marketpla
97
117
  };
98
118
  /** The entry's version: `version`, else the npm source's version, else `null` (the plugin's own). */
99
119
  export declare function marketplaceEntryVersion(e: MarketplaceEntry): string | null;
120
+ /**
121
+ * The entry's deprecation (see {@link MarketplaceEntry.deprecated}): `null` when it is not
122
+ * deprecated (absent or `false`), else its reason (`null` without one).
123
+ */
124
+ export declare function marketplaceEntryDeprecation(e: MarketplaceEntry): {
125
+ reason: string | null;
126
+ } | null;
@@ -2,8 +2,10 @@ import { z } from "zod";
2
2
  import { CATALOG_ITEM_NAME_RE, isSafePackagePath, NPM_PACKAGE_NAME_RE, SEMVER_VERSION_RE, SHA512_INTEGRITY_RE, } from "./plugin-package.js";
3
3
  /**
4
4
  * The marketplace manifest `marketplace.json` (package ecosystem spec §5.2): a list of plugins,
5
- * each with one source. One entry is one version: a marketplace that offers several versions lists
6
- * the plugin once per version, so `(name, marketplaceEntryVersion)` is unique in a manifest.
5
+ * each with one source. One entry is one version. `capybara-plugin marketplace --write` lists one
6
+ * entry per plugin, its current version (plugin component management spec §13.2); readers still
7
+ * accept several versions of a plugin (marketplaces written in the older form), so
8
+ * `(name, marketplaceEntryVersion)` is unique in a manifest.
7
9
  *
8
10
  * Sources: a same-repo `path`, a `git` repo, an `npm` package (exact version + sha512, downloads
9
11
  * are verified against it, never npm metadata) or an `archive` URL (sha512 or sha256 SRI).
@@ -20,6 +22,10 @@ export const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1;
20
22
  export const MARKETPLACE_LIMITS = {
21
23
  nameChars: 100,
22
24
  summaryChars: 280,
25
+ /** Characters of each `displayName` language (as `plugin.json`'s). */
26
+ displayNameChars: 100,
27
+ /** Characters of a `deprecated.reason`. */
28
+ deprecationReasonChars: 280,
23
29
  plugins: 5000,
24
30
  permissionKeys: 500,
25
31
  urlChars: 2000,
@@ -123,6 +129,12 @@ function buildSchema(opts) {
123
129
  en: z.string().min(1).max(M.summaryChars),
124
130
  zh: z.string().min(1).max(M.summaryChars).optional(),
125
131
  }),
132
+ displayName: z
133
+ .object({
134
+ zh: z.string().min(1).max(M.displayNameChars),
135
+ en: z.string().min(1).max(M.displayNameChars),
136
+ })
137
+ .optional(),
126
138
  version: exactVersion("version").optional(),
127
139
  minKernel: exactVersion("minKernel").optional(),
128
140
  permissions: z
@@ -132,6 +144,12 @@ function buildSchema(opts) {
132
144
  })
133
145
  .optional(),
134
146
  homepage: urlString(HTTPS_URL_RE).optional(),
147
+ deprecated: z
148
+ .union([
149
+ z.boolean(),
150
+ z.object({ reason: z.string().min(1).max(M.deprecationReasonChars).optional() }),
151
+ ])
152
+ .optional(),
135
153
  source,
136
154
  });
137
155
  return z
@@ -254,3 +272,13 @@ export function marketplaceEntryVersion(e) {
254
272
  return e.source.npm.version;
255
273
  return null;
256
274
  }
275
+ /**
276
+ * The entry's deprecation (see {@link MarketplaceEntry.deprecated}): `null` when it is not
277
+ * deprecated (absent or `false`), else its reason (`null` without one).
278
+ */
279
+ export function marketplaceEntryDeprecation(e) {
280
+ const d = e.deprecated;
281
+ if (d === undefined || d === false)
282
+ return null;
283
+ return { reason: d === true ? null : (d.reason ?? null) };
284
+ }
@@ -0,0 +1,27 @@
1
+ import type { z } from "zod";
2
+ /**
3
+ * How an org may treat a deployment config field: `readonly` (the org sees it, never sets it) or
4
+ * `override` (an org may set its own value). A field without a level is deployment-only.
5
+ */
6
+ export type OrgAccess = "readonly" | "override";
7
+ /**
8
+ * Marks a deployment config field with an org access level (Zod 4 meta `{ org: access }`), merging
9
+ * with existing meta (`secret`, `hot`, `ui`), so `orgAccess(secret(x), …)` and
10
+ * `secret(orgAccess(x, …))` are the same. `configJsonSchema` publishes it as `"x-org"`.
11
+ *
12
+ * The author's promise for `override`: the extension reads the field from the per-call `config`
13
+ * the kernel passes with each request, not from `ctx.config`, and the field may be absent from
14
+ * `ctx.config` (the deployment value is the default, which an org may replace per call).
15
+ *
16
+ * The per-call config of an org that has its own copy still contains the platform's hidden
17
+ * (platform-only) secrets, which an org cannot set; it never contains an overridable platform
18
+ * secret. Never send a hidden secret to an endpoint taken from an overridable field: an org could
19
+ * point that endpoint at a server it controls.
20
+ */
21
+ export declare function orgAccess<T extends z.ZodTypeAny>(schema: T, access: OrgAccess): T;
22
+ /**
23
+ * The org access level of a schema's meta, or undefined when it has none. It sees through
24
+ * `.optional()`, `.default()`, `.nullable()`, `.prefault()`, `.catch()` and `.readonly()` (the
25
+ * outermost level wins), so it agrees with the `"x-org"` that `configJsonSchema` publishes.
26
+ */
27
+ export declare function orgAccessOf(schema: z.ZodTypeAny): OrgAccess | undefined;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Marks a deployment config field with an org access level (Zod 4 meta `{ org: access }`), merging
3
+ * with existing meta (`secret`, `hot`, `ui`), so `orgAccess(secret(x), …)` and
4
+ * `secret(orgAccess(x, …))` are the same. `configJsonSchema` publishes it as `"x-org"`.
5
+ *
6
+ * The author's promise for `override`: the extension reads the field from the per-call `config`
7
+ * the kernel passes with each request, not from `ctx.config`, and the field may be absent from
8
+ * `ctx.config` (the deployment value is the default, which an org may replace per call).
9
+ *
10
+ * The per-call config of an org that has its own copy still contains the platform's hidden
11
+ * (platform-only) secrets, which an org cannot set; it never contains an overridable platform
12
+ * secret. Never send a hidden secret to an endpoint taken from an overridable field: an org could
13
+ * point that endpoint at a server it controls.
14
+ */
15
+ export function orgAccess(schema, access) {
16
+ return schema.meta({ ...(schema.meta() ?? {}), org: access });
17
+ }
18
+ /** The wrappers a field's org level is read through, as `configJsonSchema` inherits meta. */
19
+ const WRAPPERS = new Set(["default", "prefault", "optional", "nullable", "readonly", "catch"]);
20
+ /**
21
+ * The org access level of a schema's meta, or undefined when it has none. It sees through
22
+ * `.optional()`, `.default()`, `.nullable()`, `.prefault()`, `.catch()` and `.readonly()` (the
23
+ * outermost level wins), so it agrees with the `"x-org"` that `configJsonSchema` publishes.
24
+ */
25
+ export function orgAccessOf(schema) {
26
+ let node = schema;
27
+ while (node) {
28
+ const v = node.meta()?.org;
29
+ if (v === "readonly" || v === "override")
30
+ return v;
31
+ const def = node._zod.def;
32
+ node = WRAPPERS.has(def.type) ? def.innerType : undefined;
33
+ }
34
+ return undefined;
35
+ }
@@ -85,6 +85,19 @@ export interface PluginExtensionSection {
85
85
  org?: JsonSchema;
86
86
  };
87
87
  }
88
+ /**
89
+ * A plugin's description: a string (English) or `{ en, zh? }`, each ≤ 1024 characters. Read it
90
+ * through {@link pluginDescription}.
91
+ */
92
+ export type PluginDescription = string | {
93
+ en: string;
94
+ zh?: string;
95
+ };
96
+ /** A plugin description as `{ en, zh? }` (a plain string is the English text). */
97
+ export declare function pluginDescription(d: PluginDescription): {
98
+ en: string;
99
+ zh?: string;
100
+ };
88
101
  /** `.capybara-plugin/plugin.json`. */
89
102
  export interface PluginManifest {
90
103
  $schema?: string;
@@ -94,8 +107,8 @@ export interface PluginManifest {
94
107
  /** Exact semver version. */
95
108
  version: string;
96
109
  publisher: string;
97
- /** ≤ 1024 characters. */
98
- description: string;
110
+ /** A string (English) or `{ en, zh? }`, each ≤ 1024 characters; see {@link pluginDescription}. */
111
+ description: PluginDescription;
99
112
  displayName?: LocalizedName;
100
113
  /** `./…` .svg / .png inside the plugin. */
101
114
  icon?: string;
@@ -48,6 +48,10 @@ export const PLUGIN_MANIFEST_LIMITS = {
48
48
  displayNameChars: 100,
49
49
  pathChars: 512,
50
50
  };
51
+ /** A plugin description as `{ en, zh? }` (a plain string is the English text). */
52
+ export function pluginDescription(d) {
53
+ return typeof d === "string" ? { en: d } : { en: d.en, ...(d.zh !== undefined && { zh: d.zh }) };
54
+ }
51
55
  const L = PLUGIN_MANIFEST_LIMITS;
52
56
  const MAX_ERRORS = 50;
53
57
  const ICON_RE = /\.(?:svg|png)$/i;
@@ -116,7 +120,13 @@ const schema = z
116
120
  .max(256)
117
121
  .regex(SEMVER_VERSION_RE, { message: "version must be an exact semver version" }),
118
122
  publisher: text(L.publisherChars, "publisher"),
119
- description: text(L.descriptionChars, "description"),
123
+ description: z.union([
124
+ text(L.descriptionChars, "description"),
125
+ z.strictObject({
126
+ en: text(L.descriptionChars, "description.en"),
127
+ zh: text(L.descriptionChars, "description.zh").optional(),
128
+ }),
129
+ ]),
120
130
  displayName: localized.optional(),
121
131
  icon: dotPath("icon")
122
132
  .regex(ICON_RE, { message: "icon must be a .svg or .png file" })
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;