devstuff 2.4.0__tar.gz → 2.5.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.
Files changed (171) hide show
  1. {devstuff-2.4.0 → devstuff-2.5.0}/.github/workflows/test-installs.yml +1 -0
  2. {devstuff-2.4.0 → devstuff-2.5.0}/CHANGELOG.md +6 -0
  3. {devstuff-2.4.0 → devstuff-2.5.0}/PKG-INFO +42 -2
  4. {devstuff-2.4.0 → devstuff-2.5.0}/README.md +41 -1
  5. {devstuff-2.4.0 → devstuff-2.5.0}/pyproject.toml +1 -1
  6. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/__init__.py +6 -0
  7. devstuff-2.5.0/src/dev_setup/configure/reptyr/__init__.py +1 -0
  8. devstuff-2.5.0/src/dev_setup/configure/reptyr/wizard.py +136 -0
  9. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/tools.yaml +10 -0
  10. devstuff-2.5.0/tests/test_configure_reptyr.py +150 -0
  11. {devstuff-2.4.0 → devstuff-2.5.0}/uv.lock +1 -1
  12. {devstuff-2.4.0 → devstuff-2.5.0}/.claude/skills/run-stuff/SKILL.md +0 -0
  13. {devstuff-2.4.0 → devstuff-2.5.0}/.claude/skills/run-stuff/smoke.sh +0 -0
  14. {devstuff-2.4.0 → devstuff-2.5.0}/.github/workflows/bump.yml +0 -0
  15. {devstuff-2.4.0 → devstuff-2.5.0}/.github/workflows/ci.yml +0 -0
  16. {devstuff-2.4.0 → devstuff-2.5.0}/.github/workflows/publish.yml +0 -0
  17. {devstuff-2.4.0 → devstuff-2.5.0}/.gitignore +0 -0
  18. {devstuff-2.4.0 → devstuff-2.5.0}/CLAUDE.md +0 -0
  19. {devstuff-2.4.0 → devstuff-2.5.0}/dev/Dockerfile +0 -0
  20. {devstuff-2.4.0 → devstuff-2.5.0}/dev/Dockerfile.ci +0 -0
  21. {devstuff-2.4.0 → devstuff-2.5.0}/dev/Makefile +0 -0
  22. {devstuff-2.4.0 → devstuff-2.5.0}/dev/docker-compose.yml +0 -0
  23. {devstuff-2.4.0 → devstuff-2.5.0}/devstuff +0 -0
  24. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/README.md +0 -0
  25. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/agent/development-plan.md +0 -0
  26. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/agent/specifications.md +0 -0
  27. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/agent/stack-decisions.md +0 -0
  28. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/ansible-config/development-plan.md +0 -0
  29. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/ansible-config/specifications.md +0 -0
  30. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/ansible-config/stack-decisions.md +0 -0
  31. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/bat-config/development-plan.md +0 -0
  32. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/bat-config/specifications.md +0 -0
  33. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/bat-config/stack-decisions.md +0 -0
  34. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/commitizen-config/development-plan.md +0 -0
  35. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/commitizen-config/specifications.md +0 -0
  36. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/commitizen-config/stack-decisions.md +0 -0
  37. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/docker-config/development-plan.md +0 -0
  38. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/docker-config/specifications.md +0 -0
  39. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/docker-config/stack-decisions.md +0 -0
  40. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/lazygit-config/development-plan.md +0 -0
  41. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/lazygit-config/specifications.md +0 -0
  42. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/lazygit-config/stack-decisions.md +0 -0
  43. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/precommit-config/development-plan.md +0 -0
  44. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/precommit-config/specifications.md +0 -0
  45. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/precommit-config/stack-decisions.md +0 -0
  46. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/starship-config/development-plan.md +0 -0
  47. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/starship-config/specifications.md +0 -0
  48. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/starship-config/stack-decisions.md +0 -0
  49. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/uv-backbone/development-plan.md +0 -0
  50. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/uv-backbone/specifications.md +0 -0
  51. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/uv-backbone/stack-decisions.md +0 -0
  52. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/verbose-mode/development-plan.md +0 -0
  53. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/verbose-mode/specifications.md +0 -0
  54. {devstuff-2.4.0 → devstuff-2.5.0}/docs/specs/verbose-mode/stack-decisions.md +0 -0
  55. {devstuff-2.4.0 → devstuff-2.5.0}/install.sh +0 -0
  56. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/__init__.py +0 -0
  57. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/__main__.py +0 -0
  58. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/__init__.py +0 -0
  59. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/approval.py +0 -0
  60. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/bridges.py +0 -0
  61. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/catalog.py +0 -0
  62. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/completion.py +0 -0
  63. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/config.py +0 -0
  64. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/loop.py +0 -0
  65. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/ollama.py +0 -0
  66. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/preflight.py +0 -0
  67. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/primitives.py +0 -0
  68. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/registry.py +0 -0
  69. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/sandbox.py +0 -0
  70. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/session.py +0 -0
  71. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/transcript.py +0 -0
  72. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent/wizard.py +0 -0
  73. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent_tools.schema.json +0 -0
  74. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/agent_tools.yaml +0 -0
  75. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/base.py +0 -0
  76. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/catalog.py +0 -0
  77. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/cli.py +0 -0
  78. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/__init__.py +0 -0
  79. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/add_cmd.py +0 -0
  80. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/agent_cmd.py +0 -0
  81. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/catalog_cmd.py +0 -0
  82. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/configure_cmd.py +0 -0
  83. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/delete_cmd.py +0 -0
  84. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/docs_cmd.py +0 -0
  85. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/doctor_cmd.py +0 -0
  86. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/functions_cmd.py +0 -0
  87. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/help_cmd.py +0 -0
  88. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/install_cmd.py +0 -0
  89. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/list_cmd.py +0 -0
  90. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/remove_cmd.py +0 -0
  91. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/run_cmd.py +0 -0
  92. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/skills_cmd.py +0 -0
  93. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/commands/update_cmd.py +0 -0
  94. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/__init__.py +0 -0
  95. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/detect.py +0 -0
  96. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/model.py +0 -0
  97. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/render.py +0 -0
  98. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/validate.py +0 -0
  99. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/ansible/wizard.py +0 -0
  100. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/__init__.py +0 -0
  101. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/detect.py +0 -0
  102. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/model.py +0 -0
  103. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/preview.py +0 -0
  104. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/render.py +0 -0
  105. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/bat/wizard.py +0 -0
  106. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/__init__.py +0 -0
  107. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/detect.py +0 -0
  108. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/model.py +0 -0
  109. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/render.py +0 -0
  110. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/validate.py +0 -0
  111. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/commitizen/wizard.py +0 -0
  112. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/__init__.py +0 -0
  113. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/detect.py +0 -0
  114. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/model.py +0 -0
  115. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/render.py +0 -0
  116. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/validate.py +0 -0
  117. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/docker/wizard.py +0 -0
  118. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/__init__.py +0 -0
  119. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/detect.py +0 -0
  120. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/model.py +0 -0
  121. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/render.py +0 -0
  122. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/validate.py +0 -0
  123. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/lazygit/wizard.py +0 -0
  124. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/__init__.py +0 -0
  125. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/detect.py +0 -0
  126. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/model.py +0 -0
  127. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/render.py +0 -0
  128. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/validate.py +0 -0
  129. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/precommit/wizard.py +0 -0
  130. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/__init__.py +0 -0
  131. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/fonts.py +0 -0
  132. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/model.py +0 -0
  133. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/preview.py +0 -0
  134. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/render.py +0 -0
  135. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/configure/starship/wizard.py +0 -0
  136. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/doctor.py +0 -0
  137. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/function_runner.py +0 -0
  138. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/functions.schema.json +0 -0
  139. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/functions.yaml +0 -0
  140. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/functions_catalog.py +0 -0
  141. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/functions_registry.py +0 -0
  142. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/generic.py +0 -0
  143. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/registry.py +0 -0
  144. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/ui.py +0 -0
  145. {devstuff-2.4.0 → devstuff-2.5.0}/src/dev_setup/verbose.py +0 -0
  146. {devstuff-2.4.0 → devstuff-2.5.0}/tests/__init__.py +0 -0
  147. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/__init__.py +0 -0
  148. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/conftest.py +0 -0
  149. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/test_agent_smoke.py +0 -0
  150. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/test_agent_tui.py +0 -0
  151. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/test_commitizen_config.py +0 -0
  152. {devstuff-2.4.0 → devstuff-2.5.0}/tests/integration/test_tools.py +0 -0
  153. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent.py +0 -0
  154. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent_completion.py +0 -0
  155. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent_loop.py +0 -0
  156. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent_sandbox.py +0 -0
  157. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent_tools.py +0 -0
  158. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_agent_wizard.py +0 -0
  159. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_catalog.py +0 -0
  160. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_ansible.py +0 -0
  161. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_bat.py +0 -0
  162. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_commitizen.py +0 -0
  163. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_docker.py +0 -0
  164. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_lazygit.py +0 -0
  165. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_precommit.py +0 -0
  166. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_configure_starship.py +0 -0
  167. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_doctor.py +0 -0
  168. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_functions.py +0 -0
  169. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_generic.py +0 -0
  170. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_registry.py +0 -0
  171. {devstuff-2.4.0 → devstuff-2.5.0}/tests/test_verbose.py +0 -0
