boxman 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.
- boxman-0.1.0/PKG-INFO +169 -0
- boxman-0.1.0/README.md +146 -0
- boxman-0.1.0/boxman/__init__.py +0 -0
- boxman-0.1.0/boxman/cli.py +135 -0
- boxman-0.1.0/boxman/data/__init__.py +0 -0
- boxman-0.1.0/boxman/data/ec2-template.yaml +126 -0
- boxman-0.1.0/boxman/data/linux-tools.toml +80 -0
- boxman-0.1.0/boxman/ec2.py +297 -0
- boxman-0.1.0/boxman/system.py +179 -0
- boxman-0.1.0/boxman/user.py +190 -0
- boxman-0.1.0/boxman/verify.py +66 -0
- boxman-0.1.0/boxman.egg-info/PKG-INFO +169 -0
- boxman-0.1.0/boxman.egg-info/SOURCES.txt +19 -0
- boxman-0.1.0/boxman.egg-info/dependency_links.txt +1 -0
- boxman-0.1.0/boxman.egg-info/entry_points.txt +2 -0
- boxman-0.1.0/boxman.egg-info/requires.txt +3 -0
- boxman-0.1.0/boxman.egg-info/top_level.txt +1 -0
- boxman-0.1.0/pyproject.toml +41 -0
- boxman-0.1.0/setup.cfg +4 -0
- boxman-0.1.0/tests/test_ec2.py +50 -0
- boxman-0.1.0/tests/test_vault.py +44 -0
boxman-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: boxman
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Set up and manage an Ubuntu development box
|
|
5
|
+
Project-URL: Homepage, https://github.com/vivainio/boxman
|
|
6
|
+
Project-URL: Documentation, https://vivainio.github.io/boxman/
|
|
7
|
+
Project-URL: Repository, https://github.com/vivainio/boxman
|
|
8
|
+
Project-URL: Issues, https://github.com/vivainio/boxman/issues
|
|
9
|
+
Keywords: ubuntu,development,ec2,cloudformation,cli
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: System :: Systems Administration
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Provides-Extra: ec2
|
|
22
|
+
Requires-Dist: boto3>=1.34; extra == "ec2"
|
|
23
|
+
|
|
24
|
+
# boxman
|
|
25
|
+
|
|
26
|
+
[Read the Boxman book](https://vivainio.github.io/boxman/) for the concepts, setup steps, and command reference.
|
|
27
|
+
|
|
28
|
+
Set up and manage an Ubuntu 24.04 development box. `boxman` is a Python command
|
|
29
|
+
with system, user, and vault operations.
|
|
30
|
+
|
|
31
|
+
## Install a host
|
|
32
|
+
|
|
33
|
+
From this checkout, run the system step as root, then the user step from each
|
|
34
|
+
login account. The first command works with the Ubuntu system Python before
|
|
35
|
+
`uv` is installed:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
sudo python3 -m boxman.cli system
|
|
39
|
+
python3 -m boxman.cli user
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
With `uv` available, run it without installing the package:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uvx --from . boxman vault status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
After publishing the repository, `uvx --from
|
|
49
|
+
git+https://github.com/vivainio/boxman boxman ...` can run the same console
|
|
50
|
+
command. For a persistent command, use `uv tool install .` from this checkout.
|
|
51
|
+
|
|
52
|
+
`boxman system` uses zipget to install the apt packages declared in
|
|
53
|
+
`linux-tools.toml`, sets up Git LFS, and configures rootless Podman for normal
|
|
54
|
+
login users. It accepts explicit usernames, or `--packages-only` for a
|
|
55
|
+
container build. It downloads zipget if no version supporting
|
|
56
|
+
`recipe --system-only` is on root's PATH. A fresh install needs a released
|
|
57
|
+
zipget with that option.
|
|
58
|
+
|
|
59
|
+
`boxman user` installs the tools in the recipe, Node.js 22, Claude Code,
|
|
60
|
+
Copilot CLI, and uv. Run the user step for each account.
|
|
61
|
+
Run `boxman verify` afterward to check the installed commands and rootless
|
|
62
|
+
Podman.
|
|
63
|
+
|
|
64
|
+
The same package set can be used in a dev container:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
podman build -f container/Dockerfile -t boxman-dev .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The container build skips host login-user and systemd configuration. FUSE
|
|
71
|
+
mounting may require additional container privileges, so the vault commands
|
|
72
|
+
are intended for the native host.
|
|
73
|
+
|
|
74
|
+
## Private directory
|
|
75
|
+
|
|
76
|
+
The recipe installs `gocryptfs` and FUSE 3. Each user initializes their own
|
|
77
|
+
vault once and unlocks it after a reboot or unmount:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uvx --from . boxman vault init
|
|
81
|
+
uvx --from . boxman vault unlock
|
|
82
|
+
uvx --from . boxman vault status
|
|
83
|
+
uvx --from . boxman claude # starts Claude with its config in the mounted vault
|
|
84
|
+
uvx --from . boxman vault lock # after stopping processes that use the mount
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Encrypted files live in `~/.private.cipher`; the plaintext mount is
|
|
88
|
+
`~/private`. Initialization and unlocking prompt for the password. Keep the
|
|
89
|
+
password and gocryptfs recovery key outside the host. Back up the encrypted
|
|
90
|
+
directory, including `gocryptfs.conf`, while keeping the plaintext mount out
|
|
91
|
+
of backups.
|
|
92
|
+
|
|
93
|
+
`boxman claude` refuses to run unless the vault is mounted. Use it before the
|
|
94
|
+
first Claude login. It sets `CLAUDE_CONFIG_DIR` to `~/private/claude` and does
|
|
95
|
+
not move existing credentials from `~/.claude`. Configure other tools'
|
|
96
|
+
credential locations separately if they should use the vault. Persistent
|
|
97
|
+
agents need the vault mounted while they use credentials. Locking fails while
|
|
98
|
+
a process holds files in the mount open.
|
|
99
|
+
|
|
100
|
+
The vault protects its backing files and snapshots while locked. It does not
|
|
101
|
+
hide credentials from the user's running processes or a host administrator
|
|
102
|
+
while unlocked. Keep home directory permissions private to each Unix owner.
|
|
103
|
+
|
|
104
|
+
## EC2 host
|
|
105
|
+
|
|
106
|
+
Install the optional AWS dependency, then create
|
|
107
|
+
`${XDG_CONFIG_HOME:-~/.config}/boxman/ec2.toml`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv tool install '.[ec2]'
|
|
111
|
+
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/boxman"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[ec2]
|
|
116
|
+
profile = "your-aws-profile"
|
|
117
|
+
region = "your-region"
|
|
118
|
+
stack_name = "your-stack-name"
|
|
119
|
+
|
|
120
|
+
[ec2.tags]
|
|
121
|
+
Owner = "your-owner"
|
|
122
|
+
Environment = "your-environment"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The config contains no credentials; AWS uses the named profile. Supply the tags
|
|
126
|
+
required by your account. The TOML file selects the AWS profile, region, stack,
|
|
127
|
+
and tags. Command-line options override the file;
|
|
128
|
+
`--tag KEY=VALUE` adds or overrides a tag. `--config PATH` selects another TOML
|
|
129
|
+
file. Put global options before the action:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
boxman ec2 init --vpc-id vpc-... --subnet-id subnet-... \
|
|
133
|
+
--instance-type t3.xlarge --volume-size-gb 100 \
|
|
134
|
+
--instance-name mybox \
|
|
135
|
+
--ami-id /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id
|
|
136
|
+
boxman ec2 deploy
|
|
137
|
+
boxman ec2 status
|
|
138
|
+
boxman ec2 start
|
|
139
|
+
boxman ec2 stop
|
|
140
|
+
boxman ec2 connect -u myuser
|
|
141
|
+
boxman ec2 ssh -u myuser
|
|
142
|
+
boxman ec2 ssh-config -u myuser --alias mybox
|
|
143
|
+
boxman ec2 run -u myuser 'uname -a'
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`init` writes `${XDG_CONFIG_HOME:-~/.config}/boxman/stacks/<stack_name>.yaml`
|
|
147
|
+
with the instance settings as parameter defaults and refuses to overwrite an
|
|
148
|
+
existing file. Edit that YAML to customize the stack. `deploy` reads the YAML
|
|
149
|
+
for the configured stack name and sends its defaults as CloudFormation
|
|
150
|
+
parameters. Tags from `[ec2.tags]` and repeatable
|
|
151
|
+
`--tag KEY=VALUE` options become CloudFormation stack tags. A single config
|
|
152
|
+
file selects one stack; use `--config PATH` for another box.
|
|
153
|
+
|
|
154
|
+
Deployment creates or updates a CloudFormation stack containing an Ubuntu EC2
|
|
155
|
+
instance, an SSM role, and an EC2 Instance Connect Endpoint. SSH uses that
|
|
156
|
+
endpoint and installs a generated public key through SSM. `ssh-config` writes a
|
|
157
|
+
marked host entry to `~/.ssh/config`. The stack name selects the instance for
|
|
158
|
+
all subsequent commands. Starting, stopping, and deploying incur AWS charges.
|
|
159
|
+
|
|
160
|
+
## Release
|
|
161
|
+
|
|
162
|
+
Build the Zensical book locally with `zensical build --clean`. The docs workflow
|
|
163
|
+
publishes it to GitHub Pages when documentation changes on `main`.
|
|
164
|
+
|
|
165
|
+
A published GitHub release triggers the PyPI workflow. Use a `vX.Y.Z` tag;
|
|
166
|
+
the workflow sets the package version from the release tag, builds the wheel
|
|
167
|
+
and source distribution, and publishes with PyPI trusted publishing. Configure
|
|
168
|
+
a PyPI trusted publisher for repository `vivainio/boxman`, workflow
|
|
169
|
+
`publish.yml`, environment `pypi` before the first release.
|
boxman-0.1.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# boxman
|
|
2
|
+
|
|
3
|
+
[Read the Boxman book](https://vivainio.github.io/boxman/) for the concepts, setup steps, and command reference.
|
|
4
|
+
|
|
5
|
+
Set up and manage an Ubuntu 24.04 development box. `boxman` is a Python command
|
|
6
|
+
with system, user, and vault operations.
|
|
7
|
+
|
|
8
|
+
## Install a host
|
|
9
|
+
|
|
10
|
+
From this checkout, run the system step as root, then the user step from each
|
|
11
|
+
login account. The first command works with the Ubuntu system Python before
|
|
12
|
+
`uv` is installed:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
sudo python3 -m boxman.cli system
|
|
16
|
+
python3 -m boxman.cli user
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
With `uv` available, run it without installing the package:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uvx --from . boxman vault status
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
After publishing the repository, `uvx --from
|
|
26
|
+
git+https://github.com/vivainio/boxman boxman ...` can run the same console
|
|
27
|
+
command. For a persistent command, use `uv tool install .` from this checkout.
|
|
28
|
+
|
|
29
|
+
`boxman system` uses zipget to install the apt packages declared in
|
|
30
|
+
`linux-tools.toml`, sets up Git LFS, and configures rootless Podman for normal
|
|
31
|
+
login users. It accepts explicit usernames, or `--packages-only` for a
|
|
32
|
+
container build. It downloads zipget if no version supporting
|
|
33
|
+
`recipe --system-only` is on root's PATH. A fresh install needs a released
|
|
34
|
+
zipget with that option.
|
|
35
|
+
|
|
36
|
+
`boxman user` installs the tools in the recipe, Node.js 22, Claude Code,
|
|
37
|
+
Copilot CLI, and uv. Run the user step for each account.
|
|
38
|
+
Run `boxman verify` afterward to check the installed commands and rootless
|
|
39
|
+
Podman.
|
|
40
|
+
|
|
41
|
+
The same package set can be used in a dev container:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
podman build -f container/Dockerfile -t boxman-dev .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The container build skips host login-user and systemd configuration. FUSE
|
|
48
|
+
mounting may require additional container privileges, so the vault commands
|
|
49
|
+
are intended for the native host.
|
|
50
|
+
|
|
51
|
+
## Private directory
|
|
52
|
+
|
|
53
|
+
The recipe installs `gocryptfs` and FUSE 3. Each user initializes their own
|
|
54
|
+
vault once and unlocks it after a reboot or unmount:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uvx --from . boxman vault init
|
|
58
|
+
uvx --from . boxman vault unlock
|
|
59
|
+
uvx --from . boxman vault status
|
|
60
|
+
uvx --from . boxman claude # starts Claude with its config in the mounted vault
|
|
61
|
+
uvx --from . boxman vault lock # after stopping processes that use the mount
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Encrypted files live in `~/.private.cipher`; the plaintext mount is
|
|
65
|
+
`~/private`. Initialization and unlocking prompt for the password. Keep the
|
|
66
|
+
password and gocryptfs recovery key outside the host. Back up the encrypted
|
|
67
|
+
directory, including `gocryptfs.conf`, while keeping the plaintext mount out
|
|
68
|
+
of backups.
|
|
69
|
+
|
|
70
|
+
`boxman claude` refuses to run unless the vault is mounted. Use it before the
|
|
71
|
+
first Claude login. It sets `CLAUDE_CONFIG_DIR` to `~/private/claude` and does
|
|
72
|
+
not move existing credentials from `~/.claude`. Configure other tools'
|
|
73
|
+
credential locations separately if they should use the vault. Persistent
|
|
74
|
+
agents need the vault mounted while they use credentials. Locking fails while
|
|
75
|
+
a process holds files in the mount open.
|
|
76
|
+
|
|
77
|
+
The vault protects its backing files and snapshots while locked. It does not
|
|
78
|
+
hide credentials from the user's running processes or a host administrator
|
|
79
|
+
while unlocked. Keep home directory permissions private to each Unix owner.
|
|
80
|
+
|
|
81
|
+
## EC2 host
|
|
82
|
+
|
|
83
|
+
Install the optional AWS dependency, then create
|
|
84
|
+
`${XDG_CONFIG_HOME:-~/.config}/boxman/ec2.toml`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv tool install '.[ec2]'
|
|
88
|
+
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/boxman"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```toml
|
|
92
|
+
[ec2]
|
|
93
|
+
profile = "your-aws-profile"
|
|
94
|
+
region = "your-region"
|
|
95
|
+
stack_name = "your-stack-name"
|
|
96
|
+
|
|
97
|
+
[ec2.tags]
|
|
98
|
+
Owner = "your-owner"
|
|
99
|
+
Environment = "your-environment"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The config contains no credentials; AWS uses the named profile. Supply the tags
|
|
103
|
+
required by your account. The TOML file selects the AWS profile, region, stack,
|
|
104
|
+
and tags. Command-line options override the file;
|
|
105
|
+
`--tag KEY=VALUE` adds or overrides a tag. `--config PATH` selects another TOML
|
|
106
|
+
file. Put global options before the action:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
boxman ec2 init --vpc-id vpc-... --subnet-id subnet-... \
|
|
110
|
+
--instance-type t3.xlarge --volume-size-gb 100 \
|
|
111
|
+
--instance-name mybox \
|
|
112
|
+
--ami-id /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id
|
|
113
|
+
boxman ec2 deploy
|
|
114
|
+
boxman ec2 status
|
|
115
|
+
boxman ec2 start
|
|
116
|
+
boxman ec2 stop
|
|
117
|
+
boxman ec2 connect -u myuser
|
|
118
|
+
boxman ec2 ssh -u myuser
|
|
119
|
+
boxman ec2 ssh-config -u myuser --alias mybox
|
|
120
|
+
boxman ec2 run -u myuser 'uname -a'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`init` writes `${XDG_CONFIG_HOME:-~/.config}/boxman/stacks/<stack_name>.yaml`
|
|
124
|
+
with the instance settings as parameter defaults and refuses to overwrite an
|
|
125
|
+
existing file. Edit that YAML to customize the stack. `deploy` reads the YAML
|
|
126
|
+
for the configured stack name and sends its defaults as CloudFormation
|
|
127
|
+
parameters. Tags from `[ec2.tags]` and repeatable
|
|
128
|
+
`--tag KEY=VALUE` options become CloudFormation stack tags. A single config
|
|
129
|
+
file selects one stack; use `--config PATH` for another box.
|
|
130
|
+
|
|
131
|
+
Deployment creates or updates a CloudFormation stack containing an Ubuntu EC2
|
|
132
|
+
instance, an SSM role, and an EC2 Instance Connect Endpoint. SSH uses that
|
|
133
|
+
endpoint and installs a generated public key through SSM. `ssh-config` writes a
|
|
134
|
+
marked host entry to `~/.ssh/config`. The stack name selects the instance for
|
|
135
|
+
all subsequent commands. Starting, stopping, and deploying incur AWS charges.
|
|
136
|
+
|
|
137
|
+
## Release
|
|
138
|
+
|
|
139
|
+
Build the Zensical book locally with `zensical build --clean`. The docs workflow
|
|
140
|
+
publishes it to GitHub Pages when documentation changes on `main`.
|
|
141
|
+
|
|
142
|
+
A published GitHub release triggers the PyPI workflow. Use a `vX.Y.Z` tag;
|
|
143
|
+
the workflow sets the package version from the release tag, builds the wheel
|
|
144
|
+
and source distribution, and publishes with PyPI trusted publishing. Configure
|
|
145
|
+
a PyPI trusted publisher for repository `vivainio/boxman`, workflow
|
|
146
|
+
`publish.yml`, environment `pypi` before the first release.
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Set up and manage a Linux development box."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fail(message: str) -> None:
|
|
13
|
+
raise SystemExit(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(*args: str) -> None:
|
|
17
|
+
result = subprocess.run(args, check=False)
|
|
18
|
+
if result.returncode:
|
|
19
|
+
raise SystemExit(result.returncode)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def mounted(path: Path) -> bool:
|
|
23
|
+
return subprocess.run(
|
|
24
|
+
("mountpoint", "-q", str(path)),
|
|
25
|
+
stdout=subprocess.DEVNULL,
|
|
26
|
+
stderr=subprocess.DEVNULL,
|
|
27
|
+
check=False,
|
|
28
|
+
).returncode == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def vault_paths() -> tuple[Path, Path]:
|
|
32
|
+
home = Path.home()
|
|
33
|
+
cipher, plain = home / ".private.cipher", home / "private"
|
|
34
|
+
if cipher.is_symlink() or plain.is_symlink():
|
|
35
|
+
fail("Private vault paths must not be symlinks")
|
|
36
|
+
return cipher, plain
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def private_dir(path: Path) -> None:
|
|
40
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
41
|
+
path.chmod(0o700)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def vault(action: str) -> None:
|
|
45
|
+
if os.geteuid() == 0:
|
|
46
|
+
fail("Run vault commands as your login user, not root")
|
|
47
|
+
os.umask(0o077)
|
|
48
|
+
cipher, plain = vault_paths()
|
|
49
|
+
|
|
50
|
+
if action == "status":
|
|
51
|
+
print("Private vault is unlocked" if mounted(plain) else "Private vault is locked")
|
|
52
|
+
elif action == "init":
|
|
53
|
+
if mounted(plain):
|
|
54
|
+
fail("Private vault is already unlocked")
|
|
55
|
+
if (cipher / "gocryptfs.conf").exists():
|
|
56
|
+
fail("Private vault is already initialized")
|
|
57
|
+
private_dir(cipher)
|
|
58
|
+
private_dir(plain)
|
|
59
|
+
if any(cipher.iterdir()):
|
|
60
|
+
fail("Encrypted backing directory is not empty")
|
|
61
|
+
if any(plain.iterdir()):
|
|
62
|
+
fail("Plaintext mount directory is not empty")
|
|
63
|
+
run("gocryptfs", "-init", str(cipher))
|
|
64
|
+
print("Keep the password and recovery key outside this host.")
|
|
65
|
+
elif action == "unlock":
|
|
66
|
+
if not (cipher / "gocryptfs.conf").is_file():
|
|
67
|
+
fail("Private vault is not initialized; run boxman vault init")
|
|
68
|
+
private_dir(cipher)
|
|
69
|
+
private_dir(plain)
|
|
70
|
+
if mounted(plain):
|
|
71
|
+
print("Private vault is already unlocked")
|
|
72
|
+
return
|
|
73
|
+
if any(plain.iterdir()):
|
|
74
|
+
fail("Plaintext mount directory is not empty; refusing to hide its files")
|
|
75
|
+
run("gocryptfs", str(cipher), str(plain))
|
|
76
|
+
elif action == "lock":
|
|
77
|
+
if not mounted(plain):
|
|
78
|
+
print("Private vault is already locked")
|
|
79
|
+
return
|
|
80
|
+
run("fusermount3", "-u", str(plain))
|
|
81
|
+
else:
|
|
82
|
+
fail("Usage: boxman vault {init|unlock|lock|status}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def claude(args: list[str]) -> None:
|
|
86
|
+
if os.geteuid() == 0:
|
|
87
|
+
fail("Run Claude as your login user, not root")
|
|
88
|
+
_, plain = vault_paths()
|
|
89
|
+
if not mounted(plain):
|
|
90
|
+
fail("Private vault is locked; run boxman vault unlock first")
|
|
91
|
+
config = plain / "claude"
|
|
92
|
+
if config.is_symlink():
|
|
93
|
+
fail("Claude config directory must not be a symlink")
|
|
94
|
+
os.umask(0o077)
|
|
95
|
+
private_dir(config)
|
|
96
|
+
os.environ["CLAUDE_CONFIG_DIR"] = str(config)
|
|
97
|
+
os.execvp("claude", ["claude", *args])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> None:
|
|
101
|
+
if len(sys.argv) < 2:
|
|
102
|
+
fail("Usage: boxman {system|user|vault|claude|verify|ec2} ...")
|
|
103
|
+
command, *args = sys.argv[1:]
|
|
104
|
+
if command == "system":
|
|
105
|
+
from boxman import system
|
|
106
|
+
|
|
107
|
+
system.main(args)
|
|
108
|
+
elif command == "user":
|
|
109
|
+
if args:
|
|
110
|
+
fail("Usage: boxman user")
|
|
111
|
+
from boxman import user
|
|
112
|
+
|
|
113
|
+
user.main()
|
|
114
|
+
elif command == "vault":
|
|
115
|
+
if len(args) != 1:
|
|
116
|
+
fail("Usage: boxman vault {init|unlock|lock|status}")
|
|
117
|
+
vault(args[0])
|
|
118
|
+
elif command == "claude":
|
|
119
|
+
claude(args)
|
|
120
|
+
elif command == "ec2":
|
|
121
|
+
from boxman import ec2
|
|
122
|
+
|
|
123
|
+
ec2.main(args)
|
|
124
|
+
elif command == "verify":
|
|
125
|
+
if args:
|
|
126
|
+
fail("Usage: boxman verify")
|
|
127
|
+
from boxman import verify
|
|
128
|
+
|
|
129
|
+
verify.main()
|
|
130
|
+
else:
|
|
131
|
+
fail("Usage: boxman {system|user|vault|claude|verify|ec2} ...")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
AWSTemplateFormatVersion: "2010-09-09"
|
|
2
|
+
Description: >
|
|
3
|
+
EC2 development box:
|
|
4
|
+
one instance, no inbound security-group rules, SSM-only access.
|
|
5
|
+
|
|
6
|
+
Parameters:
|
|
7
|
+
VpcId:
|
|
8
|
+
Type: AWS::EC2::VPC::Id
|
|
9
|
+
SubnetId:
|
|
10
|
+
Type: AWS::EC2::Subnet::Id
|
|
11
|
+
InstanceType:
|
|
12
|
+
Type: String
|
|
13
|
+
VolumeSizeGb:
|
|
14
|
+
Type: Number
|
|
15
|
+
InstanceName:
|
|
16
|
+
Type: String
|
|
17
|
+
LatestAmiId:
|
|
18
|
+
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
|
|
19
|
+
|
|
20
|
+
Resources:
|
|
21
|
+
SecurityGroup:
|
|
22
|
+
Type: AWS::EC2::SecurityGroup
|
|
23
|
+
Properties:
|
|
24
|
+
GroupName: !Sub "${AWS::StackName}-sg"
|
|
25
|
+
GroupDescription: Shared coding-agent dev box - no inbound rules, SSM only
|
|
26
|
+
VpcId: !Ref VpcId
|
|
27
|
+
Tags:
|
|
28
|
+
- Key: Name
|
|
29
|
+
Value: !Sub "${AWS::StackName}-sg"
|
|
30
|
+
# No SecurityGroupIngress here: no inbound rules from the internet, on
|
|
31
|
+
# purpose. The only inbound access is via SecurityGroupIngressFromEice
|
|
32
|
+
# below, scoped to the Instance Connect Endpoint's own security group.
|
|
33
|
+
# No SecurityGroupEgress: keeps CloudFormation's default allow-all outbound.
|
|
34
|
+
|
|
35
|
+
EiceSecurityGroup:
|
|
36
|
+
Type: AWS::EC2::SecurityGroup
|
|
37
|
+
Properties:
|
|
38
|
+
GroupName: !Sub "${AWS::StackName}-eice-sg"
|
|
39
|
+
GroupDescription: EC2 Instance Connect Endpoint - no inbound rules needed
|
|
40
|
+
VpcId: !Ref VpcId
|
|
41
|
+
Tags:
|
|
42
|
+
- Key: Name
|
|
43
|
+
Value: !Sub "${AWS::StackName}-eice-sg"
|
|
44
|
+
|
|
45
|
+
SecurityGroupIngressFromEice:
|
|
46
|
+
Type: AWS::EC2::SecurityGroupIngress
|
|
47
|
+
Properties:
|
|
48
|
+
GroupId: !Ref SecurityGroup
|
|
49
|
+
IpProtocol: tcp
|
|
50
|
+
FromPort: 22
|
|
51
|
+
ToPort: 22
|
|
52
|
+
SourceSecurityGroupId: !Ref EiceSecurityGroup
|
|
53
|
+
Description: SSH from the EC2 Instance Connect Endpoint only
|
|
54
|
+
|
|
55
|
+
InstanceConnectEndpoint:
|
|
56
|
+
Type: AWS::EC2::InstanceConnectEndpoint
|
|
57
|
+
Properties:
|
|
58
|
+
SubnetId: !Ref SubnetId
|
|
59
|
+
SecurityGroupIds:
|
|
60
|
+
- !Ref EiceSecurityGroup
|
|
61
|
+
PreserveClientIp: true
|
|
62
|
+
Tags:
|
|
63
|
+
- Key: Name
|
|
64
|
+
Value: !Sub "${AWS::StackName}-eice"
|
|
65
|
+
|
|
66
|
+
InstanceRole:
|
|
67
|
+
Type: AWS::IAM::Role
|
|
68
|
+
Properties:
|
|
69
|
+
RoleName: !Sub "${AWS::StackName}-ssm-role"
|
|
70
|
+
AssumeRolePolicyDocument:
|
|
71
|
+
Version: "2012-10-17"
|
|
72
|
+
Statement:
|
|
73
|
+
- Effect: Allow
|
|
74
|
+
Principal:
|
|
75
|
+
Service: ec2.amazonaws.com
|
|
76
|
+
Action: sts:AssumeRole
|
|
77
|
+
ManagedPolicyArns:
|
|
78
|
+
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
|
|
79
|
+
Tags:
|
|
80
|
+
- Key: Name
|
|
81
|
+
Value: !Sub "${AWS::StackName}-ssm-role"
|
|
82
|
+
|
|
83
|
+
InstanceProfile:
|
|
84
|
+
Type: AWS::IAM::InstanceProfile
|
|
85
|
+
Properties:
|
|
86
|
+
InstanceProfileName: !Sub "${AWS::StackName}-ssm-profile"
|
|
87
|
+
Roles:
|
|
88
|
+
- !Ref InstanceRole
|
|
89
|
+
|
|
90
|
+
Instance:
|
|
91
|
+
Type: AWS::EC2::Instance
|
|
92
|
+
Properties:
|
|
93
|
+
ImageId: !Ref LatestAmiId
|
|
94
|
+
InstanceType: !Ref InstanceType
|
|
95
|
+
SubnetId: !Ref SubnetId
|
|
96
|
+
SecurityGroupIds:
|
|
97
|
+
- !Ref SecurityGroup
|
|
98
|
+
IamInstanceProfile: !Ref InstanceProfile
|
|
99
|
+
BlockDeviceMappings:
|
|
100
|
+
- DeviceName: /dev/sda1
|
|
101
|
+
Ebs:
|
|
102
|
+
VolumeSize: !Ref VolumeSizeGb
|
|
103
|
+
VolumeType: gp3
|
|
104
|
+
DeleteOnTermination: true
|
|
105
|
+
MetadataOptions:
|
|
106
|
+
HttpTokens: required
|
|
107
|
+
HttpPutResponseHopLimit: 2
|
|
108
|
+
Tags:
|
|
109
|
+
- Key: Name
|
|
110
|
+
Value: !Ref InstanceName
|
|
111
|
+
|
|
112
|
+
Outputs:
|
|
113
|
+
InstanceId:
|
|
114
|
+
Value: !Ref Instance
|
|
115
|
+
SecurityGroupId:
|
|
116
|
+
Value: !Ref SecurityGroup
|
|
117
|
+
InstanceRoleArn:
|
|
118
|
+
Value: !GetAtt InstanceRole.Arn
|
|
119
|
+
InstanceProfileArn:
|
|
120
|
+
Value: !GetAtt InstanceProfile.Arn
|
|
121
|
+
# No PublicIp output: !GetAtt Instance.PublicIp fails outright whenever the
|
|
122
|
+
# instance isn't running (stopped by uptime governance, mid-restart, etc.),
|
|
123
|
+
# breaking every subsequent stack operation. basbox.py status() already
|
|
124
|
+
# reads the live public IP via ec2:DescribeInstances instead.
|
|
125
|
+
InstanceConnectEndpointId:
|
|
126
|
+
Value: !Ref InstanceConnectEndpoint
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# zipget recipe for boxman on Ubuntu 24.04
|
|
2
|
+
|
|
3
|
+
[system_packages]
|
|
4
|
+
apt = [
|
|
5
|
+
"acl", "build-essential", "ca-certificates", "curl", "dbus-user-session",
|
|
6
|
+
"direnv", "file", "fuse3", "fuse-overlayfs", "gh", "git", "git-lfs",
|
|
7
|
+
"gocryptfs", "jq",
|
|
8
|
+
"less", "lf", "libsecret-tools", "openssh-client", "podman", "python3",
|
|
9
|
+
"python3-venv", "python-is-python3", "rsync", "shellcheck",
|
|
10
|
+
"slirp4netns", "uidmap", "unzip", "xz-utils", "zip", "zstd",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[vars]
|
|
14
|
+
bin_dir = "~/.local/bin"
|
|
15
|
+
|
|
16
|
+
[herdr]
|
|
17
|
+
# Agent multiplexer - https://herdr.dev
|
|
18
|
+
github = { repo = "herdrdev/herdr", asset = "herdr-linux-x86_64" }
|
|
19
|
+
save_as = "${bin_dir}/herdr"
|
|
20
|
+
executable = true
|
|
21
|
+
|
|
22
|
+
[cship]
|
|
23
|
+
# Claude Code statusline - https://cship.dev
|
|
24
|
+
github = { repo = "stephenleo/cship", asset = "cship-x86_64-unknown-linux-musl" }
|
|
25
|
+
save_as = "${bin_dir}/cship"
|
|
26
|
+
executable = true
|
|
27
|
+
|
|
28
|
+
[microsoft-edit]
|
|
29
|
+
# Terminal-based text editor - https://github.com/microsoft/edit
|
|
30
|
+
# Note: only gnu builds available (no musl)
|
|
31
|
+
github = { repo = "microsoft/edit", asset = "x86_64-linux-gnu.tar.gz" }
|
|
32
|
+
unzip_to = "${bin_dir}"
|
|
33
|
+
files = "edit"
|
|
34
|
+
|
|
35
|
+
[starship]
|
|
36
|
+
# Cross-shell prompt - https://github.com/starship/starship
|
|
37
|
+
github = { repo = "starship/starship", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
38
|
+
unzip_to = "${bin_dir}"
|
|
39
|
+
files = "starship"
|
|
40
|
+
|
|
41
|
+
[ripgrep]
|
|
42
|
+
# Fast regex search tool - https://github.com/BurntSushi/ripgrep
|
|
43
|
+
github = { repo = "BurntSushi/ripgrep", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
44
|
+
unzip_to = "${bin_dir}"
|
|
45
|
+
files = "*/rg"
|
|
46
|
+
|
|
47
|
+
[fd]
|
|
48
|
+
# Fast find alternative - https://github.com/sharkdp/fd
|
|
49
|
+
github = { repo = "sharkdp/fd", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
50
|
+
unzip_to = "${bin_dir}"
|
|
51
|
+
files = "*/fd"
|
|
52
|
+
|
|
53
|
+
[bat]
|
|
54
|
+
# Cat with syntax highlighting - https://github.com/sharkdp/bat
|
|
55
|
+
github = { repo = "sharkdp/bat", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
56
|
+
unzip_to = "${bin_dir}"
|
|
57
|
+
files = "*/bat"
|
|
58
|
+
|
|
59
|
+
[fzf]
|
|
60
|
+
# Fuzzy finder - https://github.com/junegunn/fzf
|
|
61
|
+
github = { repo = "junegunn/fzf", asset = "linux_amd64.tar.gz" }
|
|
62
|
+
unzip_to = "${bin_dir}"
|
|
63
|
+
files = "fzf"
|
|
64
|
+
|
|
65
|
+
[zoxide]
|
|
66
|
+
# Smarter cd command - https://github.com/ajeetdsouza/zoxide
|
|
67
|
+
github = { repo = "ajeetdsouza/zoxide", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
68
|
+
unzip_to = "${bin_dir}"
|
|
69
|
+
files = "zoxide"
|
|
70
|
+
|
|
71
|
+
[delta]
|
|
72
|
+
# Git diff viewer - https://github.com/dandavison/delta
|
|
73
|
+
github = { repo = "dandavison/delta", asset = "x86_64-unknown-linux-musl.tar.gz" }
|
|
74
|
+
unzip_to = "${bin_dir}"
|
|
75
|
+
files = "*/delta"
|
|
76
|
+
|
|
77
|
+
[aws-cli]
|
|
78
|
+
# AWS CLI v2 - official installer
|
|
79
|
+
url = "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
|
|
80
|
+
unzip_to = "./aws-cli-installer"
|