projectwrap 202604.2__tar.gz → 202604.3__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.
- {projectwrap-202604.2 → projectwrap-202604.3}/PKG-INFO +106 -12
- {projectwrap-202604.2 → projectwrap-202604.3}/README.md +105 -11
- {projectwrap-202604.2 → projectwrap-202604.3}/pyproject.toml +6 -2
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/core.py +18 -6
- projectwrap-202604.3/src/project_wrap/deps.py +159 -0
- projectwrap-202604.3/src/project_wrap/templates/project.toml +48 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/validate.py +1 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/vault.py +45 -5
- projectwrap-202604.2/src/project_wrap/deps.py +0 -87
- projectwrap-202604.2/src/project_wrap/templates/project.toml +0 -36
- {projectwrap-202604.2 → projectwrap-202604.3}/LICENSE +0 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/__init__.py +0 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/cli.py +0 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/templates/init.fish +0 -0
- {projectwrap-202604.2 → projectwrap-202604.3}/src/project_wrap/templates/init.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: projectwrap
|
|
3
|
-
Version: 202604.
|
|
3
|
+
Version: 202604.3
|
|
4
4
|
Summary: Isolated project environments with bubblewrap sandboxing
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: sandbox,bubblewrap,bwrap,isolation,development,security
|
|
@@ -59,7 +59,9 @@ _absolutely no warranty_.
|
|
|
59
59
|
- Protect you from `root`
|
|
60
60
|
|
|
61
61
|
**Dependencies:** Python 3.11+ (stdlib only, no pip dependencies),
|
|
62
|
-
[bubblewrap](https://github.com/containers/bubblewrap) for sandboxing
|
|
62
|
+
[bubblewrap](https://github.com/containers/bubblewrap) ≥ 0.4 for sandboxing
|
|
63
|
+
(encrypted volumes need `--unshare-user` / `--uid` support to drop back to
|
|
64
|
+
the real uid inside the sandbox),
|
|
63
65
|
[gocryptfs](https://nuetzlich.net/gocryptfs/) for encrypted volumes.
|
|
64
66
|
|
|
65
67
|
**Design principles:**
|
|
@@ -113,6 +115,8 @@ blacklist = [ # can't be accessed at all from the sandbox
|
|
|
113
115
|
"~/.aws",
|
|
114
116
|
"~/.ssh",
|
|
115
117
|
"~/projects/", # hide all other projects
|
|
118
|
+
# trimmed for brevity — see the generated template (`pwrap --new`) for
|
|
119
|
+
# the full default blacklist (GCP, Azure, GPG, docker, npm, PyPI, /mnt).
|
|
116
120
|
]
|
|
117
121
|
whitelist = [ # exceptions to the blacklist
|
|
118
122
|
"~/.kube/myproject",
|
|
@@ -170,6 +174,95 @@ set -gx AICHAT_CONFIG_DIR vault/aichat
|
|
|
170
174
|
|
|
171
175
|
```
|
|
172
176
|
|
|
177
|
+
##### Claude Code in sandbox #####
|
|
178
|
+
|
|
179
|
+
Claude Code writes state to several paths under `~/` (`~/.claude`,
|
|
180
|
+
`~/.claude.json.lock`, `~/.local/state/claude`, `~/.cache/claude-cli-nodejs`,
|
|
181
|
+
`~/.npm`). Rather than making each of these writable, redirect all of Claude's
|
|
182
|
+
state into the project directory:
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
[env]
|
|
186
|
+
CLAUDE_CONFIG_DIR = "vault/claude" # or any writable path inside the sandbox
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
This gives each project its own Claude state (settings, history, MCP logs) —
|
|
190
|
+
no leakage between sandboxed projects. With an `[encrypted]` volume, Claude's
|
|
191
|
+
state is encrypted at rest.
|
|
192
|
+
|
|
193
|
+
##### Encrypted vault, minimal sandbox rules #####
|
|
194
|
+
|
|
195
|
+
Minimal sandbox configuration — no custom blacklists or whitelists, no
|
|
196
|
+
`clean_env`. The goal is to keep secrets (kubeconfig, shell history, LLM
|
|
197
|
+
chat logs) encrypted at rest and isolated per project, without locking down
|
|
198
|
+
the rest of the environment.
|
|
199
|
+
|
|
200
|
+
```toml
|
|
201
|
+
[project]
|
|
202
|
+
name = "myproject"
|
|
203
|
+
dir = "~/projects/myproject"
|
|
204
|
+
shell = "/usr/bin/fish"
|
|
205
|
+
|
|
206
|
+
[sandbox]
|
|
207
|
+
enabled = true
|
|
208
|
+
|
|
209
|
+
[encrypted]
|
|
210
|
+
cipherdir = "encrypted"
|
|
211
|
+
mountpoint = "~/projects/myproject/vault"
|
|
212
|
+
|
|
213
|
+
[env]
|
|
214
|
+
KUBECONFIG = "vault/kubeconfig"
|
|
215
|
+
CLAUDE_CONFIG_DIR = "vault/claude"
|
|
216
|
+
XDG_DATA_HOME = "vault/.config" # fish history, tool state
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`init.fish`:
|
|
220
|
+
```fish
|
|
221
|
+
source .venv/bin/activate.fish
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`[sandbox] enabled = true` still applies the security defaults below: home
|
|
225
|
+
is bound read-only, the config dir is blacklisted, the docker socket is
|
|
226
|
+
masked, `/tmp` is a tmpfs, and PID/IPC namespaces are isolated. With no
|
|
227
|
+
custom blacklist/whitelist/writable entries, the only writable paths are
|
|
228
|
+
the project directory and the vault mountpoint — reads from anywhere else
|
|
229
|
+
under home still work, but writes outside those two paths fail. Add entries
|
|
230
|
+
to `writable` to poke rw holes in the read-only home if you need them. The
|
|
231
|
+
encrypted vault holds project-specific secrets and history that disappear
|
|
232
|
+
when the shell exits; `vault/` lives inside the project directory so it's
|
|
233
|
+
writable by default.
|
|
234
|
+
|
|
235
|
+
##### Maximum isolation #####
|
|
236
|
+
|
|
237
|
+
Default-deny for both filesystem and environment. Nothing is visible or set
|
|
238
|
+
unless explicitly allowed.
|
|
239
|
+
|
|
240
|
+
```toml
|
|
241
|
+
[project]
|
|
242
|
+
name = "myproject"
|
|
243
|
+
dir = "~/projects/myproject"
|
|
244
|
+
shell = "/usr/bin/fish"
|
|
245
|
+
|
|
246
|
+
[sandbox]
|
|
247
|
+
enabled = true
|
|
248
|
+
clean_env = true # only PATH/HOME/USER/SHELL/TERM/LANG
|
|
249
|
+
blacklist = [
|
|
250
|
+
"~", # hide all dotfiles and home contents
|
|
251
|
+
"/mnt", # WSL: hide Windows drives
|
|
252
|
+
]
|
|
253
|
+
whitelist = [
|
|
254
|
+
"~/.config/fish", # shell config (read-only)
|
|
255
|
+
"~/.pyenv", # python versions (read-only)
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
[env]
|
|
259
|
+
XDG_DATA_HOME = ".config" # fish history, tool state → project dir
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The project directory is always writable regardless of blacklist. With
|
|
263
|
+
`XDG_DATA_HOME` pointing inside it, fish history and XDG-aware tools write
|
|
264
|
+
their state there instead of the (hidden) home directory.
|
|
265
|
+
|
|
173
266
|
##### GUI apps in sandbox #####
|
|
174
267
|
|
|
175
268
|
To run emacs or other GUI apps inside the sandbox on WSL2:
|
|
@@ -216,12 +309,9 @@ with an `[encrypted]` section, pointing at the mountpoint. Use it from init
|
|
|
216
309
|
scripts or app configs to redirect history/state into the vault without
|
|
217
310
|
hardcoding paths per project.
|
|
218
311
|
|
|
219
|
-
**
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
you have no real privileges on the host and cannot escalate. Your files
|
|
223
|
-
remain owned by your real uid. Scripts that gate on `$UID == 0` will
|
|
224
|
-
misbehave; check `$PROJECT_WRAP` or `$PWRAP_VAULT_DIR` instead.
|
|
312
|
+
**If `id -u` reports 0 inside the sandbox**, your bubblewrap is too old —
|
|
313
|
+
run `pwrap --check-deps`. Encrypted vaults need `--unshare-user` / `--uid`
|
|
314
|
+
support (bubblewrap ≥ 0.4) to drop back to your real uid.
|
|
225
315
|
|
|
226
316
|
**Multiple terminals** (`shared = false`, default): each terminal gets an
|
|
227
317
|
independent gocryptfs mount. Writes to different files merge on next
|
|
@@ -242,8 +332,7 @@ and the mount is released.
|
|
|
242
332
|
pwrap # list projects
|
|
243
333
|
pwrap myproject # launch project
|
|
244
334
|
pwrap -v myproject # verbose output
|
|
245
|
-
pwrap --new ~/projects/myproject # create config (name
|
|
246
|
-
pwrap --new ~/projects/myproject custom # create with explicit name
|
|
335
|
+
pwrap --new ~/projects/myproject # create config (name = dir basename)
|
|
247
336
|
pwrap --new --shell /bin/bash ~/projects/x # specify shell
|
|
248
337
|
pwrap --check-deps # check optional dependencies
|
|
249
338
|
pwrap --version # show version
|
|
@@ -255,12 +344,17 @@ When sandboxing is enabled:
|
|
|
255
344
|
|
|
256
345
|
- Home is **read-only**; only the project directory is writable
|
|
257
346
|
- Config directory (`~/.config/pwrap`) is always blacklisted
|
|
347
|
+
- Docker socket (`/run/docker.sock`) masked — `connect()` works on
|
|
348
|
+
ro-bound sockets, so an exposed docker socket is a full escape to root
|
|
349
|
+
- Default template blacklists credential dirs (SSH, GPG, AWS, GCP, Azure,
|
|
350
|
+
Docker, npm, PyPI) and `/mnt` (WSL Windows drives)
|
|
258
351
|
- PID and IPC namespaces are isolated
|
|
259
352
|
- TIOCSTI injection blocked automatically on kernels < 6.2
|
|
260
|
-
- XDG runtime directory isolated
|
|
353
|
+
- XDG runtime directory isolated (D-Bus, Wayland, keyring sockets)
|
|
261
354
|
- Sandbox dies with parent process
|
|
262
355
|
- Encrypted volumes mount in isolated namespace (invisible on host)
|
|
263
|
-
-
|
|
356
|
+
- Writable paths are auto-created (as directories) if they don't exist;
|
|
357
|
+
paths that already exist are bound as-is, so a file entry binds that file
|
|
264
358
|
|
|
265
359
|
Run your editor from inside the sandbox if it has any capacity to run
|
|
266
360
|
linters, hooks, or anything else from the project environment. A
|
|
@@ -35,7 +35,9 @@ _absolutely no warranty_.
|
|
|
35
35
|
- Protect you from `root`
|
|
36
36
|
|
|
37
37
|
**Dependencies:** Python 3.11+ (stdlib only, no pip dependencies),
|
|
38
|
-
[bubblewrap](https://github.com/containers/bubblewrap) for sandboxing
|
|
38
|
+
[bubblewrap](https://github.com/containers/bubblewrap) ≥ 0.4 for sandboxing
|
|
39
|
+
(encrypted volumes need `--unshare-user` / `--uid` support to drop back to
|
|
40
|
+
the real uid inside the sandbox),
|
|
39
41
|
[gocryptfs](https://nuetzlich.net/gocryptfs/) for encrypted volumes.
|
|
40
42
|
|
|
41
43
|
**Design principles:**
|
|
@@ -89,6 +91,8 @@ blacklist = [ # can't be accessed at all from the sandbox
|
|
|
89
91
|
"~/.aws",
|
|
90
92
|
"~/.ssh",
|
|
91
93
|
"~/projects/", # hide all other projects
|
|
94
|
+
# trimmed for brevity — see the generated template (`pwrap --new`) for
|
|
95
|
+
# the full default blacklist (GCP, Azure, GPG, docker, npm, PyPI, /mnt).
|
|
92
96
|
]
|
|
93
97
|
whitelist = [ # exceptions to the blacklist
|
|
94
98
|
"~/.kube/myproject",
|
|
@@ -146,6 +150,95 @@ set -gx AICHAT_CONFIG_DIR vault/aichat
|
|
|
146
150
|
|
|
147
151
|
```
|
|
148
152
|
|
|
153
|
+
##### Claude Code in sandbox #####
|
|
154
|
+
|
|
155
|
+
Claude Code writes state to several paths under `~/` (`~/.claude`,
|
|
156
|
+
`~/.claude.json.lock`, `~/.local/state/claude`, `~/.cache/claude-cli-nodejs`,
|
|
157
|
+
`~/.npm`). Rather than making each of these writable, redirect all of Claude's
|
|
158
|
+
state into the project directory:
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
[env]
|
|
162
|
+
CLAUDE_CONFIG_DIR = "vault/claude" # or any writable path inside the sandbox
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This gives each project its own Claude state (settings, history, MCP logs) —
|
|
166
|
+
no leakage between sandboxed projects. With an `[encrypted]` volume, Claude's
|
|
167
|
+
state is encrypted at rest.
|
|
168
|
+
|
|
169
|
+
##### Encrypted vault, minimal sandbox rules #####
|
|
170
|
+
|
|
171
|
+
Minimal sandbox configuration — no custom blacklists or whitelists, no
|
|
172
|
+
`clean_env`. The goal is to keep secrets (kubeconfig, shell history, LLM
|
|
173
|
+
chat logs) encrypted at rest and isolated per project, without locking down
|
|
174
|
+
the rest of the environment.
|
|
175
|
+
|
|
176
|
+
```toml
|
|
177
|
+
[project]
|
|
178
|
+
name = "myproject"
|
|
179
|
+
dir = "~/projects/myproject"
|
|
180
|
+
shell = "/usr/bin/fish"
|
|
181
|
+
|
|
182
|
+
[sandbox]
|
|
183
|
+
enabled = true
|
|
184
|
+
|
|
185
|
+
[encrypted]
|
|
186
|
+
cipherdir = "encrypted"
|
|
187
|
+
mountpoint = "~/projects/myproject/vault"
|
|
188
|
+
|
|
189
|
+
[env]
|
|
190
|
+
KUBECONFIG = "vault/kubeconfig"
|
|
191
|
+
CLAUDE_CONFIG_DIR = "vault/claude"
|
|
192
|
+
XDG_DATA_HOME = "vault/.config" # fish history, tool state
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`init.fish`:
|
|
196
|
+
```fish
|
|
197
|
+
source .venv/bin/activate.fish
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`[sandbox] enabled = true` still applies the security defaults below: home
|
|
201
|
+
is bound read-only, the config dir is blacklisted, the docker socket is
|
|
202
|
+
masked, `/tmp` is a tmpfs, and PID/IPC namespaces are isolated. With no
|
|
203
|
+
custom blacklist/whitelist/writable entries, the only writable paths are
|
|
204
|
+
the project directory and the vault mountpoint — reads from anywhere else
|
|
205
|
+
under home still work, but writes outside those two paths fail. Add entries
|
|
206
|
+
to `writable` to poke rw holes in the read-only home if you need them. The
|
|
207
|
+
encrypted vault holds project-specific secrets and history that disappear
|
|
208
|
+
when the shell exits; `vault/` lives inside the project directory so it's
|
|
209
|
+
writable by default.
|
|
210
|
+
|
|
211
|
+
##### Maximum isolation #####
|
|
212
|
+
|
|
213
|
+
Default-deny for both filesystem and environment. Nothing is visible or set
|
|
214
|
+
unless explicitly allowed.
|
|
215
|
+
|
|
216
|
+
```toml
|
|
217
|
+
[project]
|
|
218
|
+
name = "myproject"
|
|
219
|
+
dir = "~/projects/myproject"
|
|
220
|
+
shell = "/usr/bin/fish"
|
|
221
|
+
|
|
222
|
+
[sandbox]
|
|
223
|
+
enabled = true
|
|
224
|
+
clean_env = true # only PATH/HOME/USER/SHELL/TERM/LANG
|
|
225
|
+
blacklist = [
|
|
226
|
+
"~", # hide all dotfiles and home contents
|
|
227
|
+
"/mnt", # WSL: hide Windows drives
|
|
228
|
+
]
|
|
229
|
+
whitelist = [
|
|
230
|
+
"~/.config/fish", # shell config (read-only)
|
|
231
|
+
"~/.pyenv", # python versions (read-only)
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
[env]
|
|
235
|
+
XDG_DATA_HOME = ".config" # fish history, tool state → project dir
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The project directory is always writable regardless of blacklist. With
|
|
239
|
+
`XDG_DATA_HOME` pointing inside it, fish history and XDG-aware tools write
|
|
240
|
+
their state there instead of the (hidden) home directory.
|
|
241
|
+
|
|
149
242
|
##### GUI apps in sandbox #####
|
|
150
243
|
|
|
151
244
|
To run emacs or other GUI apps inside the sandbox on WSL2:
|
|
@@ -192,12 +285,9 @@ with an `[encrypted]` section, pointing at the mountpoint. Use it from init
|
|
|
192
285
|
scripts or app configs to redirect history/state into the vault without
|
|
193
286
|
hardcoding paths per project.
|
|
194
287
|
|
|
195
|
-
**
|
|
196
|
-
`
|
|
197
|
-
|
|
198
|
-
you have no real privileges on the host and cannot escalate. Your files
|
|
199
|
-
remain owned by your real uid. Scripts that gate on `$UID == 0` will
|
|
200
|
-
misbehave; check `$PROJECT_WRAP` or `$PWRAP_VAULT_DIR` instead.
|
|
288
|
+
**If `id -u` reports 0 inside the sandbox**, your bubblewrap is too old —
|
|
289
|
+
run `pwrap --check-deps`. Encrypted vaults need `--unshare-user` / `--uid`
|
|
290
|
+
support (bubblewrap ≥ 0.4) to drop back to your real uid.
|
|
201
291
|
|
|
202
292
|
**Multiple terminals** (`shared = false`, default): each terminal gets an
|
|
203
293
|
independent gocryptfs mount. Writes to different files merge on next
|
|
@@ -218,8 +308,7 @@ and the mount is released.
|
|
|
218
308
|
pwrap # list projects
|
|
219
309
|
pwrap myproject # launch project
|
|
220
310
|
pwrap -v myproject # verbose output
|
|
221
|
-
pwrap --new ~/projects/myproject # create config (name
|
|
222
|
-
pwrap --new ~/projects/myproject custom # create with explicit name
|
|
311
|
+
pwrap --new ~/projects/myproject # create config (name = dir basename)
|
|
223
312
|
pwrap --new --shell /bin/bash ~/projects/x # specify shell
|
|
224
313
|
pwrap --check-deps # check optional dependencies
|
|
225
314
|
pwrap --version # show version
|
|
@@ -231,12 +320,17 @@ When sandboxing is enabled:
|
|
|
231
320
|
|
|
232
321
|
- Home is **read-only**; only the project directory is writable
|
|
233
322
|
- Config directory (`~/.config/pwrap`) is always blacklisted
|
|
323
|
+
- Docker socket (`/run/docker.sock`) masked — `connect()` works on
|
|
324
|
+
ro-bound sockets, so an exposed docker socket is a full escape to root
|
|
325
|
+
- Default template blacklists credential dirs (SSH, GPG, AWS, GCP, Azure,
|
|
326
|
+
Docker, npm, PyPI) and `/mnt` (WSL Windows drives)
|
|
234
327
|
- PID and IPC namespaces are isolated
|
|
235
328
|
- TIOCSTI injection blocked automatically on kernels < 6.2
|
|
236
|
-
- XDG runtime directory isolated
|
|
329
|
+
- XDG runtime directory isolated (D-Bus, Wayland, keyring sockets)
|
|
237
330
|
- Sandbox dies with parent process
|
|
238
331
|
- Encrypted volumes mount in isolated namespace (invisible on host)
|
|
239
|
-
-
|
|
332
|
+
- Writable paths are auto-created (as directories) if they don't exist;
|
|
333
|
+
paths that already exist are bound as-is, so a file entry binds that file
|
|
240
334
|
|
|
241
335
|
Run your editor from inside the sandbox if it has any capacity to run
|
|
242
336
|
linters, hooks, or anything else from the project environment. A
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "projectwrap"
|
|
3
|
-
version = "202604.
|
|
3
|
+
version = "202604.3"
|
|
4
4
|
description = "Isolated project environments with bubblewrap sandboxing"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -54,4 +54,8 @@ strict = true
|
|
|
54
54
|
|
|
55
55
|
[tool.pytest.ini_options]
|
|
56
56
|
testpaths = ["tests"]
|
|
57
|
-
addopts = "-v"
|
|
57
|
+
addopts = "-v"
|
|
58
|
+
markers = [
|
|
59
|
+
"integration: end-to-end tests that exec bwrap/gocryptfs/unshare (opt-in)",
|
|
60
|
+
"real_probe: tests that exercise the real bwrap feature probe (not stubbed)",
|
|
61
|
+
]
|
|
@@ -118,13 +118,17 @@ def build_bwrap_args(
|
|
|
118
118
|
uid = os.getuid()
|
|
119
119
|
args.extend(["--tmpfs", f"/run/user/{uid}"])
|
|
120
120
|
|
|
121
|
+
# Mask docker socket — connect() works on ro-bound sockets, so this
|
|
122
|
+
# is a full sandbox escape to root if docker is running
|
|
123
|
+
args.extend(["--ro-bind-try", "/dev/null", "/run/docker.sock"])
|
|
124
|
+
|
|
121
125
|
# Always blacklist the config directory (prevents reading other project configs
|
|
122
126
|
# or modifying sandbox rules from inside the sandbox)
|
|
123
127
|
config_dir_resolved = get_config_dir().resolve()
|
|
124
128
|
if config_dir_resolved.exists():
|
|
125
129
|
args.extend(["--tmpfs", str(config_dir_resolved)])
|
|
126
130
|
|
|
127
|
-
# Blacklist paths by overlaying with tmpfs
|
|
131
|
+
# Blacklist paths by overlaying with tmpfs (dirs) or /dev/null (files)
|
|
128
132
|
blacklist_paths: list[Path] = [config_dir_resolved]
|
|
129
133
|
for path in sandbox.get("blacklist", []):
|
|
130
134
|
p = expand_path(path)
|
|
@@ -135,7 +139,10 @@ def build_bwrap_args(
|
|
|
135
139
|
)
|
|
136
140
|
# bwrap can't mount tmpfs over symlinks — resolve to the real path
|
|
137
141
|
mount_path = p.resolve()
|
|
138
|
-
|
|
142
|
+
if mount_path.is_file():
|
|
143
|
+
args.extend(["--ro-bind", "/dev/null", str(mount_path)])
|
|
144
|
+
else:
|
|
145
|
+
args.extend(["--tmpfs", str(mount_path)])
|
|
139
146
|
blacklist_paths.append(mount_path)
|
|
140
147
|
|
|
141
148
|
# Whitelist paths by binding them back (must be under a blacklisted path)
|
|
@@ -156,10 +163,7 @@ def build_bwrap_args(
|
|
|
156
163
|
for path in sandbox.get("writable", []):
|
|
157
164
|
p = expand_path(path)
|
|
158
165
|
if not p.exists():
|
|
159
|
-
|
|
160
|
-
f"Writable path does not exist: {p}\n"
|
|
161
|
-
f"Fix your config or remove this entry."
|
|
162
|
-
)
|
|
166
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
163
167
|
mount_path = p.resolve()
|
|
164
168
|
args.extend(["--bind", str(mount_path), str(mount_path)])
|
|
165
169
|
|
|
@@ -204,6 +208,14 @@ def build_bwrap_args(
|
|
|
204
208
|
if sandbox.get("unshare_pid", True):
|
|
205
209
|
args.append("--unshare-pid")
|
|
206
210
|
|
|
211
|
+
# Clean environment: clear everything, pass through essentials
|
|
212
|
+
if sandbox.get("clean_env", False):
|
|
213
|
+
args.append("--clearenv")
|
|
214
|
+
for var in ("PATH", "HOME", "USER", "SHELL", "TERM", "LANG"):
|
|
215
|
+
val = os.environ.get(var)
|
|
216
|
+
if val is not None:
|
|
217
|
+
args.extend(["--setenv", var, val])
|
|
218
|
+
|
|
207
219
|
# Set environment variables
|
|
208
220
|
args.extend(["--setenv", "PROJECT_WRAP", "1"])
|
|
209
221
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Dependency checking for optional external tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DepStatus(Enum):
|
|
13
|
+
"""Dependency availability status."""
|
|
14
|
+
|
|
15
|
+
AVAILABLE = "available"
|
|
16
|
+
MISSING = "missing"
|
|
17
|
+
UNSUPPORTED = "unsupported"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Dependency:
|
|
22
|
+
"""External dependency information."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
binary: str
|
|
26
|
+
install_hint: str
|
|
27
|
+
required_for: str
|
|
28
|
+
# Optional feature probe: returns (ok, reason). Runs only if the binary
|
|
29
|
+
# is found on PATH. `reason` is shown to the user on failure.
|
|
30
|
+
feature_probe: Callable[[str], tuple[bool, str]] | None = field(
|
|
31
|
+
default=None, repr=False
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def check(self) -> tuple[DepStatus, str]:
|
|
35
|
+
"""Check if dependency is available and satisfies feature requirements.
|
|
36
|
+
|
|
37
|
+
Returns (status, detail). `detail` is empty on success, otherwise a
|
|
38
|
+
human-readable reason (missing flag, install hint, etc).
|
|
39
|
+
"""
|
|
40
|
+
path = shutil.which(self.binary)
|
|
41
|
+
if not path:
|
|
42
|
+
return DepStatus.MISSING, ""
|
|
43
|
+
if self.feature_probe is not None:
|
|
44
|
+
ok, reason = self.feature_probe(path)
|
|
45
|
+
if not ok:
|
|
46
|
+
return DepStatus.UNSUPPORTED, reason
|
|
47
|
+
return DepStatus.AVAILABLE, ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_bwrap_probe_cache: tuple[bool, str] | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _probe_bwrap(path: str) -> tuple[bool, str]:
|
|
54
|
+
"""Check that bwrap supports --unshare-user and --uid.
|
|
55
|
+
|
|
56
|
+
These flags are required by the vault path (vault.py:_inject_uid) to
|
|
57
|
+
drop from the outer user-ns "root" back to the real uid inside the
|
|
58
|
+
sandbox. Result is cached per-process.
|
|
59
|
+
"""
|
|
60
|
+
global _bwrap_probe_cache
|
|
61
|
+
if _bwrap_probe_cache is not None:
|
|
62
|
+
return _bwrap_probe_cache
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
[path, "--help"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
timeout=5,
|
|
70
|
+
)
|
|
71
|
+
except (OSError, subprocess.TimeoutExpired) as e:
|
|
72
|
+
_bwrap_probe_cache = (False, f"could not run '{path} --help': {e}")
|
|
73
|
+
return _bwrap_probe_cache
|
|
74
|
+
|
|
75
|
+
help_text = result.stdout + result.stderr
|
|
76
|
+
missing = [f for f in ("--unshare-user", "--uid") if f not in help_text]
|
|
77
|
+
if missing:
|
|
78
|
+
_bwrap_probe_cache = (
|
|
79
|
+
False,
|
|
80
|
+
f"bwrap is missing required flags: {', '.join(missing)}. "
|
|
81
|
+
f"Upgrade bubblewrap (>= 0.4) — encrypted vaults need these to "
|
|
82
|
+
f"drop back to your real uid inside the sandbox.",
|
|
83
|
+
)
|
|
84
|
+
return _bwrap_probe_cache
|
|
85
|
+
|
|
86
|
+
_bwrap_probe_cache = (True, "")
|
|
87
|
+
return _bwrap_probe_cache
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Known optional dependencies
|
|
91
|
+
DEPS = {
|
|
92
|
+
"bwrap": Dependency(
|
|
93
|
+
name="bubblewrap",
|
|
94
|
+
binary="bwrap",
|
|
95
|
+
install_hint="sudo apt install bubblewrap",
|
|
96
|
+
required_for="sandbox isolation",
|
|
97
|
+
feature_probe=_probe_bwrap,
|
|
98
|
+
),
|
|
99
|
+
"gocryptfs": Dependency(
|
|
100
|
+
name="gocryptfs",
|
|
101
|
+
binary="gocryptfs",
|
|
102
|
+
install_hint="sudo apt install gocryptfs",
|
|
103
|
+
required_for="encrypted volumes",
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MissingDependencyError(Exception):
|
|
109
|
+
"""Raised when a required dependency is not available."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, dep: Dependency):
|
|
112
|
+
self.dep = dep
|
|
113
|
+
super().__init__(
|
|
114
|
+
f"{dep.name} is required for {dep.required_for} but is not installed.\n"
|
|
115
|
+
f"Install with: {dep.install_hint}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class UnsupportedDependencyError(Exception):
|
|
120
|
+
"""Raised when a dependency is installed but lacks a required feature."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, dep: Dependency, reason: str):
|
|
123
|
+
self.dep = dep
|
|
124
|
+
self.reason = reason
|
|
125
|
+
super().__init__(f"{dep.name}: {reason}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def require_dep(name: str) -> None:
|
|
129
|
+
"""Ensure a dependency is available, raise if not."""
|
|
130
|
+
dep = DEPS.get(name)
|
|
131
|
+
if dep is None:
|
|
132
|
+
raise ValueError(f"Unknown dependency: {name}")
|
|
133
|
+
|
|
134
|
+
status, detail = dep.check()
|
|
135
|
+
if status == DepStatus.MISSING:
|
|
136
|
+
raise MissingDependencyError(dep)
|
|
137
|
+
if status == DepStatus.UNSUPPORTED:
|
|
138
|
+
raise UnsupportedDependencyError(dep, detail)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def check_optional_deps(verbose: bool = False) -> dict[str, DepStatus]:
|
|
142
|
+
"""Check all optional dependencies and return their status."""
|
|
143
|
+
results = {}
|
|
144
|
+
|
|
145
|
+
for name, dep in DEPS.items():
|
|
146
|
+
status, detail = dep.check()
|
|
147
|
+
results[name] = status
|
|
148
|
+
|
|
149
|
+
if verbose:
|
|
150
|
+
icon = "✓" if status == DepStatus.AVAILABLE else "✗"
|
|
151
|
+
status_text = status.value
|
|
152
|
+
print(f" {icon} {dep.name} ({dep.binary}): {status_text}")
|
|
153
|
+
if status == DepStatus.MISSING:
|
|
154
|
+
print(f" Install: {dep.install_hint}")
|
|
155
|
+
print(f" Used for: {dep.required_for}")
|
|
156
|
+
elif status == DepStatus.UNSUPPORTED:
|
|
157
|
+
print(f" {detail}")
|
|
158
|
+
|
|
159
|
+
return results
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{name}"
|
|
3
|
+
dir = "{dir}"
|
|
4
|
+
shell = "{shell}"
|
|
5
|
+
|
|
6
|
+
[sandbox]
|
|
7
|
+
enabled = {sandbox_enabled}
|
|
8
|
+
# Note: the config directory (~/.config/pwrap) is always blacklisted automatically.
|
|
9
|
+
blacklist = [ # Paths to hide (overlaid with tmpfs)
|
|
10
|
+
"~/.ssh", # SSH keys and agent config
|
|
11
|
+
"~/.gnupg", # GPG keys
|
|
12
|
+
"~/.aws", # AWS credentials and config
|
|
13
|
+
"~/.kube", # Kubernetes contexts and tokens
|
|
14
|
+
"~/.config/gcloud", # GCP credentials and auth tokens
|
|
15
|
+
"~/.azure", # Azure CLI credentials
|
|
16
|
+
"~/.docker", # Docker config and auth tokens
|
|
17
|
+
"~/.npmrc", # npm auth tokens
|
|
18
|
+
"~/.pypirc", # PyPI upload tokens
|
|
19
|
+
"~/.boto", # Legacy GCS/S3 credentials
|
|
20
|
+
"/mnt", # WSL: Windows drives. Without this, cmd.exe and
|
|
21
|
+
# powershell.exe are callable via binfmt_misc
|
|
22
|
+
# interop — a full sandbox escape.
|
|
23
|
+
]
|
|
24
|
+
# whitelist = [ # Exceptions to blacklist (bound back read-only)
|
|
25
|
+
# "~/.kube/{name}",
|
|
26
|
+
# "/mnt/wsl", # WSL config — /etc/resolv.conf symlinks here,
|
|
27
|
+
# # so DNS breaks without it when /mnt is blacklisted.
|
|
28
|
+
# # Add "/mnt/wslg/..." writable for GUI apps.
|
|
29
|
+
# ]
|
|
30
|
+
# writable = [ # Extra writable paths (home is read-only)
|
|
31
|
+
# "~/.pyenv/shims",
|
|
32
|
+
# ]
|
|
33
|
+
# unshare_net = false # Isolate network namespace
|
|
34
|
+
# unshare_pid = true # Isolate PID namespace (default: true)
|
|
35
|
+
# new_session = true # TIOCSTI protection (auto on kernels < 6.2)
|
|
36
|
+
# clean_env = false # Clear env, pass through only PATH/HOME/USER/
|
|
37
|
+
# # SHELL/TERM/LANG. Add others via [env].
|
|
38
|
+
|
|
39
|
+
# [env] # environment variables (set before shell starts)
|
|
40
|
+
# XDG_DATA_HOME = "vault/.config" # ~ expanded for values starting with ~/
|
|
41
|
+
# AICHAT_CONFIG_DIR = "~/.local/share/aichat"
|
|
42
|
+
|
|
43
|
+
# [encrypted] # gocryptfs encrypted volume (sandbox only)
|
|
44
|
+
# cipherdir = "encrypted" # Relative to config dir, or absolute
|
|
45
|
+
# mountpoint = "~/.local/share/myapp" # Where decrypted files appear
|
|
46
|
+
# shared = false # true = share one mount across terminals; first
|
|
47
|
+
# # terminal is primary, attached terminals die with it.
|
|
48
|
+
# Inside the sandbox, $PWRAP_VAULT_DIR points at the mountpoint.
|
|
@@ -228,7 +228,10 @@ def _run_single(config: VaultConfig, bwrap_argv: list[str]) -> int:
|
|
|
228
228
|
return 130
|
|
229
229
|
os.set_inheritable(lock_fd, True)
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
# Capture real uid/gid before entering --map-root-user namespace; inject
|
|
232
|
+
# into bwrap so the sandbox shell drops back to the real user identity.
|
|
233
|
+
bwrap_with_uid = _inject_uid(bwrap_argv, os.getuid(), os.getgid())
|
|
234
|
+
bwrap_cmd = " ".join(shlex.quote(a) for a in bwrap_with_uid)
|
|
232
235
|
inner = (
|
|
233
236
|
f"gocryptfs {shlex.quote(str(config.cipherdir))} "
|
|
234
237
|
f"{shlex.quote(str(config.mountpoint))} && "
|
|
@@ -242,6 +245,31 @@ def _run_single(config: VaultConfig, bwrap_argv: list[str]) -> int:
|
|
|
242
245
|
return 1 # unreachable
|
|
243
246
|
|
|
244
247
|
|
|
248
|
+
def _inject_uid(bwrap_argv: list[str], uid: int, gid: int) -> list[str]:
|
|
249
|
+
"""Inject --unshare-user --uid --gid so bwrap drops back to the real uid.
|
|
250
|
+
|
|
251
|
+
User-namespace mapping chain for the vault path:
|
|
252
|
+
|
|
253
|
+
1. Outer: `unshare --user --map-root-user` maps real uid -> 0 so we can
|
|
254
|
+
mount gocryptfs (FUSE requires "root" in the user ns).
|
|
255
|
+
2. gocryptfs mounts; files inside it are owned by uid 0 in the outer ns
|
|
256
|
+
(= the real uid on the host kernel).
|
|
257
|
+
3. bwrap creates a nested user ns via `--unshare-user` and writes a
|
|
258
|
+
uid_map of {outer 0 -> nested REAL_UID}. We can only do this because
|
|
259
|
+
we are "root" in the outer ns, which can write arbitrary mappings
|
|
260
|
+
for a child ns.
|
|
261
|
+
4. Processes inside the sandbox see files owned by REAL_UID and
|
|
262
|
+
`id -u` / `whoami` report the real user — not root.
|
|
263
|
+
|
|
264
|
+
Requires bwrap >= 0.4 with --unshare-user and --uid support; this is
|
|
265
|
+
verified at startup by deps._probe_bwrap. The end-to-end chain is
|
|
266
|
+
exercised by tests/test_vault_integration.py.
|
|
267
|
+
"""
|
|
268
|
+
argv = list(bwrap_argv)
|
|
269
|
+
argv[1:1] = ["--unshare-user", "--uid", str(uid), "--gid", str(gid)]
|
|
270
|
+
return argv
|
|
271
|
+
|
|
272
|
+
|
|
245
273
|
def _exec_primary_serve(
|
|
246
274
|
config: VaultConfig, bwrap_argv: list[str], lock_fd: int
|
|
247
275
|
) -> None:
|
|
@@ -259,6 +287,8 @@ def _exec_primary_serve(
|
|
|
259
287
|
"--mountpoint", str(config.mountpoint),
|
|
260
288
|
"--project", config.project_name,
|
|
261
289
|
"--sock-path", str(sock),
|
|
290
|
+
"--real-uid", str(os.getuid()),
|
|
291
|
+
"--real-gid", str(os.getgid()),
|
|
262
292
|
"--bwrap-argv", json.dumps(bwrap_argv),
|
|
263
293
|
]
|
|
264
294
|
os.execvp("unshare", argv)
|
|
@@ -325,7 +355,13 @@ def _inject_token(bwrap_argv: list[str], token: str) -> list[str]:
|
|
|
325
355
|
return argv
|
|
326
356
|
|
|
327
357
|
|
|
328
|
-
def serve(
|
|
358
|
+
def serve(
|
|
359
|
+
config: VaultConfig,
|
|
360
|
+
sock_path: Path,
|
|
361
|
+
bwrap_argv: list[str],
|
|
362
|
+
real_uid: int,
|
|
363
|
+
real_gid: int,
|
|
364
|
+
) -> None:
|
|
329
365
|
"""Primary session: mount, fork primary bwrap, accept attached clients.
|
|
330
366
|
|
|
331
367
|
Called via `python -m project_wrap.vault serve ...` after unshare. Runs in
|
|
@@ -359,7 +395,7 @@ def serve(config: VaultConfig, sock_path: Path, bwrap_argv: list[str]) -> None:
|
|
|
359
395
|
signal.signal(signal.SIGHUP, handle_signal)
|
|
360
396
|
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
|
361
397
|
|
|
362
|
-
primary_argv = _inject_token(bwrap_argv, token)
|
|
398
|
+
primary_argv = _inject_uid(_inject_token(bwrap_argv, token), real_uid, real_gid)
|
|
363
399
|
|
|
364
400
|
primary_pid = os.fork()
|
|
365
401
|
if primary_pid == 0:
|
|
@@ -452,7 +488,9 @@ def serve(config: VaultConfig, sock_path: Path, bwrap_argv: list[str]) -> None:
|
|
|
452
488
|
pass
|
|
453
489
|
continue
|
|
454
490
|
|
|
455
|
-
child_argv =
|
|
491
|
+
child_argv = _inject_uid(
|
|
492
|
+
_inject_token(msg["argv"], token), real_uid, real_gid
|
|
493
|
+
)
|
|
456
494
|
|
|
457
495
|
proxy_pid = os.fork()
|
|
458
496
|
if proxy_pid == 0:
|
|
@@ -546,6 +584,8 @@ def main() -> None:
|
|
|
546
584
|
parser.add_argument("--mountpoint", required=True)
|
|
547
585
|
parser.add_argument("--project", required=True)
|
|
548
586
|
parser.add_argument("--sock-path", required=True)
|
|
587
|
+
parser.add_argument("--real-uid", type=int, required=True)
|
|
588
|
+
parser.add_argument("--real-gid", type=int, required=True)
|
|
549
589
|
parser.add_argument("--bwrap-argv", required=True)
|
|
550
590
|
args = parser.parse_args()
|
|
551
591
|
|
|
@@ -556,7 +596,7 @@ def main() -> None:
|
|
|
556
596
|
shared=True,
|
|
557
597
|
)
|
|
558
598
|
bwrap_argv = json.loads(args.bwrap_argv)
|
|
559
|
-
serve(config, Path(args.sock_path), bwrap_argv)
|
|
599
|
+
serve(config, Path(args.sock_path), bwrap_argv, args.real_uid, args.real_gid)
|
|
560
600
|
|
|
561
601
|
|
|
562
602
|
if __name__ == "__main__":
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""Dependency checking for optional external tools."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import shutil
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from enum import Enum
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class DepStatus(Enum):
|
|
11
|
-
"""Dependency availability status."""
|
|
12
|
-
|
|
13
|
-
AVAILABLE = "available"
|
|
14
|
-
MISSING = "missing"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class Dependency:
|
|
19
|
-
"""External dependency information."""
|
|
20
|
-
|
|
21
|
-
name: str
|
|
22
|
-
binary: str
|
|
23
|
-
install_hint: str
|
|
24
|
-
required_for: str
|
|
25
|
-
|
|
26
|
-
def check(self) -> DepStatus:
|
|
27
|
-
"""Check if dependency is available."""
|
|
28
|
-
if shutil.which(self.binary):
|
|
29
|
-
return DepStatus.AVAILABLE
|
|
30
|
-
return DepStatus.MISSING
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# Known optional dependencies
|
|
34
|
-
DEPS = {
|
|
35
|
-
"bwrap": Dependency(
|
|
36
|
-
name="bubblewrap",
|
|
37
|
-
binary="bwrap",
|
|
38
|
-
install_hint="sudo apt install bubblewrap",
|
|
39
|
-
required_for="sandbox isolation",
|
|
40
|
-
),
|
|
41
|
-
"gocryptfs": Dependency(
|
|
42
|
-
name="gocryptfs",
|
|
43
|
-
binary="gocryptfs",
|
|
44
|
-
install_hint="sudo apt install gocryptfs",
|
|
45
|
-
required_for="encrypted volumes",
|
|
46
|
-
),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class MissingDependencyError(Exception):
|
|
51
|
-
"""Raised when a required dependency is not available."""
|
|
52
|
-
|
|
53
|
-
def __init__(self, dep: Dependency):
|
|
54
|
-
self.dep = dep
|
|
55
|
-
super().__init__(
|
|
56
|
-
f"{dep.name} is required for {dep.required_for} but is not installed.\n"
|
|
57
|
-
f"Install with: {dep.install_hint}"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def require_dep(name: str) -> None:
|
|
62
|
-
"""Ensure a dependency is available, raise if not."""
|
|
63
|
-
dep = DEPS.get(name)
|
|
64
|
-
if dep is None:
|
|
65
|
-
raise ValueError(f"Unknown dependency: {name}")
|
|
66
|
-
|
|
67
|
-
if dep.check() == DepStatus.MISSING:
|
|
68
|
-
raise MissingDependencyError(dep)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def check_optional_deps(verbose: bool = False) -> dict[str, DepStatus]:
|
|
72
|
-
"""Check all optional dependencies and return their status."""
|
|
73
|
-
results = {}
|
|
74
|
-
|
|
75
|
-
for name, dep in DEPS.items():
|
|
76
|
-
status = dep.check()
|
|
77
|
-
results[name] = status
|
|
78
|
-
|
|
79
|
-
if verbose:
|
|
80
|
-
icon = "✓" if status == DepStatus.AVAILABLE else "✗"
|
|
81
|
-
status_text = "available" if status == DepStatus.AVAILABLE else "missing"
|
|
82
|
-
print(f" {icon} {dep.name} ({dep.binary}): {status_text}")
|
|
83
|
-
if status == DepStatus.MISSING:
|
|
84
|
-
print(f" Install: {dep.install_hint}")
|
|
85
|
-
print(f" Used for: {dep.required_for}")
|
|
86
|
-
|
|
87
|
-
return results
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "{name}"
|
|
3
|
-
dir = "{dir}"
|
|
4
|
-
shell = "{shell}"
|
|
5
|
-
|
|
6
|
-
[sandbox]
|
|
7
|
-
enabled = {sandbox_enabled}
|
|
8
|
-
# Note: the config directory (~/.config/pwrap) is always blacklisted automatically.
|
|
9
|
-
# blacklist = [ # Additional paths to hide (overlaid with tmpfs)
|
|
10
|
-
# "~/.kube",
|
|
11
|
-
# "~/.aws",
|
|
12
|
-
# "~/.ssh",
|
|
13
|
-
# ]
|
|
14
|
-
# whitelist = [ # Exceptions to blacklist (bound back)
|
|
15
|
-
# "~/.kube/{name}",
|
|
16
|
-
# ]
|
|
17
|
-
# writable = [ # Extra writable paths (home is read-only)
|
|
18
|
-
# "~/.pyenv/shims",
|
|
19
|
-
# ]
|
|
20
|
-
# unshare_net = false # Isolate network namespace
|
|
21
|
-
# unshare_pid = true # Isolate PID namespace (default: true)
|
|
22
|
-
# new_session = true # TIOCSTI protection (auto on kernels < 6.2)
|
|
23
|
-
|
|
24
|
-
# [env] # environment variables (set before shell starts)
|
|
25
|
-
# XDG_DATA_HOME = "vault/.config" # ~ expanded for values starting with ~/
|
|
26
|
-
# AICHAT_CONFIG_DIR = "~/.local/share/aichat"
|
|
27
|
-
|
|
28
|
-
# [encrypted] # gocryptfs encrypted volume (sandbox only)
|
|
29
|
-
# cipherdir = "encrypted" # Relative to config dir, or absolute
|
|
30
|
-
# mountpoint = "~/.local/share/myapp" # Where decrypted files appear
|
|
31
|
-
# shared = false # true = share one mount across terminals; first
|
|
32
|
-
# # terminal is primary, attached terminals die with it.
|
|
33
|
-
# Inside the sandbox, $PWRAP_VAULT_DIR points at the mountpoint.
|
|
34
|
-
# Note: encrypted volumes require unshare --user --map-root-user, so
|
|
35
|
-
# `whoami` / `id -u` will report root inside the sandbox (user namespace only,
|
|
36
|
-
# not real root).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|