buddelkiste 0.1.0__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.
Files changed (42) hide show
  1. buddelkiste-0.1.0/.gitignore +16 -0
  2. buddelkiste-0.1.0/.python-version +1 -0
  3. buddelkiste-0.1.0/PKG-INFO +302 -0
  4. buddelkiste-0.1.0/README.md +293 -0
  5. buddelkiste-0.1.0/justfile +32 -0
  6. buddelkiste-0.1.0/pyproject.toml +82 -0
  7. buddelkiste-0.1.0/scripts/release +480 -0
  8. buddelkiste-0.1.0/src/buddelkiste/__init__.py +3 -0
  9. buddelkiste-0.1.0/src/buddelkiste/__main__.py +4 -0
  10. buddelkiste-0.1.0/src/buddelkiste/binds.py +174 -0
  11. buddelkiste-0.1.0/src/buddelkiste/cli.py +721 -0
  12. buddelkiste-0.1.0/src/buddelkiste/dns_proxy.py +286 -0
  13. buddelkiste-0.1.0/src/buddelkiste/features.py +841 -0
  14. buddelkiste-0.1.0/src/buddelkiste/network.py +687 -0
  15. buddelkiste-0.1.0/src/buddelkiste/network_inner.py +46 -0
  16. buddelkiste-0.1.0/src/buddelkiste/shims.py +277 -0
  17. buddelkiste-0.1.0/tests/__init__.py +0 -0
  18. buddelkiste-0.1.0/tests/conftest.py +89 -0
  19. buddelkiste-0.1.0/tests/integ/__init__.py +1 -0
  20. buddelkiste-0.1.0/tests/integ/conftest.py +14 -0
  21. buddelkiste-0.1.0/tests/integ/test_bwrap_filesystem.py +256 -0
  22. buddelkiste-0.1.0/tests/integ/test_bwrap_isolation.py +103 -0
  23. buddelkiste-0.1.0/tests/integ/test_bwrap_modes.py +36 -0
  24. buddelkiste-0.1.0/tests/integ/test_nested_nft.py +99 -0
  25. buddelkiste-0.1.0/tests/integ/test_nft_and_dns.py +291 -0
  26. buddelkiste-0.1.0/tests/integ_net/__init__.py +1 -0
  27. buddelkiste-0.1.0/tests/integ_net/conftest.py +34 -0
  28. buddelkiste-0.1.0/tests/integ_net/test_filter_mode.py +73 -0
  29. buddelkiste-0.1.0/tests/integ_net/test_filter_policy.py +89 -0
  30. buddelkiste-0.1.0/tests/probes.py +102 -0
  31. buddelkiste-0.1.0/tests/test_binds.py +348 -0
  32. buddelkiste-0.1.0/tests/test_cli.py +269 -0
  33. buddelkiste-0.1.0/tests/test_config.py +281 -0
  34. buddelkiste-0.1.0/tests/test_dns_proxy.py +113 -0
  35. buddelkiste-0.1.0/tests/test_dns_proxy_extra.py +315 -0
  36. buddelkiste-0.1.0/tests/test_features.py +443 -0
  37. buddelkiste-0.1.0/tests/test_launch.py +94 -0
  38. buddelkiste-0.1.0/tests/test_network.py +315 -0
  39. buddelkiste-0.1.0/tests/test_network_extra.py +346 -0
  40. buddelkiste-0.1.0/tests/test_network_inner.py +117 -0
  41. buddelkiste-0.1.0/tests/test_shims.py +281 -0
  42. buddelkiste-0.1.0/uv.lock +337 -0
