yamine 0.20.0 → 0.21.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +62 -17
- data/lib/yamine/cli/boot.rb +5 -4
- data/lib/yamine/cli/system.rb +260 -81
- data/lib/yamine/cli/worktree.rb +6 -1
- data/lib/yamine/cli.rb +1 -1
- data/lib/yamine/database.rb +1 -0
- data/lib/yamine/doctor.rb +66 -0
- data/lib/yamine/hosts.rb +76 -8
- data/lib/yamine/privileged_payload.rb +419 -0
- data/lib/yamine/proxy.rb +28 -2
- data/lib/yamine/proxy_control.rb +45 -6
- data/lib/yamine/version.rb +1 -1
- data/lib/yamine.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e920f5f6d29a92e0a6e81ca671bccf62e0a264ec453f9b5bc4d42d4e56980d3
|
|
4
|
+
data.tar.gz: cc06342930ee7c882cf30fd2818f1a60495c8a59e44ec6779cc666f569493ac0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4164dabf9bd5537023ad59d426a958a5e284a90f0226cc83e0533cf635edf0c65acc16dd311d58a88df83d7abdcdfb92034703ecda06d89af08bb5ec96c73635
|
|
7
|
+
data.tar.gz: 1e3af2b55e54885fe8aeed98856c4371eab280ec981cd2644ebb75aba0206752ae44307c0252f51188a778426efbb882ef2285c63f142def39f4e876f77302d6
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,105 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.21.1] — 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Claiming a per-worktree database no longer raises `NoMethodError`
|
|
10
|
+
on Ruby 3.2/3.3.** The claim stamps the state file with
|
|
11
|
+
`Time#iso8601`, but `lib/yamine/database.rb` never required the
|
|
12
|
+
stdlib `time` library, and on 3.2/3.3 nothing else loads it
|
|
13
|
+
implicitly — so every claim raised (34 unit errors on those rubies
|
|
14
|
+
in CI). 0.21.0 is affected. One `require "time"`, alongside the
|
|
15
|
+
stdlibs that file already pulls in.
|
|
16
|
+
- **A closed WebSocket upgrade no longer leaks a thread on Linux.**
|
|
17
|
+
`pipe_both` tore each end down with `close` from the sibling
|
|
18
|
+
thread, and a close from another thread does not interrupt that
|
|
19
|
+
copy's blocking read on Linux (it does on macOS) — so every closed
|
|
20
|
+
upgrade parked a thread forever, `handle` never unwound, and they
|
|
21
|
+
accumulated in the root daemon. Teardown is now symmetric
|
|
22
|
+
`shutdown(2)`, which wakes the parked read with EOF; both sockets
|
|
23
|
+
still close exactly once, and no timeout was added to the
|
|
24
|
+
unbounded upgrade path.
|
|
25
|
+
|
|
26
|
+
## [0.21.0] — 2026-09-28
|
|
27
|
+
|
|
28
|
+
### Security
|
|
29
|
+
|
|
30
|
+
- **The privileged service no longer runs user-writable code.** The
|
|
31
|
+
launchd/systemd unit used to execute the gem's `bin`+`lib` straight
|
|
32
|
+
out of the user's home (a version-stamped, user-writable gem
|
|
33
|
+
directory), so anything that could write those paths — a gem upgrade,
|
|
34
|
+
a `bundle install`, an agent editing the gem — ran as root at every
|
|
35
|
+
boot. `service install` now stages `bin`+`lib` into a root-owned
|
|
36
|
+
directory at a stable, version-independent path (`/Library/Application
|
|
37
|
+
Support/yamine` on macOS, `/usr/local/share/yamine` on Linux):
|
|
38
|
+
copied aside, ownership/modes locked, verified fail-closed (a bad
|
|
39
|
+
tree registers no unit and leaves the running daemon untouched), then
|
|
40
|
+
swung live with an atomic symlink swap plus a root-owned `VERSION`
|
|
41
|
+
marker. The unit pins only that path.
|
|
42
|
+
- **The passwordless grant can no longer introduce root-executed
|
|
43
|
+
code.** `yamine sudoers` used to grant the version-stamped gem path
|
|
44
|
+
with `service install --internal` — effectively arbitrary code as
|
|
45
|
+
root for anyone who could write the gem, and stale after every
|
|
46
|
+
release. It now grants only the staged, version-independent payload:
|
|
47
|
+
`hosts sync` (the recurring agent need) and `service uninstall`
|
|
48
|
+
(which only deletes yamine's own files). Staging new root code stays
|
|
49
|
+
a human interactive sudo; a non-interactive `service install` fails
|
|
50
|
+
fast saying so. If your `/etc/sudoers.d/yamine` still pins a
|
|
51
|
+
version-stamped gem path, re-run `yamine sudoers` and replace it.
|
|
52
|
+
- **`hosts sync` is strictly validated before anything is written.**
|
|
53
|
+
Route names are user-writable and the sync runs as root, so every
|
|
54
|
+
name must now be well-formed (no newline/breakout), written only
|
|
55
|
+
inside yamine's managed block, and under `.localhost` or a domain in
|
|
56
|
+
the root-owned staged allowlist. Anything else fails closed naming
|
|
57
|
+
the policy. Custom `proxy.host` domains need one human step: append
|
|
58
|
+
the parent domain to the staged `allowed-tlds` file.
|
|
59
|
+
- **`yamine doctor` gains a `service` check.** It names a legacy unit
|
|
60
|
+
(payload in a user home or version-stamped gem path) with the exact
|
|
61
|
+
migration (`sudo yamine service install`), warns on staged-vs-CLI
|
|
62
|
+
version skew, fails on any root-executed path that is not root-owned
|
|
63
|
+
or is group/other-writable, and always reports the service
|
|
64
|
+
interpreter's path and writability. The legacy daemon keeps working
|
|
65
|
+
until the next elevated install.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- `service install` (human, interactive, elevated) migrates a legacy
|
|
70
|
+
install in place and prints the interpreter caveat; `service
|
|
71
|
+
uninstall` removes the staged payload as well as the unit, scoped to
|
|
72
|
+
yamine's own files.
|
|
73
|
+
- Non-interactive (`sudo -n`) failure messaging points at the new
|
|
74
|
+
provisioning story: humans install the 443 service once per machine,
|
|
75
|
+
agents keep hosts entries fresh through the grant.
|
|
76
|
+
|
|
77
|
+
### Fixed
|
|
78
|
+
|
|
79
|
+
- **The printed grant now matches what the CLI invokes, byte for
|
|
80
|
+
byte.** Elevated re-execs went through `env YAMINE_STATE_DIR=…`,
|
|
81
|
+
which made the command `/usr/bin/env` — a path no rule names — so
|
|
82
|
+
every passwordless attempt fell through to the password-required
|
|
83
|
+
admin rule; and the staged macOS path's space was unescaped in the
|
|
84
|
+
sudoers output, so that rule could never match either. No
|
|
85
|
+
environment crosses sudo anymore (no `env` prefix, no SETENV): the
|
|
86
|
+
root half derives the invoking user's state dir from `SUDO_USER`,
|
|
87
|
+
and spaces are backslash-escaped when the rules are printed. Pinned
|
|
88
|
+
by a parity test that parses the printed specs and asserts equality
|
|
89
|
+
with the captured elevated argv.
|
|
90
|
+
- **The `--no-service` sudo daemon no longer executes the gem
|
|
91
|
+
directory as root.** A privileged `proxy start` spawns the staged
|
|
92
|
+
root-owned payload, staging it first under the same
|
|
93
|
+
human-authorized sudo (`service stage --internal`, never granted)
|
|
94
|
+
when missing. The unprivileged spawn path is unchanged.
|
|
95
|
+
|
|
96
|
+
### Open caveat (not closed, stated plainly)
|
|
97
|
+
- **The interpreter is still user-writable.** No root-owned Ruby ≥ 3.2
|
|
98
|
+
exists on a stock machine, so the daemon necessarily runs the
|
|
99
|
+
invoking (user-writable) Ruby. The payload is root-owned; the
|
|
100
|
+
interpreter is not. Doctor reports it truthfully and the README says
|
|
101
|
+
so. Fixed only by a root-owned Ruby new enough for the gem; yamine
|
|
102
|
+
will not vendor or stage an interpreter.
|
|
103
|
+
|
|
5
104
|
## [0.20.0] — 2026-09-28
|
|
6
105
|
|
|
7
106
|
### Added
|
data/README.md
CHANGED
|
@@ -54,21 +54,35 @@ to get back to 443.
|
|
|
54
54
|
|
|
55
55
|
Binding 443 is privileged, so yamine installs a **root-owned launchd
|
|
56
56
|
service** (macOS) or systemd unit (Linux) that binds 443 at boot — the
|
|
57
|
-
same model as puma-dev and portless. Installing it needs
|
|
58
|
-
machine
|
|
59
|
-
`https://<app>.localhost` with no
|
|
57
|
+
same model as puma-dev and portless. Installing it needs an interactive
|
|
58
|
+
sudo **once per machine** (one Touch ID tap); after that, every `yamine`
|
|
59
|
+
run in any project gets a clean `https://<app>.localhost` with no
|
|
60
|
+
elevation and no prompt.
|
|
60
61
|
|
|
61
|
-
**Human (interactive):** run setup once — it
|
|
62
|
-
service, syncs hosts, and verifies:
|
|
62
|
+
**Human (interactive):** run setup once — it stages a root-owned copy of
|
|
63
|
+
yamine, installs the service, trusts the CA, syncs hosts, and verifies:
|
|
63
64
|
|
|
64
65
|
```bash
|
|
65
66
|
yamine setup
|
|
67
|
+
# or, for just the service: sudo yamine service install
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
The unit runs a **staged payload**, not your gem directory: the install
|
|
71
|
+
copies yamine's `bin`+`lib` into a root-owned directory at a stable,
|
|
72
|
+
version-independent path (`/Library/Application Support/yamine` on
|
|
73
|
+
macOS, `/usr/local/share/yamine` on Linux), verifies root ownership and
|
|
74
|
+
modes fail-closed (a bad tree registers nothing), and pins that path in
|
|
75
|
+
the unit. Ordinary user-space writes — a gem upgrade, a `bundle
|
|
76
|
+
install`, an agent editing the gem — can no longer change what runs as
|
|
77
|
+
root, and upgrades never stale the setup. `yamine doctor` names a legacy
|
|
78
|
+
install (a unit still pointing at a user-writable gem path) with the
|
|
79
|
+
exact migration, plus staged-vs-CLI version skew and any ownership
|
|
80
|
+
problem.
|
|
81
|
+
|
|
82
|
+
**Agents: the steady-state grant.** New project ⇒ new hostname ⇒ Safari
|
|
83
|
+
needs the `/etc/hosts` entry; that recurring privileged need stays
|
|
84
|
+
agent-invocable without a prompt. Install the scoped passwordless-sudo
|
|
85
|
+
rules once (as an admin):
|
|
72
86
|
|
|
73
87
|
```bash
|
|
74
88
|
yamine sudoers > /tmp/yamine.sudoers
|
|
@@ -76,11 +90,41 @@ sudo install -o root -g wheel -m 440 /tmp/yamine.sudoers /etc/sudoers.d/yamine
|
|
|
76
90
|
sudo install -o root -g root -m 440 /tmp/yamine.sudoers /etc/sudoers.d/yamine # Linux
|
|
77
91
|
```
|
|
78
92
|
|
|
79
|
-
`yamine sudoers` prints rules
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
`yamine sudoers` prints rules for the **staged payload only** — `hosts
|
|
94
|
+
sync` (hostnames strictly validated: well-formed, written only inside
|
|
95
|
+
yamine's managed block, and only under `.localhost` or an allowlisted
|
|
96
|
+
domain) and `service uninstall` (which only deletes yamine's own
|
|
97
|
+
files). Installing or upgrading the staged payload moves user-writable
|
|
98
|
+
source into root-owned paths, so it deliberately stays a human
|
|
99
|
+
interactive sudo — no grant will ever cover it, and a non-interactive
|
|
100
|
+
`service install` fails fast saying so. To undo the grant:
|
|
101
|
+
`sudo rm /etc/sudoers.d/yamine`.
|
|
102
|
+
|
|
103
|
+
If you installed an older yamine, your `/etc/sudoers.d/yamine` may still
|
|
104
|
+
pin a version-stamped gem path (`.../gems/yamine-X.Y.Z/...`) including
|
|
105
|
+
an install rule — re-run `yamine sudoers` and replace the file. The old
|
|
106
|
+
rules go stale every release (which pushed people toward `NOPASSWD:
|
|
107
|
+
ALL`); the new ones survive upgrades.
|
|
108
|
+
|
|
109
|
+
**Custom domains** (e.g. `proxy.host: myapp.local.example.com` for OAuth
|
|
110
|
+
parity) need their parent domain allowlisted once by a human — `hosts
|
|
111
|
+
sync` refuses anything outside `.localhost` and the staged allowlist,
|
|
112
|
+
because an unvalidated root hosts write could point a real vendor domain
|
|
113
|
+
at loopback:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
echo 'local.example.com' | sudo tee -a "/Library/Application Support/yamine/allowed-tlds" # macOS
|
|
117
|
+
echo 'local.example.com' | sudo tee -a /usr/local/share/yamine/allowed-tlds # Linux
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Open caveat: the interpreter.** The unit runs the invoking Ruby, and
|
|
121
|
+
there is no root-owned Ruby ≥ 3.2 on a stock machine (`/usr/bin/ruby` is
|
|
122
|
+
2.6), so the daemon necessarily runs a user-writable interpreter today.
|
|
123
|
+
The payload is root-owned; the interpreter is not — `yamine doctor`
|
|
124
|
+
reports its path and writability truthfully. Anything that can write
|
|
125
|
+
that Ruby can change what runs as root. This hole stays open until the
|
|
126
|
+
machine has a root-owned Ruby new enough for the gem; yamine will not
|
|
127
|
+
vendor or stage an interpreter to pretend otherwise.
|
|
84
128
|
|
|
85
129
|
## The one-file model
|
|
86
130
|
|
|
@@ -464,10 +508,11 @@ bundle exec rake test
|
|
|
464
508
|
self-signed, and there is no buffering or rate limiting.
|
|
465
509
|
|
|
466
510
|
|
|
467
|
-
The `
|
|
511
|
+
The `test/fixtures/apps/` fixture fleet exercises
|
|
468
512
|
detection, inference, and boot across Rails variants, Roda, Sinatra,
|
|
469
|
-
bare Rack, Jekyll, compound Procfiles, and a monorepo.
|
|
470
|
-
|
|
513
|
+
bare Rack, Jekyll, compound Procfiles, and a monorepo. It ships with
|
|
514
|
+
the repo and runs as part of the unit suite across the Ruby matrix —
|
|
515
|
+
no sibling checkout, no separate CI job.
|
|
471
516
|
|
|
472
517
|
## License
|
|
473
518
|
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -940,10 +940,11 @@ module Yamine
|
|
|
940
940
|
privileged = port < 1024 && !ProxyControl.root?
|
|
941
941
|
if privileged && !ctx.interactive?
|
|
942
942
|
$stderr.puts "Error: proxy is not running and port #{port} needs root."
|
|
943
|
-
$stderr.puts " Human: run this once — yamine setup"
|
|
944
|
-
$stderr.puts " Agent/CI:
|
|
945
|
-
$stderr.puts "
|
|
946
|
-
$stderr.puts "
|
|
943
|
+
$stderr.puts " Human: run this once in a terminal — yamine setup (or: sudo yamine service install)"
|
|
944
|
+
$stderr.puts " Agent/CI: the 443 service is installed by a human once per machine — it cannot be provisioned passwordlessly."
|
|
945
|
+
$stderr.puts " Steady-state hosts sync works via the grant instead:"
|
|
946
|
+
$stderr.puts " yamine sudoers > /tmp/yamine.sudoers"
|
|
947
|
+
$stderr.puts " sudo install -o root -g wheel -m 440 /tmp/yamine.sudoers /etc/sudoers.d/yamine"
|
|
947
948
|
$stderr.puts " Or start the proxy by hand: sudo yamine proxy start"
|
|
948
949
|
exit 1
|
|
949
950
|
end
|