samagotchi 0.133.0 → 0.134.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -1
  3. data/docs/cli.md +11 -0
  4. data/docs/configuration.md +8 -4
  5. data/docs/guardrails.md +102 -13
  6. data/docs/internals/background-tasks.md +1 -0
  7. data/docs/internals/participants.md +6 -5
  8. data/docs/internals/tool-guardrails.md +2 -0
  9. data/docs/plugins.md +2 -1
  10. data/docs/sessions.md +3 -2
  11. data/docs/testing.md +7 -2
  12. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  13. data/lib/samagotchi/chi_home.rb +12 -2
  14. data/lib/samagotchi/desktop/macos.rb +19 -4
  15. data/lib/samagotchi/engine.rb +13 -0
  16. data/lib/samagotchi/guardrails/implicit_writers.rb +143 -0
  17. data/lib/samagotchi/guardrails/pair_boundary.rb +6 -4
  18. data/lib/samagotchi/guardrails/pair_command.rb +493 -60
  19. data/lib/samagotchi/guardrails/parent_approvals.rb +1 -1
  20. data/lib/samagotchi/guardrails/read_only_shell.rb +34 -2
  21. data/lib/samagotchi/guardrails/shell_git_dirs.rb +6 -1
  22. data/lib/samagotchi/guardrails/shell_lex.rb +23 -0
  23. data/lib/samagotchi/guardrails/shell_paths.rb +3 -2
  24. data/lib/samagotchi/guardrails/targets.rb +8 -3
  25. data/lib/samagotchi/home_command.rb +22 -6
  26. data/lib/samagotchi/home_doctor.rb +36 -3
  27. data/lib/samagotchi/home_notice.rb +8 -1
  28. data/lib/samagotchi/hooks/loader.rb +5 -0
  29. data/lib/samagotchi/log_line.rb +12 -0
  30. data/lib/samagotchi/project_env.rb +3 -11
  31. data/lib/samagotchi/self_report.rb +3 -1
  32. data/lib/samagotchi/system_one/self_protect.rb +58 -2
  33. data/lib/samagotchi/testing/waiting.rb +13 -0
  34. data/lib/samagotchi/toolkit_binding.rb +16 -0
  35. data/lib/samagotchi/update_command.rb +7 -3
  36. data/lib/samagotchi/update_hint.rb +6 -1
  37. data/lib/samagotchi/version.rb +1 -1
  38. data/lib/samagotchi/web/public/answer_fold.js +262 -0
  39. data/lib/samagotchi/web/public/answer_refs.js +2 -0
  40. data/lib/samagotchi/web/public/app.js +18 -1
  41. data/lib/samagotchi/web/public/cards_place.js +5 -2
  42. data/lib/samagotchi/web/public/chip_popover.js +73 -11
  43. data/lib/samagotchi/web/public/claim_link.js +4 -2
  44. data/lib/samagotchi/web/public/composer.css +3 -1
  45. data/lib/samagotchi/web/public/history_reveal.js +23 -0
  46. data/lib/samagotchi/web/public/index.html +44 -28
  47. data/lib/samagotchi/web/public/llm_context_chip.js +5 -13
  48. data/lib/samagotchi/web/public/refs.js +6 -7
  49. data/lib/samagotchi/web/public/review_start.js +1 -1
  50. data/lib/samagotchi/web/public/start_page.js +4 -4
  51. data/lib/samagotchi/web/public/start_settings.js +29 -18
  52. data/lib/samagotchi/web/public/start_toolkit.js +5 -4
  53. data/lib/samagotchi/web/public/timing.js +5 -1
  54. data/lib/samagotchi/web/public/turn_html.js +12 -6
  55. data/lib/samagotchi/web/public/turn_range.js +32 -0
  56. data/lib/samagotchi/web/public/turn_view.js +21 -11
  57. data/lib/samagotchi/web/public/verdict_card.js +5 -13
  58. data/lib/samagotchi/web/public/verify_target.js +7 -10
  59. metadata +5 -1
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Samagotchi
4
+ module Guardrails
5
+ # Programs that write outside the folder they run in with no word of
6
+ # the command naming where: a global install (npm -g, gem install, brew,
7
+ # cargo/go install, pipx, uv tool), a user config file (npm config set
8
+ # writes ~/.npmrc, gh config set ~/.config/gh, bundle config set), the
9
+ # system's defaults, a launchd job, a tool manager's global version, git
10
+ # maintenance's schedule and git lfs's global filters. A pair turn's "a
11
+ # write outside the project always asks" counts them as outside writes
12
+ # (PairCommand#outside_writes?). The table holds examples, not a closed
13
+ # set: a program it doesn't know runs unasked.
14
+ #
15
+ # pip counts with or without --user: it writes into its interpreter's
16
+ # site-packages, which no word names and which a plain `pip` (the
17
+ # worker's PATH, any VIRTUAL_ENV it inherited) may place anywhere. A pip
18
+ # or python named by a path into the project (.venv/bin/pip) installs
19
+ # into the project, and -t/--target/--prefix/--root name where it
20
+ # writes (their value is checked as any word): those don't count
21
+ # (PairCommand checks the program's path: LOCAL_INSTALLERS).
22
+ #
23
+ # Not counted: a project install (npm install, bundle install, uv sync)
24
+ # and the caches every build writes (~/.npm, ~/.cache, go's module
25
+ # cache): they hold downloads, not what runs as the owner.
26
+ module ImplicitWriters
27
+ NPM_CHANGES = %w[install i in add isntall uninstall un unlink remove rm r update up upgrade link ln].freeze
28
+ # program => the word groups of a call that writes outside: its
29
+ # arguments hold a word of every group of one entry (:global: npm's
30
+ # global flag in any spelling, .global?; [] alone: any call).
31
+ BY_VERB = {
32
+ "npm" => [[NPM_CHANGES, :global], [%w[link ln]], [%w[config c], %w[set delete rm edit fix]],
33
+ [%w[set login adduser add-user logout]]],
34
+ "yarn" => [[%w[global]], [%w[link]], [%w[config], %w[set delete unset]]],
35
+ "pnpm" => [[NPM_CHANGES, :global], [%w[config], %w[set delete]], [%w[setup]]],
36
+ "bun" => [[NPM_CHANGES, :global], [%w[link]]],
37
+ "pip" => [[%w[install uninstall]], [%w[config], %w[set unset edit]]],
38
+ "ensurepip" => [[]],
39
+ "easy_install" => [[]],
40
+ "pipenv" => [[%w[install uninstall sync update]]],
41
+ "poetry" => [[%w[self]]],
42
+ "conda" => [[%w[install remove uninstall update upgrade create]]],
43
+ "gem" => [[%w[install i update uninstall cleanup pristine]],
44
+ [%w[sources], %w[-a --add -r --remove -c --clear-all]]],
45
+ "bundle" => [[%w[install], %w[--system]]],
46
+ "defaults" => [[%w[write delete rename import]]],
47
+ "cargo" => [[%w[install uninstall]]],
48
+ "go" => [[%w[install]], [%w[env], %w[-w -u]]],
49
+ "uv" => [[%w[tool], %w[install uninstall upgrade update-shell]], [%w[python], %w[install uninstall]],
50
+ [%w[self], %w[update]], [%w[pip], %w[install uninstall sync], %w[--system]]],
51
+ "deno" => [[%w[install uninstall], :global]],
52
+ "dotnet" => [[%w[tool], %w[install update uninstall], %w[-g --global --tool-path]]],
53
+ "corepack" => [[%w[enable disable]], [%w[install], :global]],
54
+ "pyenv" => [[%w[global install uninstall rehash]]],
55
+ "rbenv" => [[%w[global install uninstall rehash]]],
56
+ "nodenv" => [[%w[global install uninstall rehash]]],
57
+ "asdf" => [[%w[install uninstall global plugin reshim]]],
58
+ "volta" => [[%w[install uninstall setup fetch]]],
59
+ "nvm" => [[%w[install uninstall alias unalias]]],
60
+ "code" => [[%w[--install-extension --uninstall-extension]]],
61
+ "gh" => [[%w[auth], %w[login logout refresh setup-git switch]], [%w[config], %w[set]],
62
+ [%w[extension extensions ext], %w[install upgrade remove]], [%w[alias], %w[set delete import]]],
63
+ # the global config and a launchd/cron schedule; git lfs's global filters
64
+ "git" => [[%w[maintenance], %w[register start unregister stop]], [%w[lfs], %w[install uninstall]]],
65
+ "git-maintenance" => [[%w[register start unregister stop]]],
66
+ "git-lfs" => [[%w[install uninstall]]],
67
+ "scalar" => [[%w[register unregister clone reconfigure delete]]]
68
+ }.freeze
69
+ # program => the verbs (its first argument) that only read: every
70
+ # other one writes outside.
71
+ READ_VERBS = {
72
+ "brew" => %w[list ls info search desc deps uses outdated leaves config doctor --prefix --cellar --repository
73
+ --version -v --help -h help],
74
+ "pipx" => %w[list environment --version --help -h],
75
+ "launchctl" => %w[list print print-cache print-disabled blame procinfo hostinfo examine version help],
76
+ "rustup" => %w[show which --version -V help --help -h completions]
77
+ }.freeze
78
+ # Installers whose program named by a path into the project (a
79
+ # project venv's pip) install there, and the options that say where.
80
+ LOCAL_INSTALLERS = %w[pip ensurepip easy_install].freeze
81
+ PLACE_OPTIONS = /\A(?:-t|--target|--prefix|--root)(?:=|\z)/
82
+ # pip3, pip3.12 → pip.
83
+ ALIASES = { /\Apip\d*(?:\.\d+)?\z/ => "pip" }.freeze
84
+ # npm's global flag: -g, --global, --global=true, --location=global
85
+ # (and --location global, two words: .global?).
86
+ GLOBAL = /\A(?:-g|--global|--global=(?!false\z).*|--location=global)\z/
87
+ # npm reads its config from the environment too.
88
+ NPM_ENV_GLOBAL = /\Anpm_config_(?:global|location)\z/i
89
+
90
+ module_function
91
+
92
+ # The table's name for +program+ (a word's basename).
93
+ def name(program)
94
+ base = File.basename(program.to_s)
95
+ ALIASES.each { |pattern, to| return to if base.match?(pattern) }
96
+ base
97
+ end
98
+
99
+ # Whether +program+ (its word) run with +args+ writes outside the
100
+ # folder it runs in with no word naming where. +local+ says the
101
+ # program's word is a path into the project (LOCAL_INSTALLERS);
102
+ # +assigned+ the variables the command sets before it.
103
+ def outside?(program, args, local: false, assigned: {})
104
+ program = name(program)
105
+ if (verbs = READ_VERBS[program])
106
+ return !(args.empty? || verbs.include?(args.first))
107
+ end
108
+ return bundle_config_outside?(args) if program == "bundle" && args.first == "config"
109
+
110
+ entries = BY_VERB[program] or return false
111
+ return false unless entries.any? { |groups| groups.all? { |group| group?(group, args, assigned) } }
112
+ return true unless LOCAL_INSTALLERS.include?(program)
113
+ return true if args.include?("--user")
114
+
115
+ !(local || args.any? { |arg| arg.match?(PLACE_OPTIONS) })
116
+ end
117
+
118
+ def group?(group, args, assigned)
119
+ group == :global ? global?(args, assigned) : args.intersect?(group)
120
+ end
121
+
122
+ # npm's (pnpm's, bun's, deno's, corepack's) global flag, in any of
123
+ # its spellings or the environment's (npm_config_global=true).
124
+ def global?(args, assigned = {})
125
+ args.any? { |arg| arg.match?(GLOBAL) } || args.each_cons(2).include?(%w[--location global]) ||
126
+ assigned.any? { |key, value| key.match?(NPM_ENV_GLOBAL) && !%w[false local project].include?(value.to_s) }
127
+ end
128
+
129
+ # bundle config set/unset writes ~/.bundle/config unless --local, and
130
+ # so does the old form `bundle config NAME VALUE`; a list or a get
131
+ # (or a NAME alone) only reads.
132
+ def bundle_config_outside?(args)
133
+ rest = args.drop(1)
134
+ return false if rest.include?("--local")
135
+
136
+ words = rest.reject { |arg| arg.start_with?("-") }
137
+ return false if words.empty? || %w[list get].include?(words.first)
138
+
139
+ %w[set unset].include?(words.first) || words.size > 1
140
+ end
141
+ end
142
+ end
143
+ end
@@ -28,7 +28,7 @@ module Samagotchi
28
28
  # loopback name, URL openers (open, xdg-open, a browser) and
