agent-container-sandbox 0.1.1__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konovor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,334 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-container-sandbox
3
+ Version: 0.1.1
4
+ Summary: Docker container sandbox for LangChain Deep Agents, with the container lifecycle kept separate from the sandbox.
5
+ Keywords: langchain,deepagents,docker,sandbox,agents
6
+ Author: Konovor
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: deepagents>=0.7.10,<0.8
17
+ Requires-Dist: docker>=7.2.0
18
+ Requires-Python: >=3.11
19
+ Project-URL: Homepage, https://github.com/Konovor/agent-container-sandbox
20
+ Project-URL: Issues, https://github.com/Konovor/agent-container-sandbox/issues
21
+ Description-Content-Type: text/markdown
22
+
23
+ # agent-container-sandbox
24
+
25
+ Docker container sandbox for [LangChain Deep Agents](https://github.com/langchain-ai/deepagents): run commands and move files inside a container, with the container lifecycle kept separate from the sandbox itself.
26
+
27
+ - **`ContainerSandbox`** is a thin adapter around a container you already have. It runs commands and transfers files. It never creates, stops, or removes containers.
28
+ - **`ContainerProvisioner`** creates, finds, and deletes containers. It never runs your commands.
29
+ - **`ContainerSpec`** is an immutable description of the container you want.
30
+ - **`ManagedContainerSandbox`** is a context manager for scripts. It provisions a container, hands you a `ContainerSandbox`, and always deletes the container when the `with` block ends.
31
+
32
+ You own the Docker client. The library never closes it. You also own the container lifecycle, unless you hand it to `ManagedContainerSandbox`.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install agent-container-sandbox
38
+ ```
39
+
40
+ or with uv: `uv add agent-container-sandbox`. To install from git instead: `uv add git+https://github.com/Konovor/agent-container-sandbox@v0.1.1`.
41
+
42
+ **Prerequisites**
43
+
44
+ - Docker installed and running. Podman works through its Docker-compatible socket: point `DOCKER_HOST` at it, and `docker.from_env()` picks it up.
45
+ - An image that provides `sh`, `sleep`, and `timeout` (GNU coreutils). Very minimal images (for example `scratch`-based ones) will not work.
46
+ - `python3` in the image, if you use the file helpers inherited from `BaseSandbox` (`read`, `write`, `edit`, `ls`, `glob`). They run Python inside the container. Without it, `read`, `write`, `edit` and `glob` return an error, but `ls` returns an empty list instead of an error. `grep`, `execute`, `upload_files` and `download_files` do not need it (observed with deepagents 0.7.19 and 0.7.32). `ManagedContainerSandbox` checks for `python3` by default and fails early if it is missing.
47
+
48
+ The recommended image meets all of this:
49
+
50
+ ```bash
51
+ docker pull ghcr.io/astral-sh/uv:0.10.0-python3.13-trixie-slim
52
+ ```
53
+
54
+ Pulling beforehand is optional: the provisioner pulls a missing image automatically (this needs registry access and can make the first `provision()` slow). If the pull fails, it raises `DockerImageNotFound`.
55
+
56
+ ## Quick start
57
+
58
+ ```python
59
+ import docker
60
+
61
+ from agent_container_sandbox import ContainerSpec, ManagedContainerSandbox
62
+
63
+ IMAGE = "ghcr.io/astral-sh/uv:0.10.0-python3.13-trixie-slim"
64
+
65
+ client = docker.from_env()
66
+ try:
67
+ with ManagedContainerSandbox(client, ContainerSpec(image=IMAGE)) as sandbox:
68
+ result = sandbox.execute("echo 'Hello from your sandbox!'")
69
+ print(result.output) # Hello from your sandbox!
70
+ print(result.exit_code) # 0
71
+ finally:
72
+ client.close()
73
+ ```
74
+
75
+ The container is deleted when the block ends, even if the body raises. You still close the client yourself.
76
+
77
+ Pass any `docker.DockerClient`; see the docker-py documentation for remote and TLS setups.
78
+
79
+ ### Manual lifecycle
80
+
81
+ Use the provisioner and sandbox directly when you need reuse, want to leave the container running, or need the `Container` object:
82
+
83
+ ```python
84
+ import docker
85
+
86
+ from agent_container_sandbox import ContainerProvisioner, ContainerSandbox, ContainerSpec
87
+
88
+ IMAGE = "ghcr.io/astral-sh/uv:0.10.0-python3.13-trixie-slim"
89
+
90
+ client = docker.from_env()
91
+ provisioner = ContainerProvisioner(client)
92
+ container = provisioner.provision(ContainerSpec(image=IMAGE, name="my-sandbox"))
93
+
94
+ try:
95
+ sandbox = ContainerSandbox(container)
96
+ print(sandbox.execute("echo 'Hello from your sandbox!'").output)
97
+ finally:
98
+ provisioner.delete(container)
99
+ client.close()
100
+ ```
101
+
102
+ Always delete the container in a `finally` block. If your process crashes, `max_lifetime_seconds` (see below) is the safety net.
103
+
104
+ ## Using it with an agent
105
+
106
+ Any LangChain chat model works. This example uses Google Gemini, which requires `langchain-google-genai` and a `GOOGLE_API_KEY`.
107
+
108
+ ```python
109
+ from deepagents import create_deep_agent
110
+ from langchain.chat_models import init_chat_model
111
+
112
+ # `sandbox` is the ContainerSandbox from the snippets above
113
+ agent = create_deep_agent(
114
+ model=init_chat_model("google_genai:gemini-3.5-flash-lite"),
115
+ system_prompt="You are a coding assistant with sandbox access.",
116
+ backend=sandbox,
117
+ )
118
+ result = agent.invoke(
119
+ {"messages": [{"role": "user", "content": "Write and run a script for Armstrong numbers"}]}
120
+ )
121
+ ```
122
+
123
+ The container has no network by default, so tasks that need `pip install`, `git clone` or web requests require `ContainerSpec(image=IMAGE, network_mode="bridge")`.
124
+
125
+ For plain LangChain agents, pass the sandbox through `FilesystemMiddleware(backend=sandbox)` from `deepagents.middleware.filesystem`.
126
+
127
+ With `ManagedContainerSandbox`, keep the agent call inside the `with` block. The container is gone afterwards.
128
+
129
+ ### Direct usage
130
+
131
+ `ContainerSandbox` inherits the file helpers from `BaseSandbox` (`read`, `write`, `ls`, `edit`, and so on) and adds binary transfer:
132
+
133
+ ```python
134
+ sandbox.write("/workspace/test.txt", "Hello, World!")
135
+
136
+ sandbox.upload_files([("data.bin", b"\x00\x01\x02")]) # relative paths resolve to working_dir
137
+ files = sandbox.download_files(["data.bin"])
138
+ print(files[0].content) # b'\x00\x01\x02'
139
+
140
+ result = sandbox.execute("sleep 10", timeout=2)
141
+ print(result.exit_code) # 124
142
+ ```
143
+
144
+ `upload_files` and `download_files` are intended for the developer, not the model: you control what enters the sandbox and what you extract.
145
+
146
+ ## Container lifecycle
147
+
148
+ ### `ContainerSpec`
149
+
150
+ | Field | Type | Default | Description |
151
+ |-------|------|---------|-------------|
152
+ | `image` | `str` | required | Image name. Pulled automatically if missing locally. |
153
+ | `name` | `str \| None` | `None` | Container name. If omitted, a unique name is generated. Required for `find` and `get_or_create`, where it is the lookup key. |
154
+ | `volumes` | `dict` | `{}` | Mounts, e.g. `{"/host": {"bind": "/container", "mode": "rw"}}`. |
155
+ | `working_dir` | `str` | `/workspace` | Working directory. Created if missing. |
156
+ | `max_lifetime_seconds` | `int` | `3600` | Hard upper bound on the container's life (see below). |
157
+ | `labels` | `dict` | `{}` | Extra labels. The marker label below and a spec fingerprint label are always added and cannot be overridden. |
158
+ | `mem_limit` | `str \| None` | `"2g"` | Memory cap. Swap is capped at the same value. `None` = unlimited. |
159
+ | `cpus` | `float \| None` | `2.0` | CPU cap in cores. `None` = unlimited. |
160
+ | `network_mode` | `str \| None` | `"none"` | Container network mode. `"none"` disables networking. Use `"bridge"` for normal network access. `"host"` shares the host's network stack and removes the isolation. `None` sends nothing, so the daemon's default applies (usually full network access). |
161
+ | `pids_limit` | `int \| None` | `512` | Maximum processes and threads. `None` = unlimited. |
162
+ | `no_new_privileges` | `bool` | `True` | Block privilege gain through setuid binaries. |
163
+ | `cap_drop_all` | `bool` | `True` | Drop all Linux capabilities. |
164
+ | `cap_add` | `tuple[str, ...]` | `()` | Capabilities to add back after dropping, e.g. `("CHOWN", "DAC_OVERRIDE")`. |
165
+
166
+ All fields are keyword-only: write `ContainerSpec(image=IMAGE)`, not `ContainerSpec(IMAGE)`.
167
+
168
+ ### `ContainerProvisioner`
169
+
170
+ | Method | Description |
171
+ |--------|-------------|
172
+ | `provision(spec)` | Create and start a new container. Cleans up after itself if setup fails. |
173
+ | `find(name)` | Return the container with exactly this name, or `None`. |
174
+ | `get_or_create(spec)` | Return the running container created from this spec, looked up by `spec.name`, or provision one. Raises `SpecMismatchError` or `ContainerNotRunningError` if the existing container does not match or is not running. |
175
+ | `delete(container)` | Stop and remove a container. Tolerates it already being gone. |
176
+
177
+ **Reusing containers.** `get_or_create` lets several `ContainerSandbox` objects share one container, or attach to one that another process started from the same spec. It looks the container up by `spec.name` and hands it out only if it carries the library's marker label and the fingerprint of this exact spec (every field except `name`), and is running. Otherwise it raises and leaves the container as it is: nothing is started, stopped, or deleted. The error carries the container as `e.container`, so you can recover without a second lookup:
178
+
179
+ ```python
180
+ from agent_container_sandbox import ContainerNotRunningError, SpecMismatchError
181
+
182
+ try:
183
+ container = provisioner.get_or_create(spec)
184
+ except ContainerNotRunningError as e:
185
+ e.container.start() # or provisioner.delete(e.container), then get_or_create(spec) again
186
+ container = e.container
187
+ except SpecMismatchError as e:
188
+ # e.reason: "not_created_by_library", "no_fingerprint" or "different_spec"
189
+ provisioner.delete(e.container) # only if you are sure it is unused
190
+ container = provisioner.get_or_create(spec)
191
+ ```
192
+
193
+ A container that is not running is usually one whose `max_lifetime_seconds` has expired. A `SpecMismatchError` means the container is not the one your spec describes, so check what it is before you delete it. Catch `ContainerReuseError` to handle both failures in one place.
194
+
195
+ `find()` does not verify the container, and `get_or_create()` does. `ContainerSandbox` accepts any running container as it is and does not check its configuration, so the hardening only applies to containers created from a `ContainerSpec`. A sandbox over a container that did not come from your spec uses that container's own working directory, which may differ from your spec. `ManagedContainerSandbox` does not reuse containers.
196
+
197
+ ### `ManagedContainerSandbox`
198
+
199
+ A convenience layer over `ContainerProvisioner` and `ContainerSandbox`. It adds no capability of its own.
200
+
201
+ ```python
202
+ with ManagedContainerSandbox(client, spec) as sandbox:
203
+ ... # sandbox is a ContainerSandbox
204
+ ```
205
+
206
+ | Parameter | Type | Default | Description |
207
+ |-----------|------|---------|-------------|
208
+ | `client` | `DockerClient` | required | Your Docker client. Never closed by this class. |
209
+ | `spec` | `ContainerSpec` | required | The container to provision. If `spec.name` is `None`, a unique name is generated. |
210
+ | `working_dir` | `str \| None` | container's configured working directory | Passed to `ContainerSandbox`. |
211
+ | `command_timeout` | `int` | `150` | Passed to `ContainerSandbox`. Must be positive. |
212
+ | `max_output_size` | `int` | `65536` | Passed to `ContainerSandbox`. Must be positive. |
213
+ | `verify_timeout` | `bool` | `True` | Run `verify_timeout()` on entry, so an image without a usable `timeout` binary fails early. |
214
+ | `verify_python` | `bool` | `True` | Run `verify_python()` on entry, so an image without `python3` fails early. Set it to `False` if you only use `execute` and file transfer. |
215
+
216
+ - **Cleanup is guaranteed.** The container is deleted on exit, including when the body raises. If setup fails after the container was created (for example `MissingTimeoutError` or `MissingPythonError`), it is deleted before the exception propagates.
217
+ - **Errors are never masked.** If deletion itself fails, a warning is logged and your original exception still propagates. The container then stops on its own after `max_lifetime_seconds`.
218
+ - **Single-use.** Entering the same instance twice raises `RuntimeError`. Create a new instance.
219
+ - **Fixed names can collide.** With an explicit `spec.name`, entering while a container of that name still exists fails with a docker-py error. Omit the name unless you need it.
220
+
221
+ ### How the container is kept alive
222
+
223
+ The provisioner starts the container with a placeholder process, `sleep <max_lifetime_seconds>`, and real work arrives later through `exec_run`. When the sleep ends, the container stops on its own, even if your Python process crashed. This is a maximum lifetime, not an idle timeout (Docker has no idle TTL), so raise `max_lifetime_seconds` for long sessions.
224
+
225
+ ### Hardening
226
+
227
+ Containers are hardened by default, to limit the damage an agent can do to the host by accident or by mistake. Every setting is a `ContainerSpec` field.
228
+
229
+ | Setting | Default | Protects against | What the agent sees when it hits the limit |
230
+ |---------|---------|------------------|--------------------------------------------|
231
+ | `mem_limit` (plus equal swap cap) | `2g` | A runaway allocation exhausting host memory | The process is killed: `Killed`, exit code `137` |
232
+ | `pids_limit` | `512` | Fork bombs exhausting the host's process table | `Cannot fork` errors from the shell |
233
+ | `cpus` | `2.0` | One container starving the rest of the host | Commands run slower, so they reach their timeout sooner |
234
+ | `network_mode` | `none` | Reaching your LAN, cloud metadata endpoints, and the internet | Name-resolution or connection errors, for example `Temporary failure in name resolution` |
235
+ | `no_new_privileges` | on | Privilege escalation through setuid binaries such as `sudo` | `sudo` and similar tools stop working |
236
+ | `cap_drop_all` | on | Kernel attack surface reachable through Linux capabilities | `Permission denied` on some root-only operations |
237
+
238
+ Mention these limits in your system prompt or tool description, so the model can tell an exceeded limit from a bug in its own code.
239
+
240
+ **Adjusting the defaults.** Set a field to `None` or `False` to remove a restriction, for example `ContainerSpec(image=IMAGE, mem_limit="8g", pids_limit=None)`. `pids_limit=None` sends an explicit unlimited value, so a daemon-wide default (Podman applies 2048, for example) cannot apply. For the network, `network_mode="bridge"` allows access, which reaches your LAN, cloud metadata endpoints, and the internet, and `None` leaves the daemon's default. `"host"` shares the host's network stack and removes the isolation entirely. With all capabilities dropped, root inside the container loses some abilities. The most likely surprise is writing to a mounted host directory owned by another user, which needs `DAC_OVERRIDE`. `apt-get` needs `SETUID`, `SETGID`, and `CHOWN`, plus a `network_mode` that allows network access. Add back only what you need with `cap_add=("CHOWN", "DAC_OVERRIDE")`.
241
+
242
+ **Verifying the setup.** Confirm that Docker applied the settings:
243
+
244
+ ```python
245
+ sandbox.execute("grep -E 'CapEff|NoNewPrivs' /proc/self/status")
246
+ # CapEff: 0000000000000000, NoNewPrivs: 1 (with the default empty cap_add)
247
+ ```
248
+
249
+ ### Finding orphans
250
+
251
+ Every container created by this library carries the label `created-by=agent-container-sandbox`, even if you pass no labels. To list leftovers:
252
+
253
+ ```bash
254
+ docker ps -a --filter label=created-by=agent-container-sandbox
255
+ ```
256
+
257
+ ## Execution behavior
258
+
259
+ ### `ContainerSandbox`
260
+
261
+ | Parameter | Type | Default | Description |
262
+ |-----------|------|---------|-------------|
263
+ | `container` | `Container` | required | A running container. You own its lifecycle. |
264
+ | `working_dir` | `str \| None` | container's configured working directory (`/` if none) | Directory for commands and relative paths. If omitted, it is read from the container's configuration, which for provisioned containers is `ContainerSpec.working_dir`. If you pass it explicitly, it must already exist. |
265
+ | `command_timeout` | `int` | `150` | Default per-command timeout in seconds. Must be positive. |
266
+ | `max_output_size` | `int` | `65536` | Output beyond this many bytes is truncated. Must be positive. |
267
+
268
+ A non-positive `command_timeout` or `max_output_size` raises `ValueError` in the constructor, before any call to Docker. `execute()` returns the deepagents `ExecuteResponse` (`output`, `exit_code`, `truncated`).
269
+
270
+ - **`timeout` must be positive.** A per-call `timeout` of zero or less is rejected without running anything: `execute()` returns exit code `1` and a `Sandbox error:` message that names the value. Zero does not mean "no limit" here. For long-running commands, pass a large value instead.
271
+ - **Timeouts are enforced inside the container.** Each command runs as `timeout -k 2 N sh -c <command>`: SIGTERM at the deadline, SIGKILL 2 seconds later if the process is still running. The exit code is `124` after a normal timeout. It is `137` when SIGKILL was needed, which is also the code for an out-of-memory kill. Busybox-based images such as Alpine report `143` instead, without the "timed out" note. Processes that detach (`setsid`, double fork) can outlive `timeout`. Deleting the container is the only hard guarantee.
272
+ - **Docker-level hangs are reported separately.** If Docker itself does not answer within the timeout plus 7 seconds, you get exit code `124` and a message that the command's state is unknown and the sandbox may be unhealthy. The command may still be running.
273
+ - **Output is truncated by bytes**, without splitting a UTF-8 character, and `truncated` is set to `True`. Output is read fully into memory first, so avoid commands that print gigabytes.
274
+ - **Errors raised by the sandbox itself** are prefixed with `Sandbox error:` in `output`, to tell them apart from errors printed by your command.
275
+
276
+ `download_files` reports problems per file in `FileDownloadResponse.error`: `"file_not_found"`, `"is_directory"`, or a message string for anything else. A container that no longer exists produces a message string, not `"file_not_found"`. `upload_files` reports failures in `FileUploadResponse.error`. Neither raises for a single bad file.
277
+
278
+ `upload_files` never replaces an existing directory. If the target path is a directory, that file gets a per-file error and the directory is left untouched.
279
+
280
+ ### Checking a container
281
+
282
+ Every `execute()` call needs `timeout` inside the container. `ContainerProvisioner` does not check for it, because its own keep-alive command does not use it. If your image might lack `timeout`, or you hand `ContainerSandbox` a container you did not provision, check once up front:
283
+
284
+ ```python
285
+ sandbox = ContainerSandbox(container)
286
+ sandbox.verify_timeout() # raises MissingTimeoutError if `timeout` is unusable
287
+ sandbox.verify_python() # raises MissingPythonError if `python3` is unusable
288
+ ```
289
+
290
+ Without the `timeout` check, every `execute()` call fails with exit code `127` and an "executable file not found in $PATH" message from the container runtime in `output`. `execute()` does not raise, so an agent would see this on every command and may misread it as a problem with its own code. `verify_timeout()` turns it into a single `MissingTimeoutError` at setup time.
291
+
292
+ `verify_python()` matters for the file helpers `read`, `write`, `edit`, `ls` and `glob`, which run Python inside the container. Without it, `ls` returns an empty list instead of an error, so an agent can conclude that a directory is empty. `execute()` and file transfer do not need Python, so `verify_python()` is a separate, optional call.
293
+
294
+ `ManagedContainerSandbox` runs both checks on entry by default, `timeout` first. Turn either off with `verify_timeout=False` or `verify_python=False`.
295
+
296
+ ## Errors
297
+
298
+ Everything the library raises on purpose inherits from `SandboxError`:
299
+
300
+ ```
301
+ SandboxError
302
+ ├── DockerImageNotFound image missing and could not be pulled (including auth or rate-limit failures); has an .image attribute
303
+ ├── MissingTimeoutError the container cannot run commands under `timeout`
304
+ ├── MissingPythonError the container has no usable `python3`
305
+ └── ContainerReuseError get_or_create found the name but will not hand the container out; has .name and .container attributes
306
+ ├── SpecMismatchError it is not the container this spec describes; has a .reason attribute
307
+ └── ContainerNotRunningError it is the right container, but it is not running; has a .status attribute
308
+ ```
309
+
310
+ ```python
311
+ from agent_container_sandbox import DockerImageNotFound
312
+
313
+ try:
314
+ container = provisioner.provision(spec)
315
+ except DockerImageNotFound as e:
316
+ print(f"Could not get image {e.image}: {e}")
317
+ ```
318
+
319
+ Other failures, such as a container that cannot start or a daemon that is unreachable, surface as the underlying `docker-py` exceptions. Entering a `ManagedContainerSandbox` twice raises a plain `RuntimeError`, and a non-positive `command_timeout` or `max_output_size` raises a plain `ValueError`, because both are programming errors.
320
+
321
+ ## Known limitations
322
+
323
+ - **`get_or_create` checks the spec, not the live container.** It verifies that the container carries the library's marker label and the fingerprint of the spec, and that it is running. It cannot see settings changed on a live container (for example with `docker update`), a mutable image tag such as `latest` that now points to different content, or a later library version translating the same spec into different Docker arguments.
324
+ - **`get_or_create` is not safe against races.** Two callers creating the same name at the same moment can conflict.
325
+ - **`ls` and `grep` do not report clipped output.** When a listing or a search exceeds `max_output_size`, the deepagents helpers return the part that fit without any signal that results are missing, so the answer looks complete (observed with deepagents 0.7.19 and 0.7.32, reported as [deepagents#7480](https://github.com/langchain-ai/deepagents/issues/7480)). `glob` and `execute` do report truncation. Raise `max_output_size` for large directories or trees.
326
+ - **Network access is all or nothing.** The library can switch networking on or off but cannot restrict it to specific hosts. If the agent needs some access but not all of it, attach the container to a dedicated Docker network or use firewall rules.
327
+ - **Mounted directories are exposed.** A read-write volume gives the agent full access to its contents. Mount a dedicated directory, never a home directory, and never the Docker socket. Likewise, if your own code runs in a container, mounting the host socket gives that container control of the host's Docker daemon.
328
+ - **Commands run as the image's default user,** which is usually root inside the container. Capabilities are dropped, but the container is not rootless.
329
+ - **Disk usage is not limited.** The container's writable layer can grow until the host's disk is full.
330
+ - **Docker is not a hard security boundary.** Containers share the host kernel, so a kernel vulnerability bypasses all of the settings above. For untrusted code, add a stronger runtime such as gVisor or Kata, or run Docker in rootless mode.
331
+
332
+ ## License
333
+
334
+ MIT, see LICENSE