@@ -0,0 +1,16 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Coverage
13
+ .coverage
14
+ .coverage.*
15
+ htmlcov/
16
+ coverage.xml
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.5
2
+ Name: buddelkiste
3
+ Version: 0.1.0
4
+ Summary: Run a command inside a bubblewrap sandbox
5
+ Author-email: Michael van Bracht <michael@wontfix.org>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.1
8
+ Description-Content-Type: text/markdown
9
+
10
+ # buddelkiste
11
+
12
+ Run a command in a rootless sandbox (`bk`): filesystem and process isolation
13
+ via [bubblewrap](https://github.com/containers/bubblewrap), plus optional
14
+ network controls — share the host stack, cut connectivity entirely, or filter
15
+ egress with pasta/slirp4netns and in-namespace nftables (IP/CIDR and hostname
16
+ rules, no root required).
17
+
18
+ ## Usage
19
+
20
+ ```bash
21
+ # Run a command in the sandbox
22
+ bk run /path/to/executable [args...]
23
+
24
+ # Inspect the sandbox (login shell)
25
+ bk run
26
+
27
+ # Catalog helpers
28
+ bk list-features
29
+ bk list-net-presets
30
+
31
+ # PATH shims for configured [executables.*] (in ~/.local/bin)
32
+ bk shims install
33
+ bk shims check
34
+
35
+ # Topic features (all on by default)
36
+ bk run --no-feature gui --no-feature google cursor-agent
37
+ bk run --feature python --feature ssh python myscript.py
38
+
39
+ # Network modes
40
+ bk run --network host … # share host network (default)
41
+ bk run --network none … # no connectivity
42
+ bk run --network filter \
43
+ --net-policy deny \
44
+ --net-allow 1.1.1.1/32 \
45
+ --net-allow api.github.com \
46
+ --net-allow '*.pypi.org' \
47
+ --net-deny-preset metadata \
48
+ --net-deny-preset private …
49
+ ```
50
+
51
+ `bk run` wrapper options: `--debug`, `--feature` / `--no-feature`,
52
+ `--network`, `--net-policy`, `--net-allow`, `--net-deny`, `--net-deny-preset`.
53
+ Everything else is the sandboxed command.
54
+
55
+ ## Features
56
+
57
+ Optional permission sets are grouped by topic. Built-ins default to **on**.
58
+ Toggle with CLI flags or config; see `bk list-features` for the live catalog
59
+ (including each feature's module path).
60
+
61
+ | Feature | What it grants |
62
+ |---------|----------------|
63
+ | `asdf` | `~/.asdf`, `~/.tool-versions`, `ASDF_DIR` |
64
+ | `cursor` | Cursor IDE/CLI install and state dirs |
65
+ | `dbus` | session/system bus sockets, `DBUS_SESSION_BUS_ADDRESS` |
66
+ | `docker` | `~/.docker`, Docker socket / `DOCKER_HOST` |
67
+ | `git` | `~/.gitconfig`, `~/.config/git` |
68
+ | `google` | `/opt/google` |
69
+ | `gui` | display, GPU, audio, fonts, related env |
70
+ | `java` | OpenJDK `/etc/java-*-openjdk` configs |
71
+ | `locale` | `LANG`, `LC_NUMERIC`, `LC_TIME` |
72
+ | `node` | npm/nvm/bun paths, `BUN_INSTALL` |
73
+ | `nvim` | `~/nvim` |
74
+ | `python` | pip/virtualenv paths and env |
75
+ | `ssh` | `~/.ssh/config` plus a dedicated agent with `~/.ssh/sandbox_*` keys |
76
+ | `term` | `TERM`, `TERMINFO`, `COLORTERM`, `TERM_PROGRAM`, `EDITOR` |
77
+ | `user` | `~/.local` (ro), `~/.cache` (rw) |
78
+ | `xdg-open` | host `xdg-open` via flatpak-xdg-utils |
79
+
80
+ ### CLI
81
+
82
+ ```bash
83
+ bk list-features
84
+
85
+ # Tighten a desktop-ish run
86
+ bk run --no-feature gui --no-feature dbus --no-feature xdg-open curl https://example.com
87
+
88
+ # Minimal toolchains for a script
89
+ bk run --no-feature cursor --no-feature google --feature python --feature git python app.py
90
+
91
+ # Allowlist-style: disable broadly in config, then enable what you need
92
+ bk run --feature ssh --feature git git fetch
93
+ ```
94
+
95
+ Precedence (later wins for CLI flags): feature defaults → global config
96
+ `features` → per-executable `executables.<name>.features` →
97
+ `--feature` / `--no-feature`.
98
+
99
+ ### Config
100
+
101
+ Configuration is read from `~/.config/buddelkiste/config.toml`, then merged with
102
+ `.buddelkiste.toml` from the current directory or an ancestor (project values
103
+ win). The project file is masked inside the sandbox (empty file bound over the
104
+ path) so the sandboxed command cannot read it.
105
+
106
+ ```toml
107
+ # Global allowlist (exactly these features), or use a table of overrides:
108
+ # features = { gui = false, google = false }
109
+ features = ["git", "ssh", "python", "rust", "term", "locale", "user"]
110
+
111
+ [feature.rust]
112
+ description = "Rust toolchain directories"
113
+ default = false
114
+ env = ["CARGO_HOME", "RUSTUP_HOME"]
115
+
116
+ [[feature.rust.binds]]
117
+ source = "$CARGO_HOME"
118
+ mode = "rw"
119
+
120
+ [[feature.rust.binds]]
121
+ source = "${HOME}/.rustup"
122
+ mode = "ro"
123
+
124
+ [[feature.rust.binds]]
125
+ source = "${HOME}/.cargo/registry"
126
+ mode = "overlay" # persistent upper under $XDG_CACHE_HOME/buddelkiste/overlays/<hash>
127
+
128
+ [executables.cursor-agent]
129
+ features = ["cursor", "git", "ssh", "gui", "dbus", "xdg-open", "term"]
130
+ args = ["--force"]
131
+
132
+ [executables.cursor-agent.network]
133
+ mode = "filter"
134
+ policy = "deny"
135
+ allow = ["1.1.1.1/32", "api.github.com"]
136
+
137
+ [executables.python]
138
+ features = { python = true, git = true, gui = false }
139
+ shim = false
140
+
141
+ # shims = false # disable PATH shims globally (per-entry shim = true still wins)
142
+
143
+ [[binds]]
144
+ source = "/path/to/directory"
145
+ mode = "ro" # or "rw", "tmp-overlay", "overlay", "overlay:$XDG_CACHE_HOME/bk-upper"
146
+
147
+ [[binds]]
148
+ source = "${HOME}/scratch"
149
+ target = "/mnt/scratch"
150
+ mode = "tmp-overlay" # writable in the sandbox; host tree unchanged
151
+
152
+ [[envvars]]
153
+ name = "MYENVVAR"
154
+ value = "example value" # omit value= to take it from the process environment
155
+
156
+ [network]
157
+ mode = "filter"
158
+ policy = "deny"
159
+ allow = ["1.1.1.1/32", "api.github.com", "*.pypi.org"]
160
+ deny = ["203.0.113.0/24"]
161
+ deny_presets = ["metadata", "linklocal", "corp"]
162
+
163
+ [network.presets]
164
+ corp = ["10.50.0.0/16", "*.internal.example.com"]
165
+ ```
166
+
167
+ Bind `mode` values:
168
+
169
+ | Mode | Meaning |
170
+ |------|---------|
171
+ | `ro` | read-only bind (default) |
172
+ | `rw` | read-write bind |
173
+ | `tmp-overlay` | overlayfs; writes are ephemeral |
174
+ | `overlay` | overlayfs; upper under `$XDG_CACHE_HOME/buddelkiste/overlays/<sha256(source)>` |
175
+ | `overlay:<path>` | overlayfs with an explicit upper path (`$VAR` / `${VAR}` / `~` ok) |
176
+
177
+ Paths in `source`, `target`, and `overlay:<path>` may use `$VAR` / `${VAR}`.
178
+
179
+ The current working directory must be visible inside the sandbox. If it is not
180
+ covered by any bind, `bk` asks whether to whitelist it for this run or
181
+ permanently (appends a `[[binds]]` entry). Non-interactively it errors instead.
182
+
183
+ ### PATH shims
184
+
185
+ For each `[executables.<name>]` entry with shimming enabled, `bk shims install`
186
+ writes a small `sh` wrapper into `$XDG_BIN_HOME` (default `~/.local/bin`). The
187
+ shim resolves the real binary (skipping its own directory) and runs
188
+ `bk run <real> "$@"`. Shims include a `# buddelkiste-shim:` marker so later
189
+ installs can refresh our own files without overwriting unrelated binaries.
190
+
191
+ Toggle with a global `shims` boolean (default `true`) and optional per-entry
192
+ `shim = true/false` overrides. Disabled entries remove a managed shim on the
193
+ next `install`.
194
+
195
+ ```toml
196
+ shims = true
197
+
198
+ [executables.cursor-agent]
199
+ features = ["cursor"]
200
+
201
+ [executables.python]
202
+ shim = false
203
+ ```
204
+
205
+ ```bash
206
+ bk shims install # create/update/remove shims per config
207
+ bk shims check # warn if an original binary appears earlier on PATH
208
+ ```
209
+
210
+ Ensure `~/.local/bin` is early on your `PATH`. `bk shims check` exits non-zero
211
+ when something would bypass the sandbox.
212
+
213
+ ### Python features (entry points)
214
+
215
+ Built-ins and third-party packages register features under the
216
+ `buddelkiste.features` entry-point group. The entry point value must be a
217
+ `Feature` instance, or a zero-argument callable that returns one. When loaded,
218
+ `name` and `origin` are set from the entry-point name and value
219
+ (e.g. `rust` / `mypkg.features:RUST`).
220
+
221
+ `Feature` fields:
222
+
223
+ | Field | Type | Default | Meaning |
224
+ |-------|------|---------|---------|
225
+ | `name` | `str` | *(required)* | Feature id used in CLI/config (`--feature`, `features = [...]`). Overwritten by the entry-point name at load time. |
226
+ | `description` | `str` | *(required)* | One-line summary shown by `bk list-features`. |
227
+ | `default` | `bool` | `True` | Whether the feature is enabled before config/CLI overrides. |
228
+ | `env_vars` | `tuple[str, ...]` | `()` | Host env var names to forward into the sandbox when the feature is on. |
229
+ | `binds` | `Callable[[], list]` | `lambda: []` | Zero-arg callable returning bind objects (`ROBindConfig`, `RWBindConfig`, `DevBindConfig`, overlay configs, `Tmpfs`, or raw bwrap arg tuples). Called each run. |
230
+ | `setup` | `Callable[[], AbstractContextManager[Sequence[str]]] \| None` | `None` | Optional factory returning a context manager. Entered while the sandbox runs; its yielded sequence is appended as extra bwrap args (binds, `--setenv`, …). Use for sockets/agents that need lifecycle. |
231
+ | `origin` | `str` | `""` | Shown in `bk list-features`. Overwritten by the entry-point value at load time (e.g. `mypkg.features:RUST`). |
232
+
233
+ ```python
234
+ # mypkg/features.py
235
+ from collections.abc import Iterator, Sequence
236
+ from contextlib import contextmanager
237
+ from pathlib import Path
238
+
239
+ from buddelkiste.binds import ROBindConfig, RWBindConfig
240
+ from buddelkiste.features import Feature
241
+
242
+
243
+ def rust_binds() -> list:
244
+ home = Path.home()
245
+ return [
246
+ RWBindConfig(home / ".cargo"),
247
+ ROBindConfig(home / ".rustup"),
248
+ ]
249
+
250
+
251
+ @contextmanager
252
+ def rust_setup() -> Iterator[Sequence[str]]:
253
+ # Optional: start helpers, yield extra bwrap args, clean up on exit.
254
+ # Built-in ssh uses this pattern for a dedicated ssh-agent.
255
+ yield []
256
+
257
+
258
+ RUST = Feature(
259
+ name="rust", # replaced by entry-point name "rust" when loaded
260
+ description="Rust toolchain directories",
261
+ default=False,
262
+ env_vars=("CARGO_HOME", "RUSTUP_HOME"),
263
+ binds=rust_binds,
264
+ setup=rust_setup, # or omit / None
265
+ origin="mypkg.features:RUST", # replaced by entry-point value when loaded
266
+ )
267
+ ```
268
+
269
+ ```toml
270
+ # pyproject.toml
271
+ [project.entry-points."buddelkiste.features"]
272
+ rust = "mypkg.features:RUST"
273
+ ```
274
+
275
+ After install, `bk list-features` shows the entry and its origin. Pure-TOML
276
+ `[feature.<name>]` covers env/bind cases without a package; use Python entry
277
+ points for custom bind logic or `setup` hooks. Config feature names must not
278
+ collide with an existing entry point.
279
+
280
+ ## Network filter dependencies
281
+
282
+ `network.mode = "filter"` needs **pasta** (from `passt`) or **slirp4netns**, plus
283
+ **nft** and **setpriv**. No root and no reserved host subnets are required.
284
+ Hostname rules start a DNS proxy that redirects UDP/53 and updates dynamic
285
+ nftables allow sets from resolved A/AAAA records. Nameserver IPs from
286
+ `/etc/resolv.conf` are auto-allowed under a default-deny policy.
287
+
288
+ ## Development
289
+
290
+ ```bash
291
+ uv sync --group dev
292
+ uv run bk --help
293
+ uv run pytest # unit + nested-safe integ + coverage; TUN e2e skipped if unavailable
294
+ uv run pytest -m integration # real bwrap host/none + nested nft
295
+ uv run pytest -m requires_tun # filter/pasta e2e (needs /dev/net/tun)
296
+ ```
297
+
298
+ Integration layout:
299
+
300
+ - `tests/` — unit/contract tests (mocked subprocess where needed)
301
+ - `tests/integ/` — nested-safe real `bwrap` (`host`/`none`), binds/env isolation, nested `nft` + DNS proxy/`nft add element`/`UDP/53` redirect against real tools
302
+ - `tests/integ_net/` — filter-mode e2e via pasta/slirp (allow/deny IP & host, guest caps); skipped without `/dev/net/tun`
@@ -0,0 +1,293 @@
1
+ # buddelkiste
2
+
3
+ Run a command in a rootless sandbox (`bk`): filesystem and process isolation
4
+ via [bubblewrap](https://github.com/containers/bubblewrap), plus optional
5
+ network controls — share the host stack, cut connectivity entirely, or filter
6
+ egress with pasta/slirp4netns and in-namespace nftables (IP/CIDR and hostname
7
+ rules, no root required).
8
+
9
+ ## Usage
10
+
11
+ ```bash
12
+ # Run a command in the sandbox
13
+ bk run /path/to/executable [args...]
14
+
15
+ # Inspect the sandbox (login shell)
16
+ bk run
17
+
18
+ # Catalog helpers
19
+ bk list-features
20
+ bk list-net-presets
21
+
22
+ # PATH shims for configured [executables.*] (in ~/.local/bin)
23
+ bk shims install
24
+ bk shims check
25
+
26
+ # Topic features (all on by default)
27
+ bk run --no-feature gui --no-feature google cursor-agent
28
+ bk run --feature python --feature ssh python myscript.py
29
+
30
+ # Network modes
31
+ bk run --network host … # share host network (default)
32
+ bk run --network none … # no connectivity
33
+ bk run --network filter \
34
+ --net-policy deny \
35
+ --net-allow 1.1.1.1/32 \
36
+ --net-allow api.github.com \
37
+ --net-allow '*.pypi.org' \
38
+ --net-deny-preset metadata \
39
+ --net-deny-preset private …
40
+ ```
41
+
42
+ `bk run` wrapper options: `--debug`, `--feature` / `--no-feature`,
43
+ `--network`, `--net-policy`, `--net-allow`, `--net-deny`, `--net-deny-preset`.
44
+ Everything else is the sandboxed command.
45
+
46
+ ## Features
47
+
48
+ Optional permission sets are grouped by topic. Built-ins default to **on**.
49
+ Toggle with CLI flags or config; see `bk list-features` for the live catalog
50
+ (including each feature's module path).
51
+
52
+ | Feature | What it grants |
53
+ |---------|----------------|
54
+ | `asdf` | `~/.asdf`, `~/.tool-versions`, `ASDF_DIR` |
55
+ | `cursor` | Cursor IDE/CLI install and state dirs |
56
+ | `dbus` | session/system bus sockets, `DBUS_SESSION_BUS_ADDRESS` |
57
+ | `docker` | `~/.docker`, Docker socket / `DOCKER_HOST` |
58
+ | `git` | `~/.gitconfig`, `~/.config/git` |
59
+ | `google` | `/opt/google` |
60
+ | `gui` | display, GPU, audio, fonts, related env |
61
+ | `java` | OpenJDK `/etc/java-*-openjdk` configs |
62
+ | `locale` | `LANG`, `LC_NUMERIC`, `LC_TIME` |
63
+ | `node` | npm/nvm/bun paths, `BUN_INSTALL` |
64
+ | `nvim` | `~/nvim` |
65
+ | `python` | pip/virtualenv paths and env |
66
+ | `ssh` | `~/.ssh/config` plus a dedicated agent with `~/.ssh/sandbox_*` keys |
67
+ | `term` | `TERM`, `TERMINFO`, `COLORTERM`, `TERM_PROGRAM`, `EDITOR` |
68
+ | `user` | `~/.local` (ro), `~/.cache` (rw) |
69
+ | `xdg-open` | host `xdg-open` via flatpak-xdg-utils |
70
+
71
+ ### CLI
72
+
73
+ ```bash
74
+ bk list-features
75
+
76
+ # Tighten a desktop-ish run
77
+ bk run --no-feature gui --no-feature dbus --no-feature xdg-open curl https://example.com
78
+
79
+ # Minimal toolchains for a script
80
+ bk run --no-feature cursor --no-feature google --feature python --feature git python app.py
81
+
82
+ # Allowlist-style: disable broadly in config, then enable what you need
83
+ bk run --feature ssh --feature git git fetch
84
+ ```
85
+
86
+ Precedence (later wins for CLI flags): feature defaults → global config
87
+ `features` → per-executable `executables.<name>.features` →
88
+ `--feature` / `--no-feature`.
89
+
90
+ ### Config
91
+
92
+ Configuration is read from `~/.config/buddelkiste/config.toml`, then merged with
93
+ `.buddelkiste.toml` from the current directory or an ancestor (project values
94
+ win). The project file is masked inside the sandbox (empty file bound over the
95
+ path) so the sandboxed command cannot read it.
96
+
97
+ ```toml
98
+ # Global allowlist (exactly these features), or use a table of overrides:
99
+ # features = { gui = false, google = false }
100
+ features = ["git", "ssh", "python", "rust", "term", "locale", "user"]
101
+
102
+ [feature.rust]
103
+ description = "Rust toolchain directories"
104
+ default = false
105
+ env = ["CARGO_HOME", "RUSTUP_HOME"]
106
+
107
+ [[feature.rust.binds]]
108
+ source = "$CARGO_HOME"
109
+ mode = "rw"
110
+
111
+ [[feature.rust.binds]]
112
+ source = "${HOME}/.rustup"
113
+ mode = "ro"
114
+
115
+ [[feature.rust.binds]]
116
+ source = "${HOME}/.cargo/registry"
117
+ mode = "overlay" # persistent upper under $XDG_CACHE_HOME/buddelkiste/overlays/<hash>
118
+
119
+ [executables.cursor-agent]
120
+ features = ["cursor", "git", "ssh", "gui", "dbus", "xdg-open", "term"]
121
+ args = ["--force"]
122
+
123
+ [executables.cursor-agent.network]
124
+ mode = "filter"
125
+ policy = "deny"
126
+ allow = ["1.1.1.1/32", "api.github.com"]
127
+
128
+ [executables.python]
129
+ features = { python = true, git = true, gui = false }
130
+ shim = false
131
+
132
+ # shims = false # disable PATH shims globally (per-entry shim = true still wins)
133
+
134
+ [[binds]]
135
+ source = "/path/to/directory"
136
+ mode = "ro" # or "rw", "tmp-overlay", "overlay", "overlay:$XDG_CACHE_HOME/bk-upper"
137
+
138
+ [[binds]]
139
+ source = "${HOME}/scratch"
140
+ target = "/mnt/scratch"
141
+ mode = "tmp-overlay" # writable in the sandbox; host tree unchanged
142
+
143
+ [[envvars]]
144
+ name = "MYENVVAR"
145
+ value = "example value" # omit value= to take it from the process environment
146
+
147
+ [network]
148
+ mode = "filter"
149
+ policy = "deny"
150
+ allow = ["1.1.1.1/32", "api.github.com", "*.pypi.org"]
151
+ deny = ["203.0.113.0/24"]
152
+ deny_presets = ["metadata", "linklocal", "corp"]
153
+
154
+ [network.presets]
155
+ corp = ["10.50.0.0/16", "*.internal.example.com"]
156
+ ```
157
+
158
+ Bind `mode` values:
159
+
160
+ | Mode | Meaning |
161
+ |------|---------|
162
+ | `ro` | read-only bind (default) |
163
+ | `rw` | read-write bind |
164
+ | `tmp-overlay` | overlayfs; writes are ephemeral |
165
+ | `overlay` | overlayfs; upper under `$XDG_CACHE_HOME/buddelkiste/overlays/<sha256(source)>` |
166
+ | `overlay:<path>` | overlayfs with an explicit upper path (`$VAR` / `${VAR}` / `~` ok) |
167
+
168
+ Paths in `source`, `target`, and `overlay:<path>` may use `$VAR` / `${VAR}`.
169
+
170
+ The current working directory must be visible inside the sandbox. If it is not
171
+ covered by any bind, `bk` asks whether to whitelist it for this run or
172
+ permanently (appends a `[[binds]]` entry). Non-interactively it errors instead.
173
+
174
+ ### PATH shims
175
+
176
+ For each `[executables.<name>]` entry with shimming enabled, `bk shims install`
177
+ writes a small `sh` wrapper into `$XDG_BIN_HOME` (default `~/.local/bin`). The
178
+ shim resolves the real binary (skipping its own directory) and runs
179
+ `bk run <real> "$@"`. Shims include a `# buddelkiste-shim:` marker so later
180
+ installs can refresh our own files without overwriting unrelated binaries.
181
+
182
+ Toggle with a global `shims` boolean (default `true`) and optional per-entry
183
+ `shim = true/false` overrides. Disabled entries remove a managed shim on the
184
+ next `install`.
185
+
186
+ ```toml
187
+ shims = true
188
+
189
+ [executables.cursor-agent]
190
+ features = ["cursor"]
191
+
192
+ [executables.python]
193
+ shim = false
194
+ ```
195
+
196
+ ```bash
197
+ bk shims install # create/update/remove shims per config
198
+ bk shims check # warn if an original binary appears earlier on PATH
199
+ ```
200
+
201
+ Ensure `~/.local/bin` is early on your `PATH`. `bk shims check` exits non-zero
202
+ when something would bypass the sandbox.
203
+
204
+ ### Python features (entry points)
205
+
206
+ Built-ins and third-party packages register features under the
207
+ `buddelkiste.features` entry-point group. The entry point value must be a
208
+ `Feature` instance, or a zero-argument callable that returns one. When loaded,
209
+ `name` and `origin` are set from the entry-point name and value
210
+ (e.g. `rust` / `mypkg.features:RUST`).
211
+
212
+ `Feature` fields:
213
+
214
+ | Field | Type | Default | Meaning |
215
+ |-------|------|---------|---------|
216
+ | `name` | `str` | *(required)* | Feature id used in CLI/config (`--feature`, `features = [...]`). Overwritten by the entry-point name at load time. |
217
+ | `description` | `str` | *(required)* | One-line summary shown by `bk list-features`. |
218
+ | `default` | `bool` | `True` | Whether the feature is enabled before config/CLI overrides. |
219
+ | `env_vars` | `tuple[str, ...]` | `()` | Host env var names to forward into the sandbox when the feature is on. |
220
+ | `binds` | `Callable[[], list]` | `lambda: []` | Zero-arg callable returning bind objects (`ROBindConfig`, `RWBindConfig`, `DevBindConfig`, overlay configs, `Tmpfs`, or raw bwrap arg tuples). Called each run. |
221
+ | `setup` | `Callable[[], AbstractContextManager[Sequence[str]]] \| None` | `None` | Optional factory returning a context manager. Entered while the sandbox runs; its yielded sequence is appended as extra bwrap args (binds, `--setenv`, …). Use for sockets/agents that need lifecycle. |
222
+ | `origin` | `str` | `""` | Shown in `bk list-features`. Overwritten by the entry-point value at load time (e.g. `mypkg.features:RUST`). |
223
+
224
+ ```python
225
+ # mypkg/features.py
226
+ from collections.abc import Iterator, Sequence
227
+ from contextlib import contextmanager
228
+ from pathlib import Path
229
+
230
+ from buddelkiste.binds import ROBindConfig, RWBindConfig
231
+ from buddelkiste.features import Feature
232
+
233
+
234
+ def rust_binds() -> list:
235
+ home = Path.home()
236
+ return [
237
+ RWBindConfig(home / ".cargo"),
238
+ ROBindConfig(home / ".rustup"),
239
+ ]
240
+
241
+
242
+ @contextmanager
243
+ def rust_setup() -> Iterator[Sequence[str]]:
244
+ # Optional: start helpers, yield extra bwrap args, clean up on exit.
245
+ # Built-in ssh uses this pattern for a dedicated ssh-agent.
246
+ yield []
247
+
248
+
249
+ RUST = Feature(
250
+ name="rust", # replaced by entry-point name "rust" when loaded
251
+ description="Rust toolchain directories",
252
+ default=False,
253
+ env_vars=("CARGO_HOME", "RUSTUP_HOME"),
254
+ binds=rust_binds,
255
+ setup=rust_setup, # or omit / None
256
+ origin="mypkg.features:RUST", # replaced by entry-point value when loaded
257
+ )
258
+ ```
259
+
260
+ ```toml
261
+ # pyproject.toml
262
+ [project.entry-points."buddelkiste.features"]
263
+ rust = "mypkg.features:RUST"
264
+ ```
265
+
266
+ After install, `bk list-features` shows the entry and its origin. Pure-TOML
267
+ `[feature.<name>]` covers env/bind cases without a package; use Python entry
268
+ points for custom bind logic or `setup` hooks. Config feature names must not
269
+ collide with an existing entry point.
270
+
271
+ ## Network filter dependencies
272
+
273
+ `network.mode = "filter"` needs **pasta** (from `passt`) or **slirp4netns**, plus
274
+ **nft** and **setpriv**. No root and no reserved host subnets are required.
275
+ Hostname rules start a DNS proxy that redirects UDP/53 and updates dynamic
276
+ nftables allow sets from resolved A/AAAA records. Nameserver IPs from
277
+ `/etc/resolv.conf` are auto-allowed under a default-deny policy.
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ uv sync --group dev
283
+ uv run bk --help
284
+ uv run pytest # unit + nested-safe integ + coverage; TUN e2e skipped if unavailable
285
+ uv run pytest -m integration # real bwrap host/none + nested nft
286
+ uv run pytest -m requires_tun # filter/pasta e2e (needs /dev/net/tun)
287
+ ```
288
+
289
+ Integration layout:
290
+
291
+ - `tests/` — unit/contract tests (mocked subprocess where needed)
292
+ - `tests/integ/` — nested-safe real `bwrap` (`host`/`none`), binds/env isolation, nested `nft` + DNS proxy/`nft add element`/`UDP/53` redirect against real tools
293
+ - `tests/integ_net/` — filter-mode e2e via pasta/slirp (allow/deny IP & host, guest caps); skipped without `/dev/net/tun`
@@ -0,0 +1,32 @@
1
+ set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
2
+
3
+ # Create .venv and install the package with the dev dependency group (expects uv).
4
+ setup:
5
+ #!/usr/bin/env bash
6
+ set -euo pipefail
7
+ cd "{{justfile_directory()}}"
8
+ if ! command -v uv >/dev/null 2>&1; then
9
+ echo "uv is required on PATH" >&2
10
+ exit 1
11
+ fi
12
+ uv sync --group dev
13
+
14
+ # Run the test suite (pass extra pytest args after --).
15
+ test *args:
16
+ cd "{{justfile_directory()}}" && uv run pytest {{args}}
17
+
18
+ # Lint with ruff (pass extra ruff args after --).
19
+ lint *args:
20
+ cd "{{justfile_directory()}}" && uv run ruff check src tests scripts {{args}}
21
+
22
+ # Typecheck with ty (pass extra ty args after --).
23
+ typecheck *args:
24
+ cd "{{justfile_directory()}}" && uv run ty check src {{args}}
25
+
26
+ # Tag and build the next release candidate (omit version to be prompted).
27
+ rc *args:
28
+ "{{justfile_directory()}}/scripts/release" --rc {{args}}
29
+
30
+ # Tag and build a release (omit version to be prompted with recent commits).
31
+ release *args:
32
+ "{{justfile_directory()}}/scripts/release" {{args}}