@capybara-hq/plugin-sandbox-docker 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,144 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "sandbox-docker",
4
+ "version": "0.1.0-rc.1",
5
+ "publisher": "Capybara",
6
+ "description": "Conversation sandboxes (sandbox@1) as Docker containers (runc or gVisor runsc) behind a Docker socket proxy, with an egress allowlist proxy",
7
+ "displayName": {
8
+ "zh": "Docker 沙箱",
9
+ "en": "Docker sandbox"
10
+ },
11
+ "homepage": "https://github.com/capybara-hq/plugins/tree/main/plugins/sandbox-docker#readme",
12
+ "extension": {
13
+ "entry": "./dist/index.mjs",
14
+ "manifest": {
15
+ "name": "sandbox-docker",
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": "egressProxyImage",
33
+ "secret": false,
34
+ "hot": false
35
+ },
36
+ {
37
+ "path": "endpoint",
38
+ "secret": false,
39
+ "hot": false
40
+ },
41
+ {
42
+ "path": "network",
43
+ "secret": false,
44
+ "hot": false
45
+ },
46
+ {
47
+ "path": "runtime",
48
+ "secret": false,
49
+ "hot": false
50
+ },
51
+ {
52
+ "path": "volumeDriver",
53
+ "secret": false,
54
+ "hot": false
55
+ }
56
+ ],
57
+ "org": []
58
+ }
59
+ },
60
+ "configSchemas": {
61
+ "deployment": {
62
+ "type": "object",
63
+ "properties": {
64
+ "endpoint": {
65
+ "type": "string",
66
+ "pattern": "^(tcp:\\/\\/(\\[[^\\]]+\\]|[^:/]+):\\d{1,5}\\/?|unix:\\/\\/\\/.+)$",
67
+ "x-ui": {
68
+ "label": {
69
+ "zh": "Docker 代理地址",
70
+ "en": "Docker proxy endpoint"
71
+ }
72
+ },
73
+ "title": "Docker proxy endpoint"
74
+ },
75
+ "runtime": {
76
+ "default": "runc",
77
+ "type": "string",
78
+ "enum": [
79
+ "runc",
80
+ "runsc"
81
+ ],
82
+ "x-ui": {
83
+ "label": {
84
+ "zh": "容器运行时",
85
+ "en": "Container runtime"
86
+ }
87
+ },
88
+ "title": "Container runtime"
89
+ },
90
+ "network": {
91
+ "default": "capybara-sandbox",
92
+ "type": "string",
93
+ "pattern": "^cap[a-zA-Z0-9_.-]*$",
94
+ "x-ui": {
95
+ "label": {
96
+ "zh": "沙箱网络",
97
+ "en": "Sandbox network"
98
+ }
99
+ },
100
+ "title": "Sandbox network"
101
+ },
102
+ "egressProxyImage": {
103
+ "default": "capybara-sandbox-egress:latest",
104
+ "type": "string",
105
+ "minLength": 1,
106
+ "x-ui": {
107
+ "label": {
108
+ "zh": "出站代理镜像",
109
+ "en": "Egress proxy image"
110
+ }
111
+ },
112
+ "title": "Egress proxy image"
113
+ },
114
+ "volumeDriver": {
115
+ "type": "string",
116
+ "minLength": 1,
117
+ "x-ui": {
118
+ "label": {
119
+ "zh": "卷驱动",
120
+ "en": "Volume driver"
121
+ }
122
+ },
123
+ "title": "Volume driver"
124
+ },
125
+ "allowOpenEgress": {
126
+ "default": false,
127
+ "type": "boolean",
128
+ "x-ui": {
129
+ "label": {
130
+ "zh": "允许开放出站",
131
+ "en": "Allow open egress"
132
+ }
133
+ },
134
+ "title": "Allow open egress"
135
+ }
136
+ },
137
+ "required": [
138
+ "endpoint"
139
+ ],
140
+ "additionalProperties": false
141
+ }
142
+ }
143
+ }
144
+ }
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,144 @@
1
- # Temporary Holding Version
1
+ # @capybara-hq/plugin-sandbox-docker
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 Docker implementation of the conversation sandbox (`sandbox@1`): one container per conversation (`cap-<conversation id>`), with a runtime of either runc or gVisor's runsc.
4
+ The plugin **reaches Docker only through the endpoint** (the socket proxy in capybara-hq/core's [`docker/sandbox-proxy`](https://github.com/capybara-hq/core/tree/main/docker/sandbox-proxy)) and does not mount `docker.sock` itself.
5
+
6
+ ## Deployment config
7
+
8
+ | Key | Default | Description |
9
+ |---|---|---|
10
+ | `endpoint` | (required) | Socket proxy address: `tcp://sandbox-proxy:2375` or `unix:///path` |
11
+ | `runtime` | `runc` | `runc` or `runsc` (runsc must be registered in the Docker daemon first) |
12
+ | `network` | `capybara-sandbox` | Name of the shared sandbox network; it must start with `cap` (the socket proxy only allows such networks); if it does not exist the plugin creates it as a bridge with `enable_icc=false` (containers cannot reach each other) |
13
+ | `egressProxyImage` | `capybara-sandbox-egress:latest` | Egress proxy image (`egress-proxy/Dockerfile`) |
14
+ | `volumeDriver` | (unset, uses local) | Driver for the workspace volume; when set, `DriverOpts.size=<diskMb>M` is passed |
15
+ | `allowOpenEgress` | `false` | Allow `sandbox.limits.egress = "open"`; when false, `create` fails immediately on open (an English error naming `extensionConfig."sandbox-docker".allowOpenEgress`), and the kernel also logs a warn at startup |
16
+
17
+ ```bash
18
+ export CAPYBARA_EXTENSION_CONFIG='{"sandbox-docker":{"endpoint":"tcp://127.0.0.1:2375"}}'
19
+ ```
20
+
21
+ The deployment config schema is strict: extra keys are errors.
22
+
23
+ ## Images
24
+
25
+ - The sandbox image `image/Dockerfile` (shared by the Docker plugin and the E2B template). The tag matches the default of the kernel config `sandbox.image`:
26
+ `docker build -t capybara-sandbox:latest plugins/sandbox-docker/image`.
27
+ Debian trixie-slim (pinned by digest), with bash, coreutils, GNU find / grep (the kernel's `Glob` / `Grep`
28
+ depend on them), procps, util-linux (`setsid`), curl, git, jq, python3 (with pip / venv) and so on; it runs as the non-root user `sandbox`
29
+ (uid 1000), with HOME and the working directory at `/workspace`.
30
+ - The egress proxy image `egress-proxy/Dockerfile`: `docker build -t capybara-sandbox-egress:latest plugins/sandbox-docker/egress-proxy`.
31
+ - Multi-arch (amd64 + arm64): `docker buildx build --platform linux/amd64,linux/arm64 -t <registry>/<name>:<tag> --push <directory>`.
32
+ - Local development: in a [capybara-hq/core](https://github.com/capybara-hq/core) checkout, `docker compose -f docker/compose.dev.yaml --profile sandbox up -d` ([`docker/compose.dev.yaml`](https://github.com/capybara-hq/core/blob/main/docker/compose.dev.yaml)) builds both images and starts the socket proxy
33
+ (`127.0.0.1:2375`, **for local development only**, see the deployment requirements below). If an image is not local the plugin tries to pull it once.
34
+ - **In a deployment, pre-pull `sandbox.image` and `egressProxyImage` on every Docker node** (`docker pull …`): if an image is not local
35
+ the plugin pulls it only when a conversation first uses the sandbox, and a pull of several hundred MB eats into the time limit of that tool call (`tools.timeoutMs` plus the lock wait and a 3-minute margin);
36
+ when it is slow that call fails, and the next one tries again.
37
+
38
+ ## Socket proxy (`docker/sandbox-proxy/haproxy.cfg`)
39
+
40
+ The proxy and its config live in capybara-hq/core: [`docker/sandbox-proxy/haproxy.cfg`](https://github.com/capybara-hq/core/tree/main/docker/sandbox-proxy/haproxy.cfg).
41
+
42
+ HAProxy (the official image, pinned by digest); the proxy runs as root (to read the socket). Two checks:
43
+
44
+ 1. **Paths and names**: only the endpoints the plugin actually calls are allowed (container create / inspect / start / stop / kill / restart / unpause / remove,
45
+ exec create / start / inspect, volume create / inspect / remove, network create / inspect / remove, image inspect / pull,
46
+ `_ping` / `version`), everything else gets 403. The query string of an image pull (`POST /images/create`) may only be a single `fromImage`: `fromSrc`
47
+ (importing an arbitrary filesystem from a URL or the request body), `repo` / `tag` (renaming to overwrite an image), `message` and `changes` always get 403. Containers and volumes accept only names starting with `cap-` (creating a container requires `?name=cap-…`), and networks accept only names starting with `cap`;
48
+ the plugin always accesses containers by name and never by id. Exec instances (`/exec/{id}/…`) cannot be filtered by name, but creating an exec already requires
49
+ a `cap-` container.
50
+ 2. **Request body (defense in depth)**: an endpoint with a body must be JSON, carry Content-Length, be ≤ 32 KiB and be fully buffered (a body larger than the buffer
51
+ is always rejected, so an oversized body cannot be used to bypass it). Rejected (case-insensitively): privileged, keys such as `Binds` / `CapAdd` / `Devices` /
52
+ `VolumesFrom` / `CgroupParent` / `Sysctls` / `Annotations` / `MaskedPaths` / `PortBindings`, mount types other than `volume` /
53
+ `tmpfs`, mounts whose `Source` does not start with `cap-` (the kernel's or another service's volumes cannot be mounted), host / container namespace modes, a `SecurityOpt` other than `no-new-privileges`, runtimes other than runc / runsc,
54
+ networks other than `none` / `cap…`, any `\u` escape and non-ASCII byte (Go's JSON decoding folds key names by Unicode,
55
+ so `"Bindſ"` would be treated as `Binds`; an exec command may contain Chinese, and only `\u` and non-ASCII in key names are rejected), duplicate keys that would bypass a "must be a given value" check;
56
+ a volume cannot carry the local driver's `device` / `o` / `type` (which could make it a bind), and a network can only be bridge. The create container / volume /
57
+ network request bodies the plugin generates itself are all ASCII, and a non-ASCII value (image names, tags and allowlist host names must use punycode) is reported as an English error in the plugin itself.
58
+ **This is only a regex backstop, not JSON parsing; the main line of defense is network position** (see the deployment requirements below).
59
+
60
+ **Deployment requirement**: whoever can reach the proxy can almost control Docker (the second check is only a backstop). **Never publish it on any host address**, including
61
+ `127.0.0.1`: on Docker Desktop a container on an ordinary bridge network can reach the host loopback through `host.docker.internal`; on Linux
62
+ the bridge gateway address can also reach ports the host listens on. In production, put it only on the internal network the kernel is on, with no port mapping, and do not attach sandboxes to that network.
63
+ To let the local kernel connect, the dev compose publishes it on `127.0.0.1:2375`: use that for local development only, and do not turn on `allowOpenEgress`.
64
+
65
+ ## Lifecycle
66
+
67
+ | Operation | What it does |
68
+ |---|---|
69
+ | `create` | First deletes any old instance of the same name (the kernel may have exited before recording the handle), then creates the volumes, (for allowlist) the internal network and egress proxy, and the sandbox container, and starts it; the handle id is the name |
70
+ | `suspend` | Stops the sandbox container and the egress proxy (files stay on the volumes). **Processes are not preserved**: all background processes in the sandbox end and `/tmp` (tmpfs) is cleared; E2B's pause, in contrast, keeps memory too |
71
+ | `resume` | Starts (or unpauses one that was `docker pause`d); if the container does not exist it returns `gone`. If the allowlist's egress proxy or internal network is gone, it is rebuilt from the allowlist recorded in the container labels |
72
+ | Auto-start | When `exec` / `readFile` / `writeFile` / `stat` / `killAll` find the container still exists but is not running (Docker returns 409: the host or daemon restarted, the idle sweep stopped the container before it could record `suspended`, someone ran `docker pause` on it…), they recover it the way `resume` does (egress proxy and network in place; unpause a paused one, start a stopped one) and retry once (the `sandbox@1` contract: the kernel trusts only the `running` it recorded itself) |
73
+ | `killAll` | Kills every command started through `exec` that is still running (finds the wrapper by command line and sends SIGKILL by process group and session); called when the kernel takes over from a crashed previous lock holder. A process that a command puts in the background in a session of its own (`setsid`) is not guaranteed to be killed |
74
+ | Concurrent use (`concurrentExec`, `killTagged`) | Declares `concurrentExec: true`: parallel sub-agents of one conversation run commands and file operations in its sandbox at the same time (independent `docker exec` instances; the socket proxy rules are unchanged). Each exec's control file carries the holder's tag (`/tmp/.capybara-exec-<tag>-<uuid>`, tags checked against `SANDBOX_TAG_RE`); `killTagged` kills only the commands of the given tags (a variant of the `killAll` script), used when the kernel takes over from a crashed holder while others keep running. Concurrent auto-starts of a stopped container are idempotent (`start` answers 304; a concurrent egress proxy create answers 409, tolerated). Concurrent commands share the container's `PidsLimit` (1024) and memory. When the kill exec itself cannot start, the container restarts, which also ends live siblings' commands (a last resort) |
75
+ | `destroy` / `destroyByName` | Deletes the container, egress proxy, internal network and both volumes; a missing one also counts as success |
76
+ | `exec` | `docker exec` (sandbox user, bash); on timeout or abort it SIGKILLs the command and all processes it spawned by session. Commands run through `timeout -s KILL <timeout + 5 seconds>`: when a kernel replica crashes (the timer on the plugin side is gone, and a dropped connection does not end the command) the time limit inside the sandbox is the backstop. When the exec used to kill processes cannot itself start (for example the process count has hit its limit), it falls back to restarting the whole container (`restart?t=0`): files are all on the volumes, `/tmp` is cleared, and the remaining processes in the sandbox all end. When output is truncated it backs up to a full UTF-8 character boundary |
77
+ | `readFile` / `writeFile` / `stat` | Run small `head` / `stat` scripts in the container as the sandbox user (permissions and symlinks are resolved as the sandbox user, and the volume is not written as root through the daemon) |
78
+
79
+ Resources for each conversation: the container `<name>`, the egress proxy `<name>-egress`, the internal network `<name>-net`, and the volumes `<name>-workspace`
80
+ (`/workspace`) and `<name>-skills` (`/skills`). All carry the labels `dev.capybara.sandbox=1` and `dev.capybara.sandbox.name=<name>`.
81
+
82
+ ## Isolation and limits
83
+
84
+ - Commands and file operations run as non-root (uid 1000); the container's main processes (`Init`'s tini and `sleep infinity`) run as root
85
+ (`User: 0:0`, with all capabilities dropped too): the sandbox user cannot signal them, and `kill 1`, `pkill sleep` and `kill -9 -1` cannot stop
86
+ the container. Every exec specifies the user explicitly; a manual `docker exec` without `-u` runs as root (with no capabilities).
87
+ - `CapDrop: ALL`, `no-new-privileges`, a read-only root filesystem, `Init` (reaps zombie processes), `PidsLimit` 1024, private IPC.
88
+ - cpu → `NanoCpus`; memory → `Memory` (`MemorySwap` the same value, so no swap is used).
89
+ - `/tmp` is tmpfs (`nosuid,nodev`), sized at half the memory and no more than `diskMb`, and counts against the memory quota; it is cleared after suspension.
90
+ - `/skills` is **mounted read-only**; the same volume is also mounted writable at `/opt/capybara/skills` (`/opt/capybara` is owned by root, 0700,
91
+ so the sandbox user cannot enter it, nor get in through a symlink), and the plugin's `writeFile("/skills/…")` writes through it as root inside the container.
92
+ - **Disk**: the root filesystem is read-only, and all writes land on the workspace volume and `/tmp`. `--storage-opt size` limits only the container's writable layer,
93
+ which is meaningless for a read-only root filesystem, so it is not used. The workspace volume size takes effect only when `volumeDriver` is set and that driver supports the `size` option;
94
+ **the default local driver does not limit volume size** (`diskMb` has no effect). When you need a hard limit, use a volume driver that supports quotas, or configure an XFS project quota and the like
95
+ on the Docker volume directory on the host.
96
+
97
+ **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`,
98
+ default 900 seconds, Ruling M20). To run long commands, raise `runtime.maxRunSeconds` to at least `maxCommandSeconds`.
99
+
100
+ **Limits take effect only at creation**: the `cpu` / `memoryMb` / `diskMb` / `egress` / `allowHosts` of `sandbox.limits` are written into the container,
101
+ volumes and egress proxy (the allowlist is recorded in the container labels); changing the config affects only sandboxes created afterwards; existing sandboxes (including ones resumed after hibernation) keep the
102
+ limits from creation until they are destroyed and rebuilt (destroyed for inactivity, or by deleting the `cap-<conversation>` family of resources).
103
+
104
+ ## Egress (`limits.egress`)
105
+
106
+ - `none`: `NetworkMode: none`, loopback only.
107
+ - `open`: attached to the shared network `network` (an ordinary bridge), and **can reach everything the host network can reach**: services listening on the host (on Docker
108
+ Desktop it reaches the host loopback through `host.docker.internal`, so a socket proxy published on 127.0.0.1 can also be reached by it), the intranet, and
109
+ the cloud metadata address. The deployment config `allowOpenEgress` must be turned on explicitly (off by default); use it only in trusted environments.
110
+ - `allowlist` (default): the sandbox joins only an `Internal` internal network, on which there is only its egress proxy; the proxy's other leg joins the shared network.
111
+ `HTTP_PROXY` / `HTTPS_PROXY` (and the lowercase ones) are already set, and `NO_PROXY` is loopback. The proxy (`egress-proxy/proxy.mjs`, dependency-free Node):
112
+ - A host name matches `allowHosts` exactly, or `*.example.com` matches any subdomain (**not** `example.com` itself); IP literals are never allowed;
113
+ - It resolves first, and **every resolved IP** must not be a private, loopback, link-local (including 169.254.169.254), CGNAT (100.64/10), ULA
114
+ (fc00::/7), unspecified, multicast, reserved or documentation address (IPv4-mapped / NAT64 / 6to4 are judged by the embedded IPv4), otherwise 403; this blocks
115
+ DNS rebinding (such as `10.0.0.1.nip.io`); it then connects directly to the IP it checked, without resolving a second time;
116
+ - HTTPS goes through CONNECT (only ports 443 and 80 are allowed; the proxy container's environment variable `ALLOW_CONNECT_PORTS` can change this), and HTTP is forwarded by absolute URL;
117
+ when the first address cannot be reached it tries the other checked addresses in turn; a direct connection that bypasses the proxy does not work (the internal network has no exit).
118
+ - The internal network has no default route, and Docker's built-in DNS does not resolve external domains for it either (verified on Docker 29); protocols that do not go through the HTTP proxy
119
+ (ssh, git://) are unavailable.
120
+ - Known limitation: on Linux hosts with older Docker, a container in the internal network may be able to reach services the host listens on at that bridge address;
121
+ do not have host services listen on 0.0.0.0, or use a firewall to block inbound traffic from the sandbox subnet.
122
+
123
+ **What the allowlist guards against and what it does not**: it blocks the intranet, the host and the cloud metadata address, and also hosts outside the allowlist; but it **cannot stop data being sent to a host
124
+ on the allowlist**: `git push` to github.com, publishing to npm / PyPI, and uploading to GitHub APIs other than the raw / release addresses
125
+ are all within the allowlist. The proxy sees only the host name and destination IP, not the request content. Attachments in a conversation and data the agent has read can all leak this way.
126
+ For orgs that handle sensitive data: in the org settings set `sandboxToolPolicy` to `ask` (execute, write and deliver need approval), and / or set the sandbox network
127
+ `sandboxEgress` to `none`; at the deployment level you can also narrow `sandbox.limits.allowHosts`.
128
+
129
+ **Org-level egress** (org setting `sandboxEgress`, Ruling M19): an org can swap in its own allowlist (replacing the deployment's list) or cut the network; at
130
+ `create` the kernel passes the org's effective `egress` / `allowHosts` to the plugin; an org cannot set `open`, and when the deployment is `none` the org can only be `none` too.
131
+
132
+ ## Test-only options
133
+
134
+ The second parameter of `createDockerSandbox(config, testOptions)` can be passed only by code that calls it directly; the plugin config cannot set it:
135
+
136
+ - `labels`: adds extra labels to all resources (tests clean up by label);
137
+ - `egressTestHosts`: the egress proxy pins these host names to the given IPs and exempts them from the private-address check (they still must be on the allowlist),
138
+ so that a local HTTP service in a test can serve as an allowed target. It is passed in through the proxy container's environment variable `CAPYBARA_EGRESS_TEST_HOSTS`.
139
+
140
+ ## Tests
141
+
142
+ `test/egress-proxy.test.ts` does not need Docker; `test/sandbox-docker.test.ts` needs Docker (the whole group is skipped without it),
143
+ builds a uniquely tagged test image, starts the socket proxy, and cleans up by label at the end; the runsc cases run only when the daemon has runsc registered.
144
+ Through a real kernel: `test/sandbox-tools.smoke.test.ts` (the kernel's sandbox tools in one process) and `test/sandbox-artifacts.dual.test.ts` (the M5 segment 2 flow across two replicas: attachments, delivery, takeover after a killed replica, idle suspend and resume). The socket proxy starts from `test/sandbox-proxy/haproxy.cfg`, an exact copy of capybara-hq/core's [`docker/sandbox-proxy/haproxy.cfg`](https://github.com/capybara-hq/core/tree/main/docker/sandbox-proxy/haproxy.cfg). Nothing in this repository compares them; core's `check:bootstrap --network` (rule `bootstrap-haproxy`) will check the copy against the commit core pins.