runspec-linux 0.3.0__tar.gz → 0.4.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.

Potentially problematic release.


This version of runspec-linux might be problematic. Click here for more details.

@@ -1,5 +1,29 @@
1
1
  # runspec-linux Changelog
2
2
 
3
+ ## [0.4.0] — 2026-06-20
4
+
5
+ Make the privileged package/power runnables escalate via runspec's `run_as`, and
6
+ add a one-time bootstrap runnable to enable it without a password. Bumps the
7
+ `runspec-linux-core` floor to `>=0.3.0`.
8
+
9
+ - **`run_as = "root"`** on `upgrade-packages`, `install-package`,
10
+ `remove-package`, and `reboot-host`. The console's SSH executor now wraps these
11
+ in `sudo -u root …` before they run, instead of failing with permission denied
12
+ when the SSH login user isn't root. (`list-packages` / `list-upgrades` stay
13
+ un-escalated — least privilege; `list-upgrades --no-refresh` works as any user.)
14
+ A site with a service account can override `run_as` in its own spec.
15
+ - **New `enable-passwordless-sudo`** (confirm) — the one-time setup that makes the
16
+ above work password-free. Takes a **`password`-typed** arg (delivered over the
17
+ console's secret channel and piped to `sudo -S` — never on argv/ps/logs),
18
+ optional `--user` (defaults to the login user), and `--scope`
19
+ (`scoped` = the runspec venv runnables only; `all` = `NOPASSWD: ALL`). Installs
20
+ a `visudo`-validated `/etc/sudoers.d/runspec`, rolling back on a bad result. It
21
+ declares no `run_as` (it bootstraps root itself).
22
+
23
+ Setup flow: run `enable-passwordless-sudo` once per host (pick `all` for
24
+ agent-driven fleet management; `scoped` is enough for manual/CLI runs), then
25
+ `upgrade-packages` and friends run without a password.
26
+
3
27
  ## [0.3.0] — 2026-06-20
4
28
 
5
29
  Add 6 runnables for managing a fleet's packages and power state from
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runspec-linux
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Linux system admin runnables for runspec
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: runspec-linux-core>=0.2.0
6
+ Requires-Dist: runspec-linux-core>=0.3.0
7
7
  Requires-Dist: runspec>=0.27.0
8
8
  Provides-Extra: dev
9
9
  Requires-Dist: mypy; extra == 'dev'
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-linux"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Linux system admin runnables for runspec"
10
10
  dependencies = [
11
11
  "runspec>=0.27.0",
12
- "runspec-linux-core>=0.2.0",
12
+ "runspec-linux-core>=0.3.0",
13
13
  ]
14
14
 
15
15
  [project.scripts]
@@ -39,6 +39,7 @@ upgrade-packages = "runspec_linux.packages:main_upgrade_packages"
39
39
  install-package = "runspec_linux.packages:main_install_package"
40
40
  remove-package = "runspec_linux.packages:main_remove_package"
41
41
  reboot-host = "runspec_linux.power:main_reboot_host"
42
+ enable-passwordless-sudo = "runspec_linux.sudoers:main_enable_passwordless_sudo"
42
43
  nc-command = "runspec_linux.nc_command:main"
43
44
 
44
45
  [project.optional-dependencies]
@@ -15,6 +15,7 @@ from runspec_linux_core import (
15
15
  container_logs,
16
16
  detect_manager,
17
17
  disk_usage,
18
+ enable_passwordless_sudo,
18
19
  find_large_files,
19
20
  install_package,
20
21
  journalctl,
@@ -46,6 +47,7 @@ __all__ = [
46
47
  "container_logs",
47
48
  "detect_manager",
48
49
  "disk_usage",
50
+ "enable_passwordless_sudo",
49
51
  "find_large_files",
50
52
  "install_package",
51
53
  "journalctl",
@@ -335,12 +335,14 @@ serve = ["local"]
335
335
  description = "Refresh the package index and upgrade all installed packages (needs root)"
336
336
  autonomy = "confirm"
337
337
  output = "json"
338
+ run_as = "root"
338
339
 
339
340
  [install-package]
340
341
  serve = ["local"]
341
342
  description = "Install one or more packages by name (needs root)"
342
343
  autonomy = "confirm"
343
344
  output = "json"
345
+ run_as = "root"
344
346
 
345
347
  [install-package.args.package]
346
348
  type = "str"
@@ -352,6 +354,7 @@ serve = ["local"]
352
354
  description = "Remove one or more packages by name (needs root)"
353
355
  autonomy = "confirm"
354
356
  output = "json"
357
+ run_as = "root"
355
358
 
356
359
  [remove-package.args.package]
357
360
  type = "str"
@@ -365,6 +368,7 @@ serve = ["local"]
365
368
  description = "Reboot the host, optionally after a delay in minutes (needs root)"
366
369
  autonomy = "confirm"
367
370
  output = "json"
371
+ run_as = "root"
368
372
 
369
373
  [reboot-host.args.delay-minutes]
370
374
  type = "int"
@@ -372,6 +376,34 @@ short = "-d"
372
376
  description = "Minutes to wait before rebooting (0 = immediately)"
373
377
  default = 0
374
378
 
379
+ # ── Privilege bootstrap ───────────────────────────────────────────────────────
380
+
381
+ # One-time setup so the run_as="root" runnables above need no password. Itself
382
+ # escalates with the supplied password via `sudo -S` (read from stdin, never
383
+ # argv), so it must NOT declare run_as. Validated with visudo before + after
384
+ # install and rolled back on failure.
385
+ [enable-passwordless-sudo]
386
+ serve = ["local"]
387
+ description = "Install a validated /etc/sudoers.d drop-in so run_as='root' runnables need no password (one-time bootstrap)"
388
+ autonomy = "confirm"
389
+ output = "json"
390
+
391
+ [enable-passwordless-sudo.args.password]
392
+ type = "password"
393
+ description = "Your sudo password — sent over the secret channel and piped to 'sudo -S'; never appears in argv, ps, or logs"
394
+
395
+ [enable-passwordless-sudo.args.user]
396
+ type = "str"
397
+ short = "-u"
398
+ description = "User to grant passwordless sudo (defaults to the current login user)"
399
+ required = false
400
+
401
+ [enable-passwordless-sudo.args.scope]
402
+ type = "choice"
403
+ options = ["scoped", "all"]
404
+ default = "scoped"
405
+ description = "scoped = NOPASSWD only for the runspec venv runnables (manual/CLI runs); all = NOPASSWD: ALL (also covers agent-driven runs)"
406
+
375
407
  # ── TCP interfaces (nc-command) ────────────────────────────────────────────────
376
408
 
377
409
  [nc-command]
@@ -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)
File without changes