@capybara-hq/plugin-sandbox-e2b 0.0.0-stage → 0.1.0-rc.1

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.
@@ -0,0 +1,159 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "sandbox-e2b",
4
+ "version": "0.1.0-rc.1",
5
+ "publisher": "Capybara",
6
+ "description": "Conversation sandboxes (sandbox@1) as E2B sandboxes (E2B cloud or self-hosted), with pause / resume and E2B egress controls",
7
+ "displayName": {
8
+ "zh": "E2B 沙箱",
9
+ "en": "E2B sandbox"
10
+ },
11
+ "homepage": "https://github.com/capybara-hq/plugins/tree/main/plugins/sandbox-e2b#readme",
12
+ "extension": {
13
+ "entry": "./dist/index.mjs",
14
+ "manifest": {
15
+ "name": "sandbox-e2b",
16
+ "version": "0.1.0-rc.1",
17
+ "sdk": "^0.1.0-rc.0",
18
+ "provides": [
19
+ "sandbox@1"
20
+ ],
21
+ "requires": [],
22
+ "optional": [],
23
+ "permissions": {},
24
+ "config": {
25
+ "deployment": [
26
+ {
27
+ "path": "allowOpenEgress",
28
+ "secret": false,
29
+ "hot": false
30
+ },
31
+ {
32
+ "path": "apiKey",
33
+ "secret": true,
34
+ "hot": false
35
+ },
36
+ {
37
+ "path": "apiUrl",
38
+ "secret": false,
39
+ "hot": false
40
+ },
41
+ {
42
+ "path": "domain",
43
+ "secret": false,
44
+ "hot": false
45
+ },
46
+ {
47
+ "path": "domainAllowlist",
48
+ "secret": false,
49
+ "hot": false
50
+ },
51
+ {
52
+ "path": "template",
53
+ "secret": false,
54
+ "hot": false
55
+ },
56
+ {
57
+ "path": "timeoutSeconds",
58
+ "secret": false,
59
+ "hot": false
60
+ }
61
+ ],
62
+ "org": []
63
+ }
64
+ },
65
+ "configSchemas": {
66
+ "deployment": {
67
+ "type": "object",
68
+ "properties": {
69
+ "apiKey": {
70
+ "type": "string",
71
+ "minLength": 1,
72
+ "x-secret": true,
73
+ "x-ui": {
74
+ "label": {
75
+ "zh": "API 密钥",
76
+ "en": "API key"
77
+ }
78
+ },
79
+ "title": "API key"
80
+ },
81
+ "apiUrl": {
82
+ "type": "string",
83
+ "format": "uri",
84
+ "x-ui": {
85
+ "label": {
86
+ "zh": "接口地址",
87
+ "en": "API URL"
88
+ }
89
+ },
90
+ "title": "API URL"
91
+ },
92
+ "domain": {
93
+ "default": "e2b.app",
94
+ "type": "string",
95
+ "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$",
96
+ "x-ui": {
97
+ "label": {
98
+ "zh": "沙箱域名",
99
+ "en": "Sandbox domain"
100
+ }
101
+ },
102
+ "title": "Sandbox domain"
103
+ },
104
+ "template": {
105
+ "type": "string",
106
+ "minLength": 1,
107
+ "x-ui": {
108
+ "label": {
109
+ "zh": "模板",
110
+ "en": "Template"
111
+ }
112
+ },
113
+ "title": "Template"
114
+ },
115
+ "timeoutSeconds": {
116
+ "default": 3600,
117
+ "type": "integer",
118
+ "minimum": 60,
119
+ "maximum": 86400,
120
+ "x-ui": {
121
+ "label": {
122
+ "zh": "沙箱超时",
123
+ "en": "Sandbox timeout"
124
+ }
125
+ },
126
+ "title": "Sandbox timeout"
127
+ },
128
+ "domainAllowlist": {
129
+ "default": true,
130
+ "type": "boolean",
131
+ "x-ui": {
132
+ "label": {
133
+ "zh": "按域名出站白名单",
134
+ "en": "Domain egress allowlist"
135
+ }
136
+ },
137
+ "title": "Domain egress allowlist"
138
+ },
139
+ "allowOpenEgress": {
140
+ "default": false,
141
+ "type": "boolean",
142
+ "x-ui": {
143
+ "label": {
144
+ "zh": "允许开放出站",
145
+ "en": "Allow open egress"
146
+ }
147
+ },
148
+ "title": "Allow open egress"
149
+ }
150
+ },
151
+ "required": [
152
+ "apiKey",
153
+ "template"
154
+ ],
155
+ "additionalProperties": false
156
+ }
157
+ }
158
+ }
159
+ }
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,2 @@
1
+ Capybara
2
+ Copyright 2026 Meta-1 (Tianjin Yuanyilian Technology Co., Ltd.)
package/README.md CHANGED
@@ -1,3 +1,173 @@
1
- # Temporary Holding Version
1
+ # @capybara-hq/plugin-sandbox-e2b
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The E2B implementation of the conversation sandbox (`sandbox@1`): one E2B sandbox per conversation (E2B cloud or self-hosted E2B), using E2B's pause for hibernation
4
+ and connect for resume. It is an either-or choice with `@capybara-hq/plugin-sandbox-docker` (`sandbox@1` is a single-select capability), and uses the same image.
5
+
6
+ ## Deployment config
7
+
8
+ | Key | Default | Description |
9
+ |---|---|---|
10
+ | `apiKey` | (required, secret) | E2B API key (`e2b_…`) |
11
+ | `template` | (required) | Template name or id, built from `plugins/sandbox-docker/image/Dockerfile` (see below) |
12
+ | `apiUrl` | `https://api.<domain>` | Control plane API address; for self-hosted E2B, use your own address |
13
+ | `domain` | `e2b.app` | Sandbox domain: the envd address is `https://49983-<sandbox id>.<domain>`; when the API returns the sandbox's own `domain`, the returned one wins |
14
+ | `timeoutSeconds` | `3600` | Sandbox timeout (60–86400 seconds); when it expires E2B **auto-pauses** (does not delete), see "Lifecycle" |
15
+ | `domainAllowlist` | `true` | Whether E2B supports a per-domain egress allowlist; set `false` for self-hosted versions that do not, and `allowlist` degrades to `none` with a warning |
16
+ | `allowOpenEgress` | `false` | Allow `sandbox.limits.egress = "open"`; when false, `create` fails immediately on open (an English error) |
17
+
18
+ ```bash
19
+ export CAPYBARA_EXTENSIONS='["@capybara-hq/plugin-sandbox-e2b"]'
20
+ export CAPYBARA_EXTENSION_CONFIG='{"sandbox-e2b":{"apiKey":"e2b_…","template":"capybara-sandbox"}}'
21
+ ```
22
+
23
+ The deployment config schema is strict: extra keys are errors. The kernel config `sandbox.image` has no effect on this plugin (the image is decided by the template).
24
+
25
+ ## Template (the same image as the Docker plugin)
26
+
27
+ The template is built from `plugins/sandbox-docker/image/Dockerfile` (E2B's Template SDK parses the Dockerfile's
28
+ FROM / RUN / COPY / ADD / WORKDIR / USER / ENV / ARG / CMD / ENTRYPOINT). From the repository root:
29
+
30
+ ```bash
31
+ export E2B_API_KEY=e2b_… # for self-hosted, also set E2B_DOMAIN / E2B_API_URL
32
+ mkdir -p /tmp/e2b-build && cd /tmp/e2b-build && npm init -y >/dev/null && npm i e2b@2.58.0
33
+ cat > build.mjs <<'EOF'
34
+ import { readFileSync } from "node:fs";
35
+ import { defaultBuildLogger, Template } from "e2b";
36
+ const dockerfile = readFileSync(process.argv[2], "utf8");
37
+ await Template.build(Template().fromDockerfile(dockerfile), "capybara-sandbox", {
38
+ cpuCount: 1, // matches the kernel's sandbox.cpu default
39
+ memoryMB: 2048, // matches the kernel's sandbox.memoryMb default
40
+ onBuildLogs: defaultBuildLogger(),
41
+ });
42
+ EOF
43
+ node build.mjs <repo path>/plugins/sandbox-docker/image/Dockerfile
44
+ ```
45
+
46
+ Then set `template` to `capybara-sandbox`. Image conventions (the plugin depends on them): the user `sandbox` (uid 1000), with HOME and the working directory at
47
+ `/workspace`; `/skills` owned by root (0755); `/opt/capybara` owned by root and 0700; `runuser` / `setsid` (util-linux),
48
+ `pkill` / `pgrep` (procps) and GNU coreutils installed. Rebuild the template after changing the Dockerfile.
49
+
50
+ ## Lifecycle
51
+
52
+ | Operation | E2B call |
53
+ |---|---|
54
+ | `create` | First lists by the metadata `capybara-sandbox-name=<name>` (`GET /v2/sandboxes?metadata=…&state=running,paused`, paginated) and deletes any old instance of the same name (the kernel may have exited before recording the handle), then `POST /v2/sandboxes` (`templateID`, `metadata`, `timeout`, `autoPause: true`, `secure: true`, egress settings); the handle id is E2B's sandbox id |
55
+ | `resume` | `POST /v2/sandboxes/{id}/connect` (a paused one is resumed, and a running one gets its timeout pushed back); 404 → `gone` |
56
+ | `suspend` | `POST /sandboxes/{id}/pause` (files and memory are both kept: background processes and `/tmp` are still there after resume; the Docker plugin's hibernation instead ends processes and clears `/tmp`); a missing one (404) or an already paused one (409) also counts as success |
57
+ | `killAll` | After connect (a paused one is resumed first, and the processes in memory come back with it), runs a script as the sandbox user that kills every command started through `exec` that is still running (SIGKILL by process group and session); called when the kernel takes over from a crashed previous lock holder |
58
+ | Concurrent use (`concurrentExec`, `killTagged`) | Declares `concurrentExec: true`: envd runs each `process.Start` independently, so parallel sub-agents of one conversation run commands and file operations in its sandbox at the same time. Each exec's control file carries the holder's tag (`/tmp/.capybara-exec-<tag>-<uuid>`, tags checked against `SANDBOX_TAG_RE`); `killTagged` runs a script as the sandbox user that kills only the commands of the given tags. Concurrent `connect` calls are idempotent (a running sandbox only gets its timeout extended); a 409 answered while another call's resume is in flight is retried briefly (5 × 500 ms; to be confirmed on real E2B, see `test/live`). Uploads use UUID names in the staging directory. When a kill cannot run, the reboot fallback (pause + cold `connect`) also ends live siblings' commands (a last resort) |
59
+ | `destroy` / `destroyByName` | `DELETE /sandboxes/{id}` (a missing one also counts as success); by name it first lists by metadata |
60
+ | `exec` / file operations | First connect (a paused one is resumed, which is the contract's "auto-start"; the envd access token is fetched. **If no token can be obtained it does not connect and sends no request without a token; it reports `SandboxGoneError` (`resume` returns `gone`), and the kernel rebuilds a sandbox with a token on that basis; a sandbox created before `secure: true` was added never has a token**; the timeout is pushed to at least "command timeout + 120 seconds", and not beyond 24 hours), then calls envd; a connect that returns an E2B-format 404 (`{code: 404, …}`) → `SandboxGoneError`; a bare 404 (a proxy, a misconfigured `apiUrl`), 401 / 5xx / network errors → ordinary errors. pause / delete also treat only an E2B-format 404 as "does not exist" |
61
+
62
+ - **Timeout**: an E2B sandbox is handled when its timeout (`timeoutSeconds`) expires; this plugin sets `autoPause: true` when creating a sandbox, so on expiry it is **paused** instead of deleted,
63
+ and files are not lost even when the kernel has no chance to `suspend` (for example the whole kernel is down). Every use pushes it back through connect (it only extends, never shortens).
64
+ Idle hibernation and expiry destruction are handled by the kernel's maintenance tasks. E2B's continuous run limit (24 hours on Pro, 1 hour on Hobby) is recounted after pause / resume.
65
+ - **Hobby plan**: the `timeout` of connect / create is at most 1 hour, so the deployment config `timeoutSeconds` must be ≤ 3600, and a single command can actually run at most
66
+ about 1 hour minus 2 minutes; however large the `Bash` `timeout` (in milliseconds) is (capped by `sandbox.limits.maxCommandSeconds`, default 7200 seconds), it will be interrupted by the pause when the sandbox expires.
67
+ On Hobby, set `sandbox.limits.maxCommandSeconds` to below 3480.
68
+ - E2B keeps a paused sandbox indefinitely until it is deleted (the kernel deletes by `destroyAfterInactiveDays`).
69
+
70
+ ## exec and files
71
+
72
+ - **exec**: envd's `process.Process/Start` (a server stream of the Connect protocol), run as the `sandbox` user:
73
+ `setsid -w /bin/sh -c <wrapper> … timeout -s KILL <timeout + 5 seconds> /bin/bash -c <command>`; the wrapper first writes the session id to
74
+ `/tmp/.capybara-exec-<uuid>.pid`. The `timeout` inside the sandbox is the backstop: when a kernel replica crashes the timer on the plugin side is gone, disconnecting envd's stream does not end
75
+ the process, and pause then resume would bring the process back; on expiry it kills the process group the command is in.
76
+ On timeout or abort: a kill-processes script runs as the sandbox user, SIGKILLing all processes the command spawned by process group and session (the same as the Docker plugin),
77
+ then envd's `SendSignal` by pid serves as a backstop (it only ends the session's first process); it returns only after the kill finishes, and disconnects after a 3-second grace period (disconnecting itself does not
78
+ end the processes in the sandbox).
79
+ **Last resort: reboot the sandbox.** When the kill-processes script did not finish (could not start, for example process count exhausted, timeout, or unreachable), or the output stream still has not ended 3 seconds after the kill
80
+ (a process escaped the session and still holds the output): first pause, then `POST /v2/sandboxes/{id}/connect` `{timeout, memory: false}`
81
+ (that is, the SDK `e2b@2.58.0`'s `Sandbox.connect(id, { onResume: "reboot" })`) to cold-start from the disk state: all processes in memory end,
82
+ files such as `/workspace` are kept, writes not flushed before the pause may be lost, and whether things in `/tmp` survive depends on whether they are on disk. A warn log is written;
83
+ the call returns as usual with `timedOut: true` or the abort result. The SDK notes: this requires the control plane to support connect's `memory` field (E2B cloud; self-hosted must be a
84
+ `e2b-dev/runtime` after 2026-08-20), and an older control plane will **ignore the field** and restore the memory snapshot (processes still there), and an
85
+ environment without "restore only the filesystem" enabled will reject the request; in both cases the backstop is ineffective (only logged).
86
+ When a command is ended by a signal we did not send (envd's end event `exited: false`, such as OOM): by shell convention it returns `128 + signal number`
87
+ (when Go's signal name is recognized), and when it is not recognized it returns `exitCode: null, timedOut: false` (the kernel hints "the command did not finish, the result may be incomplete").
88
+ A timeout resolves `timedOut: true, exitCode: null`; an abort after start resolves `exitCode: null, timedOut: false` (with the output collected so far);
89
+ an already aborted call rejects immediately. stdout + stderr together are truncated by `maxOutputBytes` and set `truncated`; when truncating it backs up to a full
90
+ UTF-8 character boundary. envd sends a keepalive event every 50 seconds, so a command with no output for a long time is not disconnected by an intermediate proxy.
91
+ - **readFile / stat**: small `head -c` / `stat` scripts run in the sandbox as the sandbox user (permissions and symlinks are resolved as the sandbox user). A missing file → null,
92
+ and readFile on a directory also returns null.
93
+ - **writeFile**: envd's upload endpoint opens the target as root and follows symlinks, so writing to the target directly could bypass permissions with something like `/workspace/l -> /etc`.
94
+ So the plugin only transfers the bytes to a root-only staging directory `/opt/capybara/upload/<uuid>` (`/opt/capybara` owned by root, 0700), then runs as root
95
+ `runuser -u sandbox -- sh -c 'mkdir -p …; cat > target' < staging file`: root only opens the staging file, while creating the directory and writing the target are both resolved as the
96
+ sandbox user; the staging file is deleted whether it succeeds or not (when aborted it may remain in the root-only directory and is removed with the sandbox).
97
+ - **/skills is read-only**: `/skills` is owned by root (0755) and the sandbox user can only read it. `writeFile("/skills/…")` writes directly as root (`umask 022`,
98
+ file mode 0644); the sandbox user cannot place symlinks in `/skills`, so root's writes cannot be redirected elsewhere.
99
+
100
+ ## Egress (`limits.egress`)
101
+
102
+ E2B's egress control ([docs](https://docs.e2b.dev/sandbox/internet-access), verified 2026-10-21):
103
+
104
+ | `egress` | Setting when creating the sandbox |
105
+ |---|---|
106
+ | `none` | `allow_internet_access: false` (equivalent to `denyOut: ["0.0.0.0/0"]`) |
107
+ | `allowlist` | `network: { allowOut: allowHosts, denyOut: ["0.0.0.0/0"] }`; when `allowHosts` is empty it is the same as `none` |
108
+ | `open` | `allow_internet_access: true`; `allowOpenEgress` must be turned on explicitly |
109
+
110
+ Limitations of `allowlist` (decided by E2B's implementation, written here for assessment):
111
+
112
+ - Per-domain filtering applies only to **HTTP on port 80 (by the Host header) and TLS on port 443 (by SNI)**; other ports are filtered only by IP / CIDR,
113
+ and with only domains in the allowlist they are always blocked. UDP (QUIC / HTTP3) does not support per-domain filtering.
114
+ - When a domain allowlist is used, E2B automatically allows DNS to `8.8.8.8` (port 53), and the DNS queries themselves can be used to exfiltrate small amounts of data.
115
+ - **E2B looks only at the Host header / SNI, and the docs do not say it checks the destination IP**: a sandbox can connect to ports 80 / 443 of any IP and write the Host / SNI as a domain on the allowlist
116
+ (such as `curl --resolve pypi.org:443:<any IP> https://pypi.org`), and so **may reach any address outside the allowlist** (including the intranet and cloud
117
+ metadata addresses, depending on whether E2B's own network allows them). The Docker plugin's egress proxy resolves the domain and checks every destination IP; the E2B version cannot do this.
118
+ - Other sites on the same CDN / object storage may also be allowed along with it (the E2B docs say this too).
119
+ - **For self-hosted E2B, network isolation is the operator's responsibility**: intranet services and cloud metadata addresses (169.254.169.254 and so on) that the sandbox node can reach must be blocked at E2B's node /
120
+ network layer; this plugin cannot do it for you.
121
+ - If egress that "checks the destination IP after resolving" is wanted later, E2B's `network.egressProxy` can be used (SOCKS5, see SDK 2.58 `buildEgressProxyBody`):
122
+ it sends all sandbox egress to our own proxy (for example the same checks as the Docker plugin's egress proxy); this plugin does not use it yet.
123
+ - A rejected TCP connection may look "connected" inside the sandbox; you only know it was rejected from the application layer (HTTP status, TLS handshake).
124
+ - `*.example.com` matches subdomains (same as the Docker plugin, not including `example.com` itself).
125
+ - For self-hosted E2B that does not support per-domain filtering, set `domainAllowlist: false`: `allowlist` degrades to `none` with a warning (it never silently opens up).
126
+ - **The allowlist cannot stop data sent to allowlisted hosts** (the same as the Docker version): `git push` to github.com and publishing to npm / PyPI are within the allowlist,
127
+ and attachments in a conversation and data the agent has read can leak this way. Orgs that handle sensitive data should set `sandboxToolPolicy` to
128
+ `ask` in the org settings, and / or set the sandbox network `sandboxEgress` to `none` (org-level egress, Ruling M19: an org can replace the allowlist or cut the network, and cannot set `open`).
129
+
130
+ **Limits take effect only at creation**: `sandbox.limits.egress` / `allowHosts` are written into the create-sandbox request, and changing the config affects only sandboxes created afterwards; existing
131
+ sandboxes (including ones resumed after a pause) keep the egress settings from creation until they are destroyed and rebuilt.
132
+
133
+ **Command time limit**: a single command runs at most `sandbox.limits.maxCommandSeconds`, and no more than the time left in the run it belongs to (`runtime.maxRunSeconds`,
134
+ default 900 seconds, Ruling M20); to run long commands, raise `runtime.maxRunSeconds` to at least `maxCommandSeconds`.
135
+
136
+ ## Resources (`limits.cpu` / `memoryMb` / `diskMb`)
137
+
138
+ E2B's CPU and memory are fixed **when the template is built** (`cpuCount` / `memoryMB` of `Template.build`) and cannot be specified per sandbox at creation; disk size is decided by
139
+ E2B's plan / template. So for `limits` cpu / memoryMb / diskMb this plugin only validates them and **does not apply them per sandbox**: give the spec you want when
140
+ building the template (the example above matches the kernel defaults of 1 core / 2048 MB), and use different templates for different specs. The sandbox CPU / memory overrides an enterprise admin sets for an org in the admin console
141
+ (deployment setting `sandboxLimitOverrides`) likewise **have no effect on this plugin**: to give some orgs a bigger spec, the only way is to switch to a different deployment
142
+ (or template).
143
+
144
+ ## Client
145
+
146
+ It does not use the official `e2b` SDK and instead has a minimal hand-written client (`src/e2b-api.ts`, the global `fetch`, no third-party dependencies):
147
+
148
+ - Every request carries the caller's AbortSignal directly, so abort semantics are controllable (the command stream is disconnected by the plugin itself: kill the process first, then disconnect);
149
+ - The official SDK reads process environment variables such as `E2B_API_KEY` / `E2B_DOMAIN` / `E2B_SANDBOX_URL` / `E2B_DEBUG` and caches a connection pool at module level
150
+ (the multi-replica discipline does not allow module-level mutable state in a plugin), and it also brings undici, Connect, protobuf and a string of other dependencies;
151
+ - It uses only 6 control plane endpoints and 3 envd endpoints, with the protocol following the SDK `e2b@2.58.0` implementation (listed in the header comment of `src/e2b-api.ts`).
152
+
153
+ Protocol sources (verified 2026-10-21): `dist/index.mjs` of `e2b@2.58.0`; the E2B docs
154
+ [internet-access](https://docs.e2b.dev/sandbox/internet-access), [persistence](https://docs.e2b.dev/sandbox/persistence),
155
+ [sandbox lifecycle](https://docs.e2b.dev/sandbox), [template quickstart](https://docs.e2b.dev/template/quickstart),
156
+ [template migration (fromDockerfile)](https://docs.e2b.dev/template/migration-v2); the envd source
157
+ [upload.go](https://github.com/e2b-dev/infra/blob/main/packages/envd/internal/api/upload.go).
158
+
159
+ ## Test-only options
160
+
161
+ The third parameter of `createE2BSandbox(config, { log }, testOptions)` can be passed only by code that calls it directly; the plugin config cannot set it:
162
+
163
+ - `envdUrl`: the envd address is changed to this (routed by the `E2b-Sandbox-Id` request header), instead of being assembled from the sandbox id and domain.
164
+
165
+ ## Tests
166
+
167
+ - `test/sandbox-e2b.test.ts`: against `startFakeE2B` from `@capybara-hq/testing` (control plane API + envd, Connect stream protocol,
168
+ commands really run in a local temp directory), covering each method, error mapping (404 → gone, 401 / 5xx → ordinary errors), abort, kill on timeout,
169
+ file reads and writes and the way `/skills` is written, egress mapping, and the reboot backstop when killing fails / the output stream does not end (the fake server's `memory: false` connect ends all processes).
170
+ - `test/live`: real E2B, run only when `CAPYBARA_TEST_E2B_API_KEY` and `CAPYBARA_TEST_E2B_TEMPLATE` are set (optionally
171
+ `CAPYBARA_TEST_E2B_API_URL` / `CAPYBARA_TEST_E2B_DOMAIN`), otherwise skipped: the sandbox user, files, `/skills` read-only (including a write through a symlink being
172
+ rejected), the egress allowlist, output truncation, kill on timeout (including nohup child processes), abort, long periods with no output, a child process that escaped the session triggering the reboot backstop, same-name replacement,
173
+ pause / resume, gone after destroy. The file header lists the "assumptions that can only be confirmed on real E2B"; check them one by one on the first run.