@@ -74,6 +74,7 @@ jobs:
74
74
  - pre-commit # uvx tool, requires uv
75
75
  - python # uv-managed CPython, requires uv
76
76
  - ruby # compiles from source — this job takes ~15 minutes
77
+ - reptyr
77
78
  - saml2aws
78
79
  - starship
79
80
  - uv
@@ -1,3 +1,9 @@
1
+ ## v2.5.0 (2026-09-09)
2
+
3
+ ### Features
4
+
5
+ - **reptyr**: add tool and ptrace permission wizard
6
+
1
7
  ## v2.4.0 (2026-08-20)
2
8
 
3
9
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devstuff
3
- Version: 2.4.0
3
+ Version: 2.5.0
4
4
  Summary: Development environment setup CLI for Linux
5
5
  Project-URL: Repository, https://github.com/thesawdawg/dev-setup-py
6
6
  Author-email: Sawyer <sawyerksu@gmail.com>
@@ -324,6 +324,7 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
324
324
 
325
325
  | Tool | What it configures |
326
326
  |------|-------------------|
327
+ | `reptyr` | Temporary or persistent Linux ptrace permissions; command-line usage guidance |
327
328
  | `ansible` | `ansible.cfg` — inventory paths, forks, pipelining, become and vault |
328
329
  | `bat` | Theme, decorations, paging, and the man-page/`cat` shell integration |