29
29
  # listeners (nc -l, socat …LISTEN, a static HTTP server), background
30
30
  # and scheduled jobs, the environment and
31
- # credential printers, git settings that run commands, home sweeps
31
+ # credential printers, git config writes (reads only), home sweeps
32
32
  # and links to them, and a program named by a $var or a glob); a
33
33
  # write or edit of git's hooks (GIT_HOOKS);
34
34
  # - owner-voiced tools (OWNER_VOICED, and any delegate_* tool): what
@@ -77,13 +77,15 @@ module Samagotchi
77
77
  HOME_SWEEP = "a participant's turn can't search, copy or link a folder that holds secrets or chi's own files"
78
78
  LINK = "a participant's turn makes a link: a path through it can lead out of the project, so the owner " \
79
79
  "approves it"
80
- GIT_CONFIG = "a participant's turn can't change git's global config or set what makes git run a command " \
81
- "(core.hooksPath, core.fsmonitor, core.sshCommand, core.pager, core.editor, ! aliases)"
80
+ GIT_CONFIG = "a participant's turn writes no git config (git config but its reads, git -c, --config-env, a " \
81
+ "clone's or init's -c/--config, GIT_CONFIG_* and GIT_TEMPLATE_DIR): settings can make git run a " \
82
+ "command in the owner's name"
82
83
  ENV_LOADER = "a participant's turn runs mise or direnv: they load the project's env files (a mise.local.toml, " \
83
84
  "an .envrc), which may hold the owner's secrets, so the owner approves it"
84
85
  MISE_CONFIG = "a participant's turn changes mise's config: the owner's next worker start runs it (its [env] " \
85
86
  "can run commands), so the owner approves it"
86
- GIT_HOOKS = "a participant's turn can't write git's hooks (.git/hooks): git runs them in the owner's name"
87
+ GIT_HOOKS = "a participant's turn can't write or install git's hooks (.git/hooks, .git/modules/*/hooks, a " \
88
+ "template, pre-commit install, husky): git runs them in the owner's name"
87
89
  # chi web on loopback asks the owner's token too (Web::App#token_gate,
88
90
  # LAN mode); this text check stays as defense in depth: a loopback-only
89
91
  # chi web still answers loopback as the owner.