projectwrap 202604.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.
- projectwrap-202604.1/LICENSE +21 -0
- projectwrap-202604.1/PKG-INFO +279 -0
- projectwrap-202604.1/README.md +254 -0
- projectwrap-202604.1/pyproject.toml +57 -0
- projectwrap-202604.1/src/project_wrap/__init__.py +3 -0
- projectwrap-202604.1/src/project_wrap/cli.py +115 -0
- projectwrap-202604.1/src/project_wrap/core.py +566 -0
- projectwrap-202604.1/src/project_wrap/deps.py +87 -0
- projectwrap-202604.1/src/project_wrap/templates/init.fish +21 -0
- projectwrap-202604.1/src/project_wrap/templates/init.sh +21 -0
- projectwrap-202604.1/src/project_wrap/templates/project.toml +32 -0
- projectwrap-202604.1/src/project_wrap/validate.py +133 -0
- projectwrap-202604.1/src/project_wrap/vault.py +563 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Fredrik Håård
|
|
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,279 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: projectwrap
|
|
3
|
+
Version: 202604.1
|
|
4
|
+
Summary: Isolated project environments with bubblewrap sandboxing
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: sandbox,bubblewrap,bwrap,isolation,development,security
|
|
7
|
+
Author: Fredrik Håård
|
|
8
|
+
Author-email: fredrik@haard.se
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Project-URL: Homepage, https://github.com/haard/project-wrap
|
|
22
|
+
Project-URL: Repository, https://github.com/haard/project-wrap
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# pwrap
|
|
26
|
+
|
|
27
|
+
pwrap wraps project shell environments in bubblewrap sandboxes, and aims to
|
|
28
|
+
_limit the blast radius_ of e.g. supply chain attacks and protect your production
|
|
29
|
+
infrastructure from your sloppy side project.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
**Why**
|
|
34
|
+
I got tired of my ad-hoc fish bwrap scripts, and I'm increasingly worried about supply chain attacks.
|
|
35
|
+
|
|
36
|
+
If every side project feels like a potential vector — one [npm|pip|cargo] install away from pwned AWS credentials,
|
|
37
|
+
and custom-wrapping with bwrap and some vault product feels too fragile or too much work, pwrap might help.
|
|
38
|
+
|
|
39
|
+
**Status**
|
|
40
|
+
pwrap _works_ but has not been tested by anyone but me, and has not been
|
|
41
|
+
reviewed/audited by anyone except Opus 4.6 and codestral. pwrap comes with
|
|
42
|
+
_absolutely no warranty_.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
**Does:**
|
|
46
|
+
- Launches sandboxed shells with per-project filesystem isolation
|
|
47
|
+
- Hides sensitive paths (credentials, configs, SSH keys) via tmpfs overlays
|
|
48
|
+
- Exposes only what each project needs (whitelisting)
|
|
49
|
+
- Mounts gocryptfs encrypted volumes inside isolated namespaces
|
|
50
|
+
- Runs init scripts for env vars, venv activation, aliases
|
|
51
|
+
|
|
52
|
+
**Doesn't do:**
|
|
53
|
+
- Network filtering (it's all-or-nothing via `unshare_net`)
|
|
54
|
+
- Container-level isolation (no cgroups, no seccomp, no resource limits)
|
|
55
|
+
- Do package management or dependency resolution
|
|
56
|
+
- Protect you from your own misconfiguration
|
|
57
|
+
|
|
58
|
+
**Dependencies:** Python 3.11+ (stdlib only, no pip dependencies),
|
|
59
|
+
[bubblewrap](https://github.com/containers/bubblewrap) for sandboxing,
|
|
60
|
+
[gocryptfs](https://nuetzlich.net/gocryptfs/) for encrypted volumes.
|
|
61
|
+
|
|
62
|
+
**Design principles:**
|
|
63
|
+
- **Reviewable** — small codebase, no pip dependencies, no magic
|
|
64
|
+
- **Fail fast** — invalid config is an error, not a warning
|
|
65
|
+
- **Explicit over convenient** — no implicit defaults that hide security decisions
|
|
66
|
+
- **Init scripts as the extension point** — env vars, venv, aliases all go there,
|
|
67
|
+
not in config schema
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
pwrap has no pip dependencies — only the standard library. For a security tool,
|
|
72
|
+
installing from source lets you audit what you're running:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
git clone https://github.com/haard/projectwrap
|
|
76
|
+
cd projectwrap
|
|
77
|
+
pip install --no-deps .
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or from PyPI if you prefer convenience: `pipx install projectwrap`
|
|
81
|
+
|
|
82
|
+
Check optional dependencies with `pwrap --check-deps`.
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pwrap --new ~/projects/myproject # creates config + init script
|
|
88
|
+
# edit ~/.config/pwrap/myproject/project.toml and init script
|
|
89
|
+
pwrap myproject # launch sandboxed shell
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
On first run, `--new` creates editable templates in `~/.config/pwrap/`. Edit them
|
|
93
|
+
to set your defaults, then run `--new` again.
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
### Basic sandboxed project
|
|
98
|
+
|
|
99
|
+
`~/.config/pwrap/myproject/project.toml`:
|
|
100
|
+
```toml
|
|
101
|
+
[project]
|
|
102
|
+
name = "myproject"
|
|
103
|
+
dir = "~/projects/myproject"
|
|
104
|
+
shell = "/usr/bin/fish"
|
|
105
|
+
|
|
106
|
+
[sandbox]
|
|
107
|
+
enabled = true
|
|
108
|
+
blacklist = [
|
|
109
|
+
"~/.kube",
|
|
110
|
+
"~/.aws",
|
|
111
|
+
"~/.ssh",
|
|
112
|
+
]
|
|
113
|
+
whitelist = [
|
|
114
|
+
"~/.kube/myproject",
|
|
115
|
+
"~/.ssh/myproject_ed25519",
|
|
116
|
+
"~/.ssh/known_hosts",
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`~/.config/pwrap/myproject/init.fish`:
|
|
121
|
+
```fish
|
|
122
|
+
source .venv/bin/activate.fish
|
|
123
|
+
set -gx KUBECONFIG ~/.kube/myproject/config
|
|
124
|
+
set -gx GIT_SSH_COMMAND "ssh -i ~/.ssh/myproject_ed25519 -o IdentitiesOnly=yes"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The config directory (`~/.config/pwrap`) is always blacklisted automatically — code
|
|
128
|
+
inside the sandbox cannot read other project configs.
|
|
129
|
+
|
|
130
|
+
### Encrypted AI chat history
|
|
131
|
+
|
|
132
|
+
Keep aichat/Claude chat history encrypted at rest, decrypted only inside the sandbox.
|
|
133
|
+
Uses gocryptfs mounted in an isolated namespace (invisible on host).
|
|
134
|
+
|
|
135
|
+
**Setup:**
|
|
136
|
+
```bash
|
|
137
|
+
# Initialize encrypted directory (once, prompts for password)
|
|
138
|
+
mkdir -p ~/.config/pwrap/myproject/encrypted
|
|
139
|
+
gocryptfs -init ~/.config/pwrap/myproject/encrypted
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Config** (`project.toml`):
|
|
143
|
+
```toml
|
|
144
|
+
[project]
|
|
145
|
+
name = "myproject"
|
|
146
|
+
dir = "~/projects/myproject"
|
|
147
|
+
shell = "/usr/bin/fish"
|
|
148
|
+
|
|
149
|
+
[sandbox]
|
|
150
|
+
enabled = true
|
|
151
|
+
|
|
152
|
+
[encrypted]
|
|
153
|
+
cipherdir = "encrypted"
|
|
154
|
+
mountpoint = "~/.local/share/aichat"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Init script** (`init.fish`):
|
|
158
|
+
```fish
|
|
159
|
+
set -gx AICHAT_CONFIG_DIR ~/.local/share/aichat
|
|
160
|
+
# For Claude Code:
|
|
161
|
+
# set -gx CLAUDE_CONFIG_DIR ~/.local/share/claude
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
On `pwrap myproject`, gocryptfs prompts for the password, mounts the decrypted
|
|
165
|
+
volume inside an isolated mount namespace, and launches the sandboxed shell.
|
|
166
|
+
The decrypted files are invisible to host processes and disappear when the shell
|
|
167
|
+
exits.
|
|
168
|
+
|
|
169
|
+
If you enter the wrong password, gocryptfs re-prompts up to its own retry limit
|
|
170
|
+
and then exits non-zero. pwrap aborts on that failure without launching the
|
|
171
|
+
sandbox — no shell starts, no partial mount is left behind. Re-run `pwrap
|
|
172
|
+
myproject` to try again.
|
|
173
|
+
|
|
174
|
+
**`PWRAP_VAULT_DIR`**: inside any sandbox with an `[encrypted]` section, pwrap
|
|
175
|
+
exports `PWRAP_VAULT_DIR` pointing at the mountpoint. Use it from init scripts
|
|
176
|
+
or app configs to redirect history/state into the vault without hardcoding the
|
|
177
|
+
path per project (e.g. `set savehist-file (expand-file-name "history" "$PWRAP_VAULT_DIR/emacs")`).
|
|
178
|
+
|
|
179
|
+
**You will appear as root inside encrypted projects.** Mounting gocryptfs
|
|
180
|
+
unprivileged requires `unshare --user --map-root-user`, so inside the sandbox
|
|
181
|
+
`whoami` reports `root` and `id -u` reports `0`. This is a user-namespace
|
|
182
|
+
remapping only — you have no real privileges on the host, cannot read
|
|
183
|
+
root-owned files outside the namespace, and cannot escalate. Your real files
|
|
184
|
+
(project dir, home bind-mounts, the vault mountpoint itself) remain owned by
|
|
185
|
+
your real uid. Scripts that gate on `[ "$UID" = 0 ]` will misbehave; prefer
|
|
186
|
+
`[ "$USER" = root ]` checks or, better, check the presence of
|
|
187
|
+
`$PROJECT_WRAP` / `$PWRAP_VAULT_DIR`.
|
|
188
|
+
|
|
189
|
+
**Multiple terminals** (`shared = false`, default): each terminal gets an
|
|
190
|
+
independent gocryptfs mount. Writes to different files merge on next session;
|
|
191
|
+
writes to the same file from two sessions may lose the first session's changes.
|
|
192
|
+
pwrap warns and prompts for confirmation when a concurrent session is detected.
|
|
193
|
+
|
|
194
|
+
**Shared mode** (`shared = true`): the first terminal becomes the **primary**
|
|
195
|
+
session. It mounts gocryptfs, prints a vault token, and stays in the
|
|
196
|
+
foreground of the terminal that launched it (no background daemon — the
|
|
197
|
+
serve process is a normal child of your shell). Additional terminals running
|
|
198
|
+
`pwrap myproject` prompt for the token and attach as children of the primary.
|
|
199
|
+
Inside the sandbox, `echo $PWRAP_VAULT_TOKEN` retrieves the token. When the
|
|
200
|
+
primary exits (shell exit, Ctrl-C, or closing its terminal), all attached
|
|
201
|
+
terminals are terminated and the mount is released — keep the primary
|
|
202
|
+
terminal open for the duration of the session.
|
|
203
|
+
|
|
204
|
+
### GUI apps in sandbox (WSL2)
|
|
205
|
+
|
|
206
|
+
To run emacs or other GUI apps inside the sandbox on WSL2:
|
|
207
|
+
|
|
208
|
+
```toml
|
|
209
|
+
[sandbox]
|
|
210
|
+
writable = [
|
|
211
|
+
"/tmp/.X11-unix", # X11 display socket
|
|
212
|
+
"/mnt/wslg/runtime-dir", # Wayland + PulseAudio
|
|
213
|
+
]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
`pwrap --new` generates a `project.toml` template with all options documented.
|
|
219
|
+
The three config sections:
|
|
220
|
+
|
|
221
|
+
| Section | Purpose |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `[project]` | name, dir, shell |
|
|
224
|
+
| `[sandbox]` | blacklist, whitelist, writable, namespace options |
|
|
225
|
+
| `[encrypted]` | gocryptfs cipherdir, mountpoint, shared mode |
|
|
226
|
+
|
|
227
|
+
Init scripts (`init.fish` or `init.sh`) run inside the sandbox for env vars,
|
|
228
|
+
venv activation, aliases, and tool version switching.
|
|
229
|
+
|
|
230
|
+
## Usage
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pwrap # list projects
|
|
234
|
+
pwrap myproject # launch project
|
|
235
|
+
pwrap -v myproject # verbose output
|
|
236
|
+
pwrap --new ~/projects/myproject # create config (name from dir)
|
|
237
|
+
pwrap --new ~/projects/myproject custom # create with explicit name
|
|
238
|
+
pwrap --new --shell /bin/bash ~/projects/x # specify shell
|
|
239
|
+
pwrap --check-deps # check optional dependencies
|
|
240
|
+
pwrap --version # show version
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Security Defaults
|
|
244
|
+
|
|
245
|
+
When sandboxing is enabled:
|
|
246
|
+
|
|
247
|
+
- Home is **read-only**; only the project directory is writable
|
|
248
|
+
- Config directory (`~/.config/pwrap`) is always blacklisted
|
|
249
|
+
- PID and IPC namespaces are isolated
|
|
250
|
+
- TIOCSTI injection blocked automatically on kernels < 6.2
|
|
251
|
+
- XDG runtime directory isolated
|
|
252
|
+
- Sandbox dies with parent process
|
|
253
|
+
- Encrypted volumes mount in isolated namespace (invisible on host)
|
|
254
|
+
- All paths in shell commands are quoted to prevent injection
|
|
255
|
+
|
|
256
|
+
## Shell Completions
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# Fish
|
|
260
|
+
cp completions/project.fish ~/.config/fish/completions/pwrap.fish
|
|
261
|
+
# Bash
|
|
262
|
+
cp completions/project.bash /etc/bash_completion.d/pwrap
|
|
263
|
+
# Zsh
|
|
264
|
+
cp completions/_project ~/.local/share/zsh/site-functions/_pwrap
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
poetry install # install with dev dependencies
|
|
271
|
+
poetry run pytest # run tests
|
|
272
|
+
poetry run ruff check src/ # lint
|
|
273
|
+
poetry run mypy src/ # type check
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT
|
|
279
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# pwrap
|
|
2
|
+
|
|
3
|
+
pwrap wraps project shell environments in bubblewrap sandboxes, and aims to
|
|
4
|
+
_limit the blast radius_ of e.g. supply chain attacks and protect your production
|
|
5
|
+
infrastructure from your sloppy side project.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
**Why**
|
|
10
|
+
I got tired of my ad-hoc fish bwrap scripts, and I'm increasingly worried about supply chain attacks.
|
|
11
|
+
|
|
12
|
+
If every side project feels like a potential vector — one [npm|pip|cargo] install away from pwned AWS credentials,
|
|
13
|
+
and custom-wrapping with bwrap and some vault product feels too fragile or too much work, pwrap might help.
|
|
14
|
+
|
|
15
|
+
**Status**
|
|
16
|
+
pwrap _works_ but has not been tested by anyone but me, and has not been
|
|
17
|
+
reviewed/audited by anyone except Opus 4.6 and codestral. pwrap comes with
|
|
18
|
+
_absolutely no warranty_.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
**Does:**
|
|
22
|
+
- Launches sandboxed shells with per-project filesystem isolation
|
|
23
|
+
- Hides sensitive paths (credentials, configs, SSH keys) via tmpfs overlays
|
|
24
|
+
- Exposes only what each project needs (whitelisting)
|
|
25
|
+
- Mounts gocryptfs encrypted volumes inside isolated namespaces
|
|
26
|
+
- Runs init scripts for env vars, venv activation, aliases
|
|
27
|
+
|
|
28
|
+
**Doesn't do:**
|
|
29
|
+
- Network filtering (it's all-or-nothing via `unshare_net`)
|
|
30
|
+
- Container-level isolation (no cgroups, no seccomp, no resource limits)
|
|
31
|
+
- Do package management or dependency resolution
|
|
32
|
+
- Protect you from your own misconfiguration
|
|
33
|
+
|
|
34
|
+
**Dependencies:** Python 3.11+ (stdlib only, no pip dependencies),
|
|
35
|
+
[bubblewrap](https://github.com/containers/bubblewrap) for sandboxing,
|
|
36
|
+
[gocryptfs](https://nuetzlich.net/gocryptfs/) for encrypted volumes.
|
|
37
|
+
|
|
38
|
+
**Design principles:**
|
|
39
|
+
- **Reviewable** — small codebase, no pip dependencies, no magic
|
|
40
|
+
- **Fail fast** — invalid config is an error, not a warning
|
|
41
|
+
- **Explicit over convenient** — no implicit defaults that hide security decisions
|
|
42
|
+
- **Init scripts as the extension point** — env vars, venv, aliases all go there,
|
|
43
|
+
not in config schema
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
pwrap has no pip dependencies — only the standard library. For a security tool,
|
|
48
|
+
installing from source lets you audit what you're running:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/haard/projectwrap
|
|
52
|
+
cd projectwrap
|
|
53
|
+
pip install --no-deps .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or from PyPI if you prefer convenience: `pipx install projectwrap`
|
|
57
|
+
|
|
58
|
+
Check optional dependencies with `pwrap --check-deps`.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pwrap --new ~/projects/myproject # creates config + init script
|
|
64
|
+
# edit ~/.config/pwrap/myproject/project.toml and init script
|
|
65
|
+
pwrap myproject # launch sandboxed shell
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On first run, `--new` creates editable templates in `~/.config/pwrap/`. Edit them
|
|
69
|
+
to set your defaults, then run `--new` again.
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
### Basic sandboxed project
|
|
74
|
+
|
|
75
|
+
`~/.config/pwrap/myproject/project.toml`:
|
|
76
|
+
```toml
|
|
77
|
+
[project]
|
|
78
|
+
name = "myproject"
|
|
79
|
+
dir = "~/projects/myproject"
|
|
80
|
+
shell = "/usr/bin/fish"
|
|
81
|
+
|
|
82
|
+
[sandbox]
|
|
83
|
+
enabled = true
|
|
84
|
+
blacklist = [
|
|
85
|
+
"~/.kube",
|
|
86
|
+
"~/.aws",
|
|
87
|
+
"~/.ssh",
|
|
88
|
+
]
|
|
89
|
+
whitelist = [
|
|
90
|
+
"~/.kube/myproject",
|
|
91
|
+
"~/.ssh/myproject_ed25519",
|
|
92
|
+
"~/.ssh/known_hosts",
|
|
93
|
+
]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`~/.config/pwrap/myproject/init.fish`:
|
|
97
|
+
```fish
|
|
98
|
+
source .venv/bin/activate.fish
|
|
99
|
+
set -gx KUBECONFIG ~/.kube/myproject/config
|
|
100
|
+
set -gx GIT_SSH_COMMAND "ssh -i ~/.ssh/myproject_ed25519 -o IdentitiesOnly=yes"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The config directory (`~/.config/pwrap`) is always blacklisted automatically — code
|
|
104
|
+
inside the sandbox cannot read other project configs.
|
|
105
|
+
|
|
106
|
+
### Encrypted AI chat history
|
|
107
|
+
|
|
108
|
+
Keep aichat/Claude chat history encrypted at rest, decrypted only inside the sandbox.
|
|
109
|
+
Uses gocryptfs mounted in an isolated namespace (invisible on host).
|
|
110
|
+
|
|
111
|
+
**Setup:**
|
|
112
|
+
```bash
|
|
113
|
+
# Initialize encrypted directory (once, prompts for password)
|
|
114
|
+
mkdir -p ~/.config/pwrap/myproject/encrypted
|
|
115
|
+
gocryptfs -init ~/.config/pwrap/myproject/encrypted
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Config** (`project.toml`):
|
|
119
|
+
```toml
|
|
120
|
+
[project]
|
|
121
|
+
name = "myproject"
|
|
122
|
+
dir = "~/projects/myproject"
|
|
123
|
+
shell = "/usr/bin/fish"
|
|
124
|
+
|
|
125
|
+
[sandbox]
|
|
126
|
+
enabled = true
|
|
127
|
+
|
|
128
|
+
[encrypted]
|
|
129
|
+
cipherdir = "encrypted"
|
|
130
|
+
mountpoint = "~/.local/share/aichat"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Init script** (`init.fish`):
|
|
134
|
+
```fish
|
|
135
|
+
set -gx AICHAT_CONFIG_DIR ~/.local/share/aichat
|
|
136
|
+
# For Claude Code:
|
|
137
|
+
# set -gx CLAUDE_CONFIG_DIR ~/.local/share/claude
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
On `pwrap myproject`, gocryptfs prompts for the password, mounts the decrypted
|
|
141
|
+
volume inside an isolated mount namespace, and launches the sandboxed shell.
|
|
142
|
+
The decrypted files are invisible to host processes and disappear when the shell
|
|
143
|
+
exits.
|
|
144
|
+
|
|
145
|
+
If you enter the wrong password, gocryptfs re-prompts up to its own retry limit
|
|
146
|
+
and then exits non-zero. pwrap aborts on that failure without launching the
|
|
147
|
+
sandbox — no shell starts, no partial mount is left behind. Re-run `pwrap
|
|
148
|
+
myproject` to try again.
|
|
149
|
+
|
|
150
|
+
**`PWRAP_VAULT_DIR`**: inside any sandbox with an `[encrypted]` section, pwrap
|
|
151
|
+
exports `PWRAP_VAULT_DIR` pointing at the mountpoint. Use it from init scripts
|
|
152
|
+
or app configs to redirect history/state into the vault without hardcoding the
|
|
153
|
+
path per project (e.g. `set savehist-file (expand-file-name "history" "$PWRAP_VAULT_DIR/emacs")`).
|
|
154
|
+
|
|
155
|
+
**You will appear as root inside encrypted projects.** Mounting gocryptfs
|
|
156
|
+
unprivileged requires `unshare --user --map-root-user`, so inside the sandbox
|
|
157
|
+
`whoami` reports `root` and `id -u` reports `0`. This is a user-namespace
|
|
158
|
+
remapping only — you have no real privileges on the host, cannot read
|
|
159
|
+
root-owned files outside the namespace, and cannot escalate. Your real files
|
|
160
|
+
(project dir, home bind-mounts, the vault mountpoint itself) remain owned by
|
|
161
|
+
your real uid. Scripts that gate on `[ "$UID" = 0 ]` will misbehave; prefer
|
|
162
|
+
`[ "$USER" = root ]` checks or, better, check the presence of
|
|
163
|
+
`$PROJECT_WRAP` / `$PWRAP_VAULT_DIR`.
|
|
164
|
+
|
|
165
|
+
**Multiple terminals** (`shared = false`, default): each terminal gets an
|
|
166
|
+
independent gocryptfs mount. Writes to different files merge on next session;
|
|
167
|
+
writes to the same file from two sessions may lose the first session's changes.
|
|
168
|
+
pwrap warns and prompts for confirmation when a concurrent session is detected.
|
|
169
|
+
|
|
170
|
+
**Shared mode** (`shared = true`): the first terminal becomes the **primary**
|
|
171
|
+
session. It mounts gocryptfs, prints a vault token, and stays in the
|
|
172
|
+
foreground of the terminal that launched it (no background daemon — the
|
|
173
|
+
serve process is a normal child of your shell). Additional terminals running
|
|
174
|
+
`pwrap myproject` prompt for the token and attach as children of the primary.
|
|
175
|
+
Inside the sandbox, `echo $PWRAP_VAULT_TOKEN` retrieves the token. When the
|
|
176
|
+
primary exits (shell exit, Ctrl-C, or closing its terminal), all attached
|
|
177
|
+
terminals are terminated and the mount is released — keep the primary
|
|
178
|
+
terminal open for the duration of the session.
|
|
179
|
+
|
|
180
|
+
### GUI apps in sandbox (WSL2)
|
|
181
|
+
|
|
182
|
+
To run emacs or other GUI apps inside the sandbox on WSL2:
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
[sandbox]
|
|
186
|
+
writable = [
|
|
187
|
+
"/tmp/.X11-unix", # X11 display socket
|
|
188
|
+
"/mnt/wslg/runtime-dir", # Wayland + PulseAudio
|
|
189
|
+
]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
`pwrap --new` generates a `project.toml` template with all options documented.
|
|
195
|
+
The three config sections:
|
|
196
|
+
|
|
197
|
+
| Section | Purpose |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `[project]` | name, dir, shell |
|
|
200
|
+
| `[sandbox]` | blacklist, whitelist, writable, namespace options |
|
|
201
|
+
| `[encrypted]` | gocryptfs cipherdir, mountpoint, shared mode |
|
|
202
|
+
|
|
203
|
+
Init scripts (`init.fish` or `init.sh`) run inside the sandbox for env vars,
|
|
204
|
+
venv activation, aliases, and tool version switching.
|
|
205
|
+
|
|
206
|
+
## Usage
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
pwrap # list projects
|
|
210
|
+
pwrap myproject # launch project
|
|
211
|
+
pwrap -v myproject # verbose output
|
|
212
|
+
pwrap --new ~/projects/myproject # create config (name from dir)
|
|
213
|
+
pwrap --new ~/projects/myproject custom # create with explicit name
|
|
214
|
+
pwrap --new --shell /bin/bash ~/projects/x # specify shell
|
|
215
|
+
pwrap --check-deps # check optional dependencies
|
|
216
|
+
pwrap --version # show version
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Security Defaults
|
|
220
|
+
|
|
221
|
+
When sandboxing is enabled:
|
|
222
|
+
|
|
223
|
+
- Home is **read-only**; only the project directory is writable
|
|
224
|
+
- Config directory (`~/.config/pwrap`) is always blacklisted
|
|
225
|
+
- PID and IPC namespaces are isolated
|
|
226
|
+
- TIOCSTI injection blocked automatically on kernels < 6.2
|
|
227
|
+
- XDG runtime directory isolated
|
|
228
|
+
- Sandbox dies with parent process
|
|
229
|
+
- Encrypted volumes mount in isolated namespace (invisible on host)
|
|
230
|
+
- All paths in shell commands are quoted to prevent injection
|
|
231
|
+
|
|
232
|
+
## Shell Completions
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
# Fish
|
|
236
|
+
cp completions/project.fish ~/.config/fish/completions/pwrap.fish
|
|
237
|
+
# Bash
|
|
238
|
+
cp completions/project.bash /etc/bash_completion.d/pwrap
|
|
239
|
+
# Zsh
|
|
240
|
+
cp completions/_project ~/.local/share/zsh/site-functions/_pwrap
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
poetry install # install with dev dependencies
|
|
247
|
+
poetry run pytest # run tests
|
|
248
|
+
poetry run ruff check src/ # lint
|
|
249
|
+
poetry run mypy src/ # type check
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "projectwrap"
|
|
3
|
+
version = "202604.1"
|
|
4
|
+
description = "Isolated project environments with bubblewrap sandboxing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = ["Fredrik Håård <fredrik@haard.se>"]
|
|
8
|
+
keywords = ["sandbox", "bubblewrap", "bwrap", "isolation", "development", "security"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Topic :: System :: Systems Administration",
|
|
21
|
+
]
|
|
22
|
+
packages = [{ include = "project_wrap", from = "src" }]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.dependencies]
|
|
25
|
+
python = "^3.11"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^8.0"
|
|
29
|
+
pytest-cov = "^4.0"
|
|
30
|
+
ruff = "^0.4"
|
|
31
|
+
mypy = "^1.0"
|
|
32
|
+
|
|
33
|
+
[tool.poetry.scripts]
|
|
34
|
+
pwrap = "project_wrap.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.poetry.urls]
|
|
37
|
+
Homepage = "https://github.com/haard/project-wrap"
|
|
38
|
+
Repository = "https://github.com/haard/project-wrap"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["poetry-core>=1.0.0"]
|
|
42
|
+
build-backend = "poetry.core.masonry.api"
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
target-version = "py311"
|
|
46
|
+
line-length = 100
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.11"
|
|
53
|
+
strict = true
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
addopts = "-v"
|