runspec-linux-root 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ """runspec-linux-root — Linux admin runnables that need root.
2
+
3
+ Package management, reboot and kernel tuning, split out of ``runspec-linux`` so
4
+ that package never runs anything as root. Every tool here declares
5
+ ``run_as = "root"``. Install it into a **root-owned** venv: root runs this code,
6
+ so only root may be able to change it. The logic lives in ``runspec-linux-core``.
7
+ """
@@ -0,0 +1,71 @@
1
+ """Package-manager runnables that need root (apt/dnf/yum/zypper/pacman)."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ import runspec as rs
7
+ from runspec_linux_core import autoremove_packages, install_package, list_upgrades, remove_package, upgrade_packages
8
+ from runspec_linux_core.errors import CommandError
9
+
10
+
11
+ def main_list_upgrades() -> None:
12
+ spec = rs.parse("list-upgrades")
13
+ try:
14
+ print(json.dumps(list_upgrades(refresh=not spec.no_refresh.value)))
15
+ except Exception as e:
16
+ print(json.dumps({"error": str(e)}))
17
+ sys.exit(1)
18
+
19
+
20
+ def main_upgrade_packages() -> None:
21
+ rs.parse("upgrade-packages")
22
+ try:
23
+ print(json.dumps(upgrade_packages()))
24
+ except CommandError as e:
25
+ print(json.dumps({"upgraded": False, "error": str(e)}))
26
+ sys.exit(1)
27
+ except Exception as e:
28
+ # Includes ToolNotFoundError (no package manager).
29
+ print(json.dumps({"error": str(e)}))
30
+ sys.exit(1)
31
+
32
+
33
+ def main_install_package() -> None:
34
+ spec = rs.parse("install-package")
35
+ package = spec.package.value
36
+ try:
37
+ print(json.dumps(install_package(package)))
38
+ except CommandError as e:
39
+ print(json.dumps({"package": package, "installed": False, "error": str(e)}))
40
+ sys.exit(1)
41
+ except Exception as e:
42
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
43
+ print(json.dumps({"error": str(e)}))
44
+ sys.exit(1)
45
+
46
+
47
+ def main_remove_package() -> None:
48
+ spec = rs.parse("remove-package")
49
+ package = spec.package.value
50
+ try:
51
+ print(json.dumps(remove_package(package, purge=spec.purge.value)))
52
+ except CommandError as e:
53
+ print(json.dumps({"package": package, "removed": False, "error": str(e)}))
54
+ sys.exit(1)
55
+ except Exception as e:
56
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
57
+ print(json.dumps({"error": str(e)}))
58
+ sys.exit(1)
59
+
60
+
61
+ def main_autoremove_packages() -> None:
62
+ spec = rs.parse("autoremove-packages")
63
+ try:
64
+ print(json.dumps(autoremove_packages(purge=spec.purge.value, dry_run=spec.dry_run.value)))
65
+ except CommandError as e:
66
+ print(json.dumps({"removed": False, "error": str(e)}))
67
+ sys.exit(1)
68
+ except Exception as e:
69
+ # Includes ToolNotFoundError (no package manager).
70
+ print(json.dumps({"error": str(e)}))
71
+ sys.exit(1)
@@ -0,0 +1,23 @@
1
+ """Kernel tuning that needs root."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ import runspec as rs
7
+ from runspec_linux_core import set_sysctl
8
+ from runspec_linux_core.errors import CommandError
9
+
10
+
11
+ def main_set_sysctl() -> None:
12
+ spec = rs.parse("set-sysctl")
13
+ key = spec.key.value
14
+ value = spec.value.value
15
+ persist = spec.persist.value
16
+ try:
17
+ print(json.dumps(set_sysctl(key, value, persist=persist)))
18
+ except CommandError as e:
19
+ print(json.dumps({"key": key, "applied": False, "error": str(e)}))
20
+ sys.exit(1)
21
+ except Exception as e:
22
+ print(json.dumps({"error": str(e)}))
23
+ sys.exit(1)
@@ -0,0 +1,20 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import reboot_host
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_reboot_host() -> None:
10
+ spec = rs.parse("reboot-host")
11
+ delay_minutes = spec.delay_minutes.value
12
+ try:
13
+ print(json.dumps(reboot_host(delay_minutes)))
14
+ except CommandError as e:
15
+ print(json.dumps({"scheduled": False, "error": str(e)}))
16
+ sys.exit(1)
17
+ except Exception as e:
18
+ # Includes ToolNotFoundError (no shutdown/systemctl).
19
+ print(json.dumps({"error": str(e)}))
20
+ sys.exit(1)
@@ -0,0 +1,145 @@
1
+ # runspec-linux-root — Linux admin tools that need root.
2
+ #
3
+ # Install into a ROOT-OWNED venv (e.g. sudo python3 -m venv /opt/venvs/admin):
4
+ # root runs this code, so only root may be able to change it. Every tool declares
5
+ # run_as = "root"; sudoers decides who may run them.
6
+
7
+ [config]
8
+ autonomy-default = "confirm"
9
+
10
+ [config.logging]
11
+ store = "per-run"
12
+
13
+ # ── Packages ──────────────────────────────────────────────────────────────────
14
+
15
+ [list-upgrades]
16
+ serve = ["local"]
17
+ description = "List packages with an available upgrade (refreshes the package index first)"
18
+ autonomy = "autonomous"
19
+ output = "json"
20
+ run_as = "root"
21
+
22
+ [list-upgrades.args.no-refresh]
23
+ type = "flag"
24
+ description = "Skip refreshing the package index — list from the cached index (faster)"
25
+ default = false
26
+
27
+ [upgrade-packages]
28
+ serve = ["local"]
29
+ description = "Refresh the package index and upgrade all installed packages (needs root)"
30
+ autonomy = "confirm"
31
+ output = "json"
32
+ run_as = "root"
33
+
34
+ [install-package]
35
+ serve = ["local"]
36
+ description = "Install one or more packages by name (needs root)"
37
+ autonomy = "confirm"
38
+ output = "json"
39
+ run_as = "root"
40
+
41
+ [install-package.args.package]
42
+ type = "str"
43
+ short = "-p"
44
+ description = "Package name to install (space-separated for several)"
45
+
46
+ [remove-package]
47
+ serve = ["local"]
48
+ description = "Remove one or more packages by name (needs root)"
49
+ autonomy = "confirm"
50
+ output = "json"
51
+ run_as = "root"
52
+
53
+ [remove-package.args.package]
54
+ type = "str"
55
+ short = "-p"
56
+ description = "Package name to remove (space-separated for several)"
57
+
58
+ [remove-package.args.purge]
59
+ type = "flag"
60
+ description = "Also delete the packages' configuration files (apt purge / pacman -Rn; a plain remove on dnf/yum/zypper)"
61
+ default = false
62
+
63
+ [autoremove-packages]
64
+ serve = ["local"]
65
+ description = "Remove packages nothing depends on any more, e.g. old kernels (apt autoremove and equivalents; needs root)"
66
+ autonomy = "confirm"
67
+ output = "json"
68
+ run_as = "root"
69
+
70
+ [autoremove-packages.args.purge]
71
+ type = "flag"
72
+ description = "Also delete the removed packages' configuration files (apt --purge / pacman -n)"
73
+ default = false
74
+
75
+ [autoremove-packages.args.dry-run]
76
+ type = "flag"
77
+ description = "Only list what would be removed; remove nothing (not supported on yum). list-autoremovable does the same without needing approval"
78
+ default = false
79
+
80
+ # ── Kernel ────────────────────────────────────────────────────────────────────
81
+
82
+ [set-sysctl]
83
+ serve = ["local"]
84
+ description = "Set a kernel parameter (sysctl -w), optionally persisting it under /etc/sysctl.d (needs root)"
85
+ autonomy = "confirm"
86
+ output = "json"
87
+ run_as = "root"
88
+
89
+ [set-sysctl.args.key]
90
+ type = "str"
91
+ short = "-k"
92
+ description = "Dotted sysctl key (e.g. net.core.somaxconn)"
93
+
94
+ [set-sysctl.args.value]
95
+ type = "str"
96
+ description = "New value to set"
97
+
98
+ [set-sysctl.args.persist]
99
+ type = "flag"
100
+ description = "Also append the setting to /etc/sysctl.d/99-runspec.conf so it survives a reboot"
101
+ default = false
102
+
103
+ # ── Power ─────────────────────────────────────────────────────────────────────
104
+
105
+ [reboot-host]
106
+ serve = ["local"]
107
+ description = "Reboot the host, optionally after a delay in minutes (needs root)"
108
+ autonomy = "confirm"
109
+ output = "json"
110
+ run_as = "root"
111
+
112
+ [reboot-host.args.delay-minutes]
113
+ type = "int"
114
+ short = "-d"
115
+ description = "Minutes to wait before rebooting (0 = immediately)"
116
+ default = 0
117
+
118
+ # ── Privilege bootstrap ───────────────────────────────────────────────────────
119
+
120
+ # One-time setup so the run_as="root" runnables above need no password. Itself
121
+ # escalates with the supplied password via `sudo -S` (read from stdin, never
122
+ # argv), so it runs as the invoker (run_as = ""). Validated with visudo before +
123
+ # after install and rolled back on failure.
124
+ [enable-passwordless-sudo]
125
+ serve = ["local"]
126
+ description = "Install a validated /etc/sudoers.d drop-in so run_as='root' runnables need no password (one-time bootstrap)"
127
+ autonomy = "confirm"
128
+ output = "json"
129
+ run_as = ""
130
+
131
+ [enable-passwordless-sudo.args.password]
132
+ type = "password"
133
+ description = "Your sudo password — sent over the secret channel and piped to 'sudo -S'; never appears in argv, ps, or logs"
134
+
135
+ [enable-passwordless-sudo.args.user]
136
+ type = "str"
137
+ short = "-u"
138
+ description = "User to grant passwordless sudo (defaults to the current login user)"
139
+ required = false
140
+
141
+ [enable-passwordless-sudo.args.scope]
142
+ type = "choice"
143
+ options = ["scoped", "all"]
144
+ default = "scoped"
145
+ description = "scoped = NOPASSWD only for this venv's tools, run directly (sudo <venv>/bin/<tool> from a shell or cron); all = NOPASSWD: ALL (also needed for runs from runspec-console or the MCP gateway)"
@@ -0,0 +1,23 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import enable_passwordless_sudo
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_enable_passwordless_sudo() -> None:
10
+ spec = rs.parse("enable-passwordless-sudo")
11
+ password = spec.password.value
12
+ user = spec.user.value
13
+ scope = spec.scope.value
14
+ try:
15
+ print(json.dumps(enable_passwordless_sudo(password, user, scope)))
16
+ except CommandError as e:
17
+ # Auth failure / sudoers validation failure (rolled back).
18
+ print(json.dumps({"installed": False, "error": str(e)}))
19
+ sys.exit(1)
20
+ except Exception as e:
21
+ # Includes ValueError (bad scope/user) and ToolNotFoundError (no sudo/visudo).
22
+ print(json.dumps({"error": str(e)}))
23
+ sys.exit(1)
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: runspec-linux-root
3
+ Version: 0.1.0
4
+ Summary: Linux admin runnables that need root (packages, reboot, sysctl) for runspec
5
+ Project-URL: Documentation, https://runspec.app/
6
+ Project-URL: Source, https://github.com/jasonfinestone/runspec/tree/main/packages/python/runspec-linux-root
7
+ Project-URL: Changelog, https://github.com/jasonfinestone/runspec/blob/main/packages/python/runspec-linux-root/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/jasonfinestone/runspec/issues
9
+ Keywords: linux,packages,root,runnable,runspec,sysadmin
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: runspec-linux-core>=0.13.0
12
+ Requires-Dist: runspec>=0.68.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0; extra == 'dev'
16
+ Requires-Dist: ruff==0.15.20; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # runspec-linux-root
20
+
21
+ Linux admin tools that need **root**, as [runspec](https://runspec.app/) runnables:
22
+
23
+ | Tool | What it does | Autonomy |
24
+ |---|---|---|
25
+ | `list-upgrades` | List packages with an available upgrade (refreshes the index) | autonomous |
26
+ | `upgrade-packages` | Refresh the index and upgrade every package | confirm |
27
+ | `install-package` | Install packages by name | confirm |
28
+ | `remove-package` | Remove packages (`--purge` for config files too) | confirm |
29
+ | `autoremove-packages` | Remove packages nothing depends on (`--dry-run` to preview) | confirm |
30
+ | `set-sysctl` | Set a kernel parameter, optionally persisted under `/etc/sysctl.d` | confirm |
31
+ | `reboot-host` | Reboot, optionally after a delay | confirm |
32
+ | `enable-passwordless-sudo` | One-time bootstrap: a validated `/etc/sudoers.d` drop-in | confirm |
33
+
34
+ They work across apt, dnf, yum, zypper and pacman. The logic lives in
35
+ [`runspec-linux-core`](https://pypi.org/project/runspec-linux-core/). The
36
+ everyday tools that never need root are in
37
+ [`runspec-linux`](https://pypi.org/project/runspec-linux/).
38
+
39
+ ## Install into a root-owned venv
40
+
41
+ Every tool here declares `run_as = "root"`: root runs this code. So install it
42
+ into a venv **only root can change**. A venv owned by a service account would
43
+ let that account edit code root later runs.
44
+
45
+ ```bash
46
+ sudo python3 -m venv /opt/venvs/admin
47
+ sudo /opt/venvs/admin/bin/pip install runspec-linux-root
48
+ ```
49
+
50
+ ## Who can run them
51
+
52
+ sudoers decides. An admin runs a tool with sudo:
53
+
54
+ ```bash
55
+ sudo /opt/venvs/admin/bin/upgrade-packages
56
+ ```
57
+
58
+ runspec checks the process really is root (`enforce_run_as`) and refuses
59
+ otherwise, naming the account to `sudo -u` to.
60
+
61
+ **From runspec-console or the MCP gateway**, the tool is run with `sudo -n`,
62
+ which never prompts for a password, so the admin needs a passwordless sudo rule.
63
+ `enable-passwordless-sudo` installs one:
64
+
65
+ - `--scope scoped` allows only this venv's tools, run directly
66
+ (`sudo /opt/venvs/admin/bin/<tool>` from a shell or cron). It does **not**
67
+ cover the console or the gateway, which pass variables to the tool through a
68
+ `sh` prelude so that secrets never go on a command line; sudo then sees `sh`,
69
+ not the tool.
70
+ - `--scope all` grants `NOPASSWD: ALL`, which the console and the gateway need.
@@ -0,0 +1,10 @@
1
+ runspec_linux_root/__init__.py,sha256=Eb0IcoJL1HuyZNuyRessO_FB2NEy2rydYcChzmuFA2s,379
2
+ runspec_linux_root/packages.py,sha256=Hr-OO-0ITr4zwwwMESoV_yXdybLYz1z2CrjwGZ0TJY8,2374
3
+ runspec_linux_root/perf.py,sha256=6Izpoh8lYyFH3X7aq0BJl-4iXdZzSK8nznHoVLyOVEU,623
4
+ runspec_linux_root/power.py,sha256=pJy777zi8eRSUg3xaPf0AfEFUzqt75FIMN4o8KpRP-g,585
5
+ runspec_linux_root/runspec.toml,sha256=Xecag3B3HK8p4vTchzRVBdBsQnj_ccxrcxlWd8QLfns,5364
6
+ runspec_linux_root/sudoers.py,sha256=C9dzTczjFNHRnUr4tK0vSJ_DZHC2Tv_1Hg1cYmZtZC0,783
7
+ runspec_linux_root-0.1.0.dist-info/METADATA,sha256=v-ewfYX_1dN7EjERI0JRa53cbg56z4Mv2s6Y9eEpAgk,3103
8
+ runspec_linux_root-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ runspec_linux_root-0.1.0.dist-info/entry_points.txt,sha256=WYdT-S2_AculeNFD9AHXm-YfFl6O3el8Ceg7ajbu8RQ,550
10
+ runspec_linux_root-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ [console_scripts]
2
+ autoremove-packages = runspec_linux_root.packages:main_autoremove_packages
3
+ enable-passwordless-sudo = runspec_linux_root.sudoers:main_enable_passwordless_sudo
4
+ install-package = runspec_linux_root.packages:main_install_package
5
+ list-upgrades = runspec_linux_root.packages:main_list_upgrades
6
+ reboot-host = runspec_linux_root.power:main_reboot_host
7
+ remove-package = runspec_linux_root.packages:main_remove_package
8
+ set-sysctl = runspec_linux_root.perf:main_set_sysctl
9
+ upgrade-packages = runspec_linux_root.packages:main_upgrade_packages