329
330
  | `commitizen` | Commit types, what each one bumps, git tags, and changelog sections |
@@ -332,7 +333,8 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
332
333
  | `pre-commit` | Which git hooks run, when they run, and what they're allowed to rewrite |
333
334
  | `starship` | Prompt style, colour palette, which sections appear, and layout |
334
335
 
335
- Each wizard checks its result against the real tool before saving. What that check can prove
336
+ The reptyr wizard verifies the live kernel setting after applying it. Other wizards check
337
+ their result against the real tool before saving. What that check can prove
336
338
  varies a lot, and the wizards say so rather than implying more than they know:
337
339
 
338
340
  | Tool | What the tool itself checks | What the wizard has to check instead |
@@ -739,6 +741,43 @@ characters, and a quoted boolean is read as `False`. A retired section in an exi
739
741
  *preserved and reported*, never silently migrated — the wizard can't know whether an older
740
742
  ansible elsewhere is still reading it.
741
743
 
744
+ #### reptyr wizard
745
+
746
+ ```bash
747
+ devstuff install reptyr # apt package; offers setup after installation
748
+ devstuff configure reptyr # also works for an already-installed package
749
+ devstuff configure reptyr --path # /etc/sysctl.d/99-devstuff-reptyr.conf
750
+ devstuff configure reptyr --output /tmp/reptyr.conf # export only, no live changes
751
+ ```
752
+
753
+ The wizard shows the current `kernel.yama.ptrace_scope` and offers keeping it unchanged,
754
+ enabling same-user attach (`0`), restricted attach (`1`), or admin-only attach (`2`).
755
+ Choose a change for this boot only, or save and apply a persistent sysctl drop-in. The
756
+ preview and confirmation come before any write or sudo command. Persistent changes use
757
+ `sudo install --backup=numbered` to preserve an existing drop-in, then load only that file
758
+ with `sudo sysctl --load`. The wizard reads the live value back to verify success.
759
+
760
+ Setting `0` relaxes protection **system-wide**, allowing same-user processes to inspect
761
+ and control other dumpable processes. It does not grant cross-user access or bypass
762
+ container/security policies. Mode `3` cannot be changed until reboot, so the wizard
763
+ refuses live changes in that state. If Yama is unavailable or unreadable, it offers only
764
+ guidance; offline `--output` export still works. Other sysctl files may override the saved
765
+ value at boot. Temporary changes leave all boot configuration untouched.
766
+
767
+ To return to restricted attach, select `1` and the persistent option. This sets a known
768
+ policy rather than claiming to restore an unknown original value. Removing the reptyr
769
+ package does not remove this system policy; rerun the wizard before uninstalling if needed.
770
+ For unattended installs, run `devstuff configure reptyr` later in a terminal.
771
+
772
+ reptyr has no rc file: its remaining options are per invocation, and the wizard explains
773
+ them rather than inventing persistent defaults. Use `reptyr PID` inside the destination
774
+ terminal; `-T` steals the whole TTY (SSH children require root); `-s` redirects standard
775
+ streams even when they weren't attached to a terminal; `-l` creates a PTY, optionally
776
+ running a command with `REPTYR_PTY` set; `-L` also attaches that command's streams and
777
+ session; `-V` enables debug output, `-v` prints the version, and `-h` shows help.
778
+ See the [upstream manual](https://github.com/nelhage/reptyr/blob/master/reptyr.1) and
779
+ [Linux Yama documentation](https://docs.kernel.org/admin-guide/LSM/Yama.html).
780
+
742
781
  #### lazygit wizard
743
782
 
744
783
  lazygit refuses to start on a value of the wrong **type**, and silently ignores an unknown
@@ -1230,6 +1269,7 @@ Optional utilities you may want on some machines.
1230
1269
  | `homebrew` | Homebrew | Package manager for Linux (and macOS) | `brew --help` |
1231
1270
  | `ipython` | IPython | Enhanced interactive Python shell with rich tab completion and magic commands | `ipython --help` |
1232
1271
  | `lazygit` | lazygit | TUI git client for fast, keyboard-driven git workflows (`devstuff configure lazygit`) | `lazygit --help` |
1272
+ | `reptyr` | reptyr | Move a running process to another terminal (`devstuff configure reptyr` for permissions) | `reptyr -h` |
1233
1273
  | `llm-checker` | llm-checker | Check LLM compatibility with your hardware | `llm-checker --help` |
1234
1274
  | `lmstudio` | LM Studio | Run large language models locally with a GUI | `lms --help` |
1235
1275
  | `mkcert` | mkcert | Zero-config local HTTPS certificates | `mkcert --help` |
@@ -297,6 +297,7 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
297
297
 
298
298
  | Tool | What it configures |
299
299
  |------|-------------------|
300
+ | `reptyr` | Temporary or persistent Linux ptrace permissions; command-line usage guidance |
300
301
  | `ansible` | `ansible.cfg` — inventory paths, forks, pipelining, become and vault |
301
302
  | `bat` | Theme, decorations, paging, and the man-page/`cat` shell integration |
302
303
  | `commitizen` | Commit types, what each one bumps, git tags, and changelog sections |
@@ -305,7 +306,8 @@ devstuff configure starship --output /tmp/try.toml # write elsewhere, leave th
305
306
  | `pre-commit` | Which git hooks run, when they run, and what they're allowed to rewrite |
306
307
  | `starship` | Prompt style, colour palette, which sections appear, and layout |
307
308
 
308
- Each wizard checks its result against the real tool before saving. What that check can prove
309
+ The reptyr wizard verifies the live kernel setting after applying it. Other wizards check
310
+ their result against the real tool before saving. What that check can prove
309
311
  varies a lot, and the wizards say so rather than implying more than they know:
310
312
 
311
313
  | Tool | What the tool itself checks | What the wizard has to check instead |
@@ -712,6 +714,43 @@ characters, and a quoted boolean is read as `False`. A retired section in an exi
712
714
  *preserved and reported*, never silently migrated — the wizard can't know whether an older
713
715
  ansible elsewhere is still reading it.
714
716
 
717
+ #### reptyr wizard
718
+
719
+ ```bash
720
+ devstuff install reptyr # apt package; offers setup after installation
721
+ devstuff configure reptyr # also works for an already-installed package
722
+ devstuff configure reptyr --path # /etc/sysctl.d/99-devstuff-reptyr.conf
723
+ devstuff configure reptyr --output /tmp/reptyr.conf # export only, no live changes
724
+ ```
725
+
726
+ The wizard shows the current `kernel.yama.ptrace_scope` and offers keeping it unchanged,
727
+ enabling same-user attach (`0`), restricted attach (`1`), or admin-only attach (`2`).
728
+ Choose a change for this boot only, or save and apply a persistent sysctl drop-in. The
729
+ preview and confirmation come before any write or sudo command. Persistent changes use
730
+ `sudo install --backup=numbered` to preserve an existing drop-in, then load only that file
731
+ with `sudo sysctl --load`. The wizard reads the live value back to verify success.
732
+
733
+ Setting `0` relaxes protection **system-wide**, allowing same-user processes to inspect
734
+ and control other dumpable processes. It does not grant cross-user access or bypass
735
+ container/security policies. Mode `3` cannot be changed until reboot, so the wizard
736
+ refuses live changes in that state. If Yama is unavailable or unreadable, it offers only
737
+ guidance; offline `--output` export still works. Other sysctl files may override the saved
738
+ value at boot. Temporary changes leave all boot configuration untouched.
739
+
740
+ To return to restricted attach, select `1` and the persistent option. This sets a known
741
+ policy rather than claiming to restore an unknown original value. Removing the reptyr
742
+ package does not remove this system policy; rerun the wizard before uninstalling if needed.
743
+ For unattended installs, run `devstuff configure reptyr` later in a terminal.
744
+
745
+ reptyr has no rc file: its remaining options are per invocation, and the wizard explains
746
+ them rather than inventing persistent defaults. Use `reptyr PID` inside the destination
747
+ terminal; `-T` steals the whole TTY (SSH children require root); `-s` redirects standard
748
+ streams even when they weren't attached to a terminal; `-l` creates a PTY, optionally
749
+ running a command with `REPTYR_PTY` set; `-L` also attaches that command's streams and
750
+ session; `-V` enables debug output, `-v` prints the version, and `-h` shows help.
751
+ See the [upstream manual](https://github.com/nelhage/reptyr/blob/master/reptyr.1) and
752
+ [Linux Yama documentation](https://docs.kernel.org/admin-guide/LSM/Yama.html).
753
+
715
754
  #### lazygit wizard
716
755
 
717
756
  lazygit refuses to start on a value of the wrong **type**, and silently ignores an unknown
@@ -1203,6 +1242,7 @@ Optional utilities you may want on some machines.
1203
1242
  | `homebrew` | Homebrew | Package manager for Linux (and macOS) | `brew --help` |
1204
1243
  | `ipython` | IPython | Enhanced interactive Python shell with rich tab completion and magic commands | `ipython --help` |
1205
1244
  | `lazygit` | lazygit | TUI git client for fast, keyboard-driven git workflows (`devstuff configure lazygit`) | `lazygit --help` |
1245
+ | `reptyr` | reptyr | Move a running process to another terminal (`devstuff configure reptyr` for permissions) | `reptyr -h` |
1206
1246
  | `llm-checker` | llm-checker | Check LLM compatibility with your hardware | `llm-checker --help` |
1207
1247
  | `lmstudio` | LM Studio | Run large language models locally with a GUI | `lms --help` |
1208
1248
  | `mkcert` | mkcert | Zero-config local HTTPS certificates | `mkcert --help` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "devstuff"
3
- version = "2.4.0"
3
+ version = "2.5.0"
4
4
  description = "Development environment setup CLI for Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -39,6 +39,12 @@ class Configurator:
39
39
 
40
40
 
41
41
  CONFIGURATORS: dict[str, Configurator] = {
42
+ "reptyr": Configurator(
43
+ key="reptyr",
44
+ label="reptyr",
45
+ description="Temporary or persistent ptrace permissions and command-line usage",
46
+ module="dev_setup.configure.reptyr.wizard",
47
+ ),
42
48
  "ansible": Configurator(
43
49
  key="ansible",
44
50
  label="Ansible",
@@ -0,0 +1 @@
1
+ """reptyr permission setup; reptyr itself has no configuration file."""
@@ -0,0 +1,136 @@
1
+ """Configure Linux Yama permissions without inventing a reptyr rc file.
2
+
3
+ References: upstream reptyr.1 and docs.kernel.org/admin-guide/LSM/Yama.html.
4
+ Only sysctl modes 0-2 are offered: mode 3 cannot be changed until reboot.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import questionary
13
+
14
+ from dev_setup import ui
15
+ from dev_setup.generic import _run
16
+
17
+ SCOPE_PATH = Path("/proc/sys/kernel/yama/ptrace_scope")
18
+ SYSTEM_PATH = Path("/etc/sysctl.d/99-devstuff-reptyr.conf")
19
+ SETTING = "kernel.yama.ptrace_scope"
20
+ MODES = {
21
+ 0: "Enable same-user attach (0) — permits ordinary reptyr PID",
22
+ 1: "Restrict attach (1) — descendants or explicitly permitted processes",
23
+ 2: "Admin-only attach (2) — requires CAP_SYS_PTRACE",
24
+ }
25
+
26
+
27
+ def config_path() -> Path:
28
+ return SYSTEM_PATH
29
+
30
+
31
+ def read_scope() -> int | None:
32
+ try:
33
+ value = int(SCOPE_PATH.read_text().strip())
34
+ except (OSError, ValueError):
35
+ return None
36
+ return value if value in (0, 1, 2, 3) else None
37
+
38
+
39
+ def render(scope: int) -> str:
40
+ if scope not in MODES:
41
+ raise ValueError("Only ptrace_scope values 0, 1 and 2 are supported")
42
+ return (
43
+ "# Managed by devstuff configure reptyr; affects all processes on this host.\n"
44
+ "# 0: same-user attach; 1: restricted attach; 2: admin-only attach.\n"
45
+ f"{SETTING} = {scope}\n"
46
+ )
47
+
48
+
49
+ def _usage() -> None:
50
+ ui.info("reptyr has no rc file or persistent command-line defaults.")
51
+ for line in (
52
+ "reptyr PID Move a process into this terminal (for example, inside tmux).",
53
+ "reptyr -T PID Steal the whole TTY without ptracing the target; SSH children need root.",
54
+ "reptyr -s PID Redirect stdin/out/err even when they were not attached to a terminal.",
55
+ "reptyr -l Create a PTY; optional COMMAND receives REPTYR_PTY in its environment.",
56
+ "reptyr -L COMMAND Also attach the command's standard streams and session to the PTY.",
57
+ "reptyr -V PID Debug logging; -v prints the version; -h shows installed-version help.",
58
+ ):
59
+ ui.dim(line)
60
+
61
+
62
+ def run(*, target: Path | None = None) -> int | None:
63
+ ui.section("reptyr permissions")
64
+ _usage()
65
+ current = read_scope()
66
+ ui.info(f"Current {SETTING}: {current if current is not None else 'unavailable'}")
67
+ # --output is always an offline export, even on a host without Yama.
68
+ if target is None and current is None:
69
+ ui.warn("Yama is absent or unreadable; no permission change can be applied here.")
70
+ ui.dim("Other ptrace restrictions may still apply. Use --output to export a sysctl config.")
71
+ return None
72
+ if target is None and current == 3:
73
+ ui.warn("ptrace_scope=3 is locked until reboot, even for root. No changes made.")
74
+ return None
75
+
76
+ choices = [questionary.Choice("Keep current permissions / cancel", value="keep")]
77
+ choices.extend(questionary.Choice(label, value=str(value)) for value, label in MODES.items())
78
+ selected = ui.select("Permission policy:", choices)
79
+ if not selected or selected == "keep":
80
+ return None
81
+ scope = int(selected)
82
+ content = render(scope)
83
+ if scope == 0:
84
+ ui.warn("This relaxes ptrace protection system-wide: same-user processes can inspect or control")
85
+ ui.warn("other dumpable processes. Other users' permissions and container policies still apply.")
86
+
87
+ lifetime = "persistent"
88
+ if target is None:
89
+ lifetime = ui.select("Apply for:", [
90
+ questionary.Choice("This boot only (no files changed)", value="temporary"),
91
+ questionary.Choice("Now and at boot (save a sysctl drop-in)", value="persistent"),
92
+ questionary.Choice("Cancel", value="cancel"),
93
+ ])
94
+ if lifetime not in ("temporary", "persistent"):
95
+ return None
96
+
97
+ if target is not None:
98
+ ui.info(f"Export only: {target}; existing content will be replaced. No sudo or live changes.")
99
+ elif lifetime == "temporary":
100
+ ui.info(f"Will run: sudo sysctl -w {SETTING}={scope}")
101
+ ui.dim("Existing boot configuration stays in place and may restore a different value.")
102
+ else:
103
+ ui.info(f"Will save {SYSTEM_PATH} with sudo install (numbered backup if it exists).")
104
+ ui.info(f"Then run: sudo sysctl --load {SYSTEM_PATH}")
105
+ ui.dim("Other sysctl files can override this at boot; only this drop-in is loaded now.")
106
+ ui.console.print(content, markup=False)
107
+ if not ui.confirm("Apply this configuration?", default=False):
108
+ return None
109
+
110
+ if target is not None:
111
+ target.parent.mkdir(parents=True, exist_ok=True)
112
+ target.write_text(content, encoding="utf-8")
113
+ ui.success(f"Exported to {target}; live permissions unchanged.")
114
+ return scope
115
+
116
+ saved = False
117
+ try:
118
+ if lifetime == "persistent":
119
+ with tempfile.TemporaryDirectory(prefix="devstuff-reptyr-") as directory:
120
+ source = Path(directory) / "reptyr.conf"
121
+ source.write_text(content, encoding="utf-8")
122
+ _run(["sudo", "install", "--backup=numbered", "-m", "0644", str(source), str(SYSTEM_PATH)])
123
+ saved = True
124
+ _run(["sudo", "sysctl", "--load", str(SYSTEM_PATH)])
125
+ else:
126
+ _run(["sudo", "sysctl", "-w", f"{SETTING}={scope}"])
127
+ except (RuntimeError, OSError) as exc:
128
+ ui.error(f"Could not apply reptyr permissions: {exc}")
129
+ if saved:
130
+ ui.warn(f"{SYSTEM_PATH} was saved, but the live change failed; review it before reboot.")
131
+ return None
132
+ if read_scope() != scope:
133
+ ui.error("The live ptrace_scope does not match the requested value; permissions are not verified.")
134
+ return None
135
+ ui.success(f"Verified {SETTING}={scope}.")
136
+ return scope
@@ -1,5 +1,15 @@
1
1
  version: 1
2
2
  tools:
3
+ reptyr:
4
+ name: reptyr
5
+ description: Move a running process to another terminal (permission wizard available)
6
+ category: tools
7
+ type: apt
8
+ apt_packages: reptyr
9
+ check_cmd: reptyr
10
+ help_cmd: reptyr -h
11
+ docs_url: https://github.com/nelhage/reptyr
12
+
3
13
  ansible:
4
14
  name: Ansible
5
15
  description: Automation engine for configuration management and app deployment
@@ -0,0 +1,150 @@
1
+ from unittest.mock import Mock
2
+
3
+ import pytest
4
+ from click.testing import CliRunner
5
+
6
+ from dev_setup import configure
7
+ from dev_setup.commands.configure_cmd import configure_cmd, offer_after_install
8
+ from dev_setup.configure.reptyr import wizard
9
+
10
+
11
+ @pytest.fixture
12
+ def flow(monkeypatch, tmp_path):
13
+ scope = tmp_path / "ptrace_scope"
14
+ scope.write_text("1\n")
15
+ monkeypatch.setattr(wizard, "SCOPE_PATH", scope)
16
+ monkeypatch.setattr(wizard, "SYSTEM_PATH", tmp_path / "99-devstuff-reptyr.conf")
17
+ runner = Mock()
18
+ monkeypatch.setattr(wizard, "_run", runner)
19
+ monkeypatch.setattr(wizard.ui, "confirm", lambda *a, **kw: True)
20
+ return scope, runner
21
+
22
+
23
+ def answers(monkeypatch, *values):
24
+ iterator = iter(values)
25
+ monkeypatch.setattr(wizard.ui, "select", lambda *a, **kw: next(iterator))
26
+
27
+
28
+ def test_registered_and_path(flow):
29
+ assert configure.get("reptyr").load() is wizard
30
+ result = CliRunner().invoke(configure_cmd, ["reptyr", "--path"])
31
+ assert result.exit_code == 0
32
+ assert str(wizard.SYSTEM_PATH) in result.output
33
+
34
+
35
+ @pytest.mark.parametrize("value", ["0", "1", "2", "3", "broken", "4"])
36
+ def test_detection(flow, value):
37
+ path, _ = flow
38
+ path.write_text(value)
39
+ assert wizard.read_scope() == (int(value) if value in "0123" else None)
40
+
41
+
42
+ @pytest.mark.parametrize("state", ["missing", "3"])
43
+ def test_unavailable_or_locked_cannot_mutate(flow, monkeypatch, state):
44
+ path, runner = flow
45
+ if state == "missing":
46
+ path.unlink()
47
+ else:
48
+ path.write_text(state)
49
+ answers(monkeypatch) # no prompts should be needed
50
+ assert wizard.run() is None
51
+ runner.assert_not_called()
52
+
53
+
54
+ @pytest.mark.parametrize("selection", [None, "keep"])
55
+ def test_cancel_policy(flow, monkeypatch, selection):
56
+ answers(monkeypatch, selection)
57
+ assert wizard.run() is None
58
+ flow[1].assert_not_called()
59
+
60
+
61
+ def test_declined_confirmation_writes_nothing(flow, monkeypatch, tmp_path):
62
+ answers(monkeypatch, "0")
63
+ monkeypatch.setattr(wizard.ui, "confirm", lambda *a, **kw: False)
64
+ target = tmp_path / "export.conf"
65
+ assert wizard.run(target=target) is None
66
+ assert not target.exists()
67
+ flow[1].assert_not_called()
68
+
69
+
70
+ @pytest.mark.parametrize("value", [0, 1, 2])
71
+ def test_offline_export_never_uses_sudo(flow, monkeypatch, tmp_path, value):
72
+ flow[0].unlink()
73
+ answers(monkeypatch, str(value))
74
+ target = tmp_path / "export.conf"
75
+ assert wizard.run(target=target) == value
76
+ assert f"kernel.yama.ptrace_scope = {value}\n" in target.read_text()
77
+ flow[1].assert_not_called()
78
+
79
+
80
+ def test_temporary_change_verified_without_file(flow, monkeypatch):
81
+ path, runner = flow
82
+ runner.side_effect = lambda cmd: path.write_text("0")
83
+ answers(monkeypatch, "0", "temporary")
84
+ assert wizard.run() == 0
85
+ runner.assert_called_once_with(["sudo", "sysctl", "-w", "kernel.yama.ptrace_scope=0"])
86
+ assert not wizard.SYSTEM_PATH.exists()
87
+
88
+
89
+ def test_persistent_backup_and_load_only_own_file(flow, monkeypatch):
90
+ path, runner = flow
91
+
92
+ def execute(cmd):
93
+ if cmd[1] == "install":
94
+ from pathlib import Path
95
+ assert "--backup=numbered" in cmd
96
+ assert "kernel.yama.ptrace_scope = 0" in Path(cmd[-2]).read_text()
97
+ else:
98
+ assert cmd == ["sudo", "sysctl", "--load", str(wizard.SYSTEM_PATH)]
99
+ path.write_text("0")
100
+
101
+ runner.side_effect = execute
102
+ answers(monkeypatch, "0", "persistent")
103
+ assert wizard.run() == 0
104
+ assert runner.call_count == 2
105
+
106
+
107
+ @pytest.mark.parametrize("failure", [RuntimeError("denied"), FileNotFoundError("sudo")])
108
+ def test_failed_apply_never_reports_success(flow, monkeypatch, failure):
109
+ flow[1].side_effect = failure
110
+ success = Mock()
111
+ monkeypatch.setattr(wizard.ui, "success", success)
112
+ answers(monkeypatch, "0", "temporary")
113
+ assert wizard.run() is None
114
+ success.assert_not_called()
115
+
116
+
117
+ def test_saved_but_not_applied_is_reported(flow, monkeypatch):
118
+ flow[1].side_effect = [None, RuntimeError("read-only sysctl")]
119
+ warning = Mock()
120
+ monkeypatch.setattr(wizard.ui, "warn", warning)
121
+ answers(monkeypatch, "0", "persistent")
122
+ assert wizard.run() is None
123
+ assert "was saved, but the live change failed" in warning.call_args.args[0]
124
+
125
+
126
+ def test_readback_mismatch_is_failure(flow, monkeypatch):
127
+ answers(monkeypatch, "0", "temporary")
128
+ assert wizard.run() is None
129
+
130
+
131
+ def test_mode_three_cannot_be_generated():
132
+ with pytest.raises(ValueError):
133
+ wizard.render(3)
134
+
135
+
136
+ def test_post_install_offer_runs_wizard(monkeypatch):
137
+ monkeypatch.setattr("sys.stdin.isatty", lambda: True)
138
+ monkeypatch.setattr(wizard.ui, "confirm", lambda *a, **kw: True)
139
+ run = Mock()
140
+ monkeypatch.setattr(wizard, "run", run)
141
+ offer_after_install("reptyr")
142
+ run.assert_called_once_with()
143
+
144
+
145
+ def test_noninteractive_install_does_not_change_permissions(monkeypatch):
146
+ monkeypatch.setattr("sys.stdin.isatty", lambda: False)
147
+ run = Mock()
148
+ monkeypatch.setattr(wizard, "run", run)
149
+ offer_after_install("reptyr")
150
+ run.assert_not_called()
@@ -167,7 +167,7 @@ wheels = [
167
167
 
168
168
  [[package]]
169
169
  name = "devstuff"
170
- version = "2.4.0"
170
+ version = "2.5.0"
171
171
  source = { editable = "." }
172
172
  dependencies = [
173
173
  { name = "click" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes