samagotchi 0.133.0 → 0.135.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -1
  3. data/bin/chi +2 -3
  4. data/docs/architecture.md +6 -3
  5. data/docs/cli.md +50 -34
  6. data/docs/configuration.md +9 -6
  7. data/docs/guardrails.md +146 -13
  8. data/docs/internals/background-tasks.md +1 -0
  9. data/docs/internals/engine.md +6 -3
  10. data/docs/internals/participants.md +7 -6
  11. data/docs/internals/tool-guardrails.md +2 -0
  12. data/docs/internals/web-frontend.md +262 -0
  13. data/docs/plugins.md +2 -1
  14. data/docs/sessions.md +5 -2
  15. data/docs/testing.md +34 -4
  16. data/lib/samagotchi/bridge.rb +11 -6
  17. data/lib/samagotchi/bridge_client.rb +8 -5
  18. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  19. data/lib/samagotchi/chi_home.rb +12 -2
  20. data/lib/samagotchi/config.rb +0 -2
  21. data/lib/samagotchi/desktop/macos.rb +19 -4
  22. data/lib/samagotchi/engine.rb +82 -12
  23. data/lib/samagotchi/guardrail_wiring.rb +8 -6
  24. data/lib/samagotchi/guardrails/approval.rb +41 -5
  25. data/lib/samagotchi/guardrails/implicit_writers.rb +158 -0
  26. data/lib/samagotchi/guardrails/pair_boundary.rb +11 -5
  27. data/lib/samagotchi/guardrails/pair_command.rb +828 -86
  28. data/lib/samagotchi/guardrails/parent_approvals.rb +13 -16
  29. data/lib/samagotchi/guardrails/read_only_shell.rb +58 -4
  30. data/lib/samagotchi/guardrails/shared_git.rb +1 -1
  31. data/lib/samagotchi/guardrails/shell_git_dirs.rb +7 -2
  32. data/lib/samagotchi/guardrails/shell_lex.rb +45 -0
  33. data/lib/samagotchi/guardrails/shell_paths.rb +3 -2
  34. data/lib/samagotchi/guardrails/targets.rb +8 -3
  35. data/lib/samagotchi/guardrails_command.rb +13 -10
  36. data/lib/samagotchi/home_command.rb +22 -6
  37. data/lib/samagotchi/home_doctor.rb +36 -3
  38. data/lib/samagotchi/home_notice.rb +8 -1
  39. data/lib/samagotchi/hooks/loader.rb +5 -0
  40. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  41. data/lib/samagotchi/log_line.rb +12 -0
  42. data/lib/samagotchi/message_parts.rb +1 -1
  43. data/lib/samagotchi/project_env.rb +3 -11
  44. data/lib/samagotchi/revise_anchor.rb +33 -0
  45. data/lib/samagotchi/self_report.rb +3 -1
  46. data/lib/samagotchi/session_inbox.rb +20 -12
  47. data/lib/samagotchi/session_manager.rb +13 -11
  48. data/lib/samagotchi/system_one/approval_question.rb +21 -16
  49. data/lib/samagotchi/system_one/self_protect.rb +70 -16
  50. data/lib/samagotchi/testing/waiting.rb +13 -0
  51. data/lib/samagotchi/toolkit_binding.rb +16 -0
  52. data/lib/samagotchi/tools/delegate_relay.rb +6 -5
  53. data/lib/samagotchi/tools/delegate_wait.rb +5 -5
  54. data/lib/samagotchi/tools/execute.rb +6 -6
  55. data/lib/samagotchi/tools/output_guardrails.rb +9 -2
  56. data/lib/samagotchi/tools/read.rb +5 -5
  57. data/lib/samagotchi/turn_anchors.rb +49 -0
  58. data/lib/samagotchi/update_command.rb +7 -3
  59. data/lib/samagotchi/update_hint.rb +6 -1
  60. data/lib/samagotchi/verify_anchor.rb +5 -5
  61. data/lib/samagotchi/version.rb +1 -1
  62. data/lib/samagotchi/web/app.rb +2 -14
  63. data/lib/samagotchi/web/message_display.rb +17 -1
  64. data/lib/samagotchi/web/participant_access.rb +2 -2
  65. data/lib/samagotchi/web/participant_view.rb +8 -0
  66. data/lib/samagotchi/web/public/activity.js +1 -1
  67. data/lib/samagotchi/web/public/annotations.js +1 -1
  68. data/lib/samagotchi/web/public/answer_fold.js +262 -0
  69. data/lib/samagotchi/web/public/answer_refs.js +2 -0
  70. data/lib/samagotchi/web/public/app.js +462 -338
  71. data/lib/samagotchi/web/public/bubbles.js +3 -3
  72. data/lib/samagotchi/web/public/cards_place.js +9 -4
  73. data/lib/samagotchi/web/public/chip_popover.js +73 -11
  74. data/lib/samagotchi/web/public/claim_link.js +4 -2
  75. data/lib/samagotchi/web/public/composer.css +11 -1
  76. data/lib/samagotchi/web/public/context_chips.js +9 -6
  77. data/lib/samagotchi/web/public/data.js +4 -2
  78. data/lib/samagotchi/web/public/dock_reserve.js +13 -15
  79. data/lib/samagotchi/web/public/feature_gates.js +33 -0
  80. data/lib/samagotchi/web/public/history_reveal.js +27 -0
  81. data/lib/samagotchi/web/public/index.html +82 -37
  82. data/lib/samagotchi/web/public/latest.js +46 -0
  83. data/lib/samagotchi/web/public/lists_view.js +15 -13
  84. data/lib/samagotchi/web/public/live_timing.js +2 -0
  85. data/lib/samagotchi/web/public/llm_context_chip.js +5 -13
  86. data/lib/samagotchi/web/public/markdown_place.js +1 -1
  87. data/lib/samagotchi/web/public/method_api.js +14 -0
  88. data/lib/samagotchi/web/public/notifications.js +4 -11
  89. data/lib/samagotchi/web/public/participant.js +11 -6
  90. data/lib/samagotchi/web/public/participant_names.js +1 -1
  91. data/lib/samagotchi/web/public/participants_panel.js +4 -2
  92. data/lib/samagotchi/web/public/project_picker.js +18 -22
  93. data/lib/samagotchi/web/public/question_cards.js +2 -2
  94. data/lib/samagotchi/web/public/refs.js +6 -7
  95. data/lib/samagotchi/web/public/restore.js +2 -0
  96. data/lib/samagotchi/web/public/review_start.js +1 -1
  97. data/lib/samagotchi/web/public/revise_stack.js +372 -0
  98. data/lib/samagotchi/web/public/scope.js +12 -20
  99. data/lib/samagotchi/web/public/send_turn.js +29 -16
  100. data/lib/samagotchi/web/public/session_actions.js +15 -14
  101. data/lib/samagotchi/web/public/session_fields.js +152 -0
  102. data/lib/samagotchi/web/public/session_payload.js +35 -0
  103. data/lib/samagotchi/web/public/session_state.js +2 -0
  104. data/lib/samagotchi/web/public/session_store.js +191 -0
  105. data/lib/samagotchi/web/public/stage_model.js +4 -4
  106. data/lib/samagotchi/web/public/stage_view.js +11 -16
  107. data/lib/samagotchi/web/public/start_page.js +4 -4
  108. data/lib/samagotchi/web/public/start_settings.js +29 -18
  109. data/lib/samagotchi/web/public/start_toolkit.js +5 -4
  110. data/lib/samagotchi/web/public/stream_handlers.js +88 -90
  111. data/lib/samagotchi/web/public/task_stop.js +1 -1
  112. data/lib/samagotchi/web/public/timing.js +7 -3
  113. data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +46 -101
  114. data/lib/samagotchi/web/public/turn_events.js +2 -2
  115. data/lib/samagotchi/web/public/turn_html.js +17 -9
  116. data/lib/samagotchi/web/public/turn_model.js +6 -6
  117. data/lib/samagotchi/web/public/turn_range.js +32 -0
  118. data/lib/samagotchi/web/public/verdict_card.js +5 -13
  119. data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
  120. data/lib/samagotchi/web/public/verify_target.js +7 -10
  121. data/lib/samagotchi/web/public/working_set_view.js +3 -1
  122. data/lib/samagotchi/web/server.rb +4 -4
  123. data/lib/samagotchi/web/session_control.rb +2 -2
  124. data/lib/samagotchi/worker.rb +9 -9
  125. data/lib/samagotchi/worker_inbound.rb +6 -5
  126. metadata +16 -3
  127. data/lib/samagotchi/web/public/hold.js +0 -78
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shell_lex"
4
+
5
+ module Samagotchi
6
+ module Guardrails
7
+ # Programs that write outside the folder they run in with no word of
8
+ # the command naming where: a global install (npm -g, gem install, brew,
9
+ # cargo/go install, pipx, uv tool), a user config file (npm config set
10
+ # writes ~/.npmrc, gh config set ~/.config/gh, bundle config set), the
11
+ # system's defaults, a launchd job, a tool manager's global version, git
12
+ # maintenance's schedule and git lfs's global filters. A pair turn's "a
13
+ # write outside the project always asks" counts them as outside writes
14
+ # (PairCommand#outside_writes?). The table holds examples, not a closed
15
+ # set: a program it doesn't know runs unasked.
16
+ #
17
+ # pip counts with or without --user: it writes into its interpreter's
18
+ # site-packages, which no word names and which a plain `pip` (the
19
+ # worker's PATH, any VIRTUAL_ENV it inherited) may place anywhere. A pip
20
+ # or python named by a path into the project (.venv/bin/pip) installs
21
+ # into the project, and -t/--target/--prefix/--root name where it
22
+ # writes (their value is checked as any word): those don't count
23
+ # (PairCommand checks the program's path: LOCAL_INSTALLERS).
24
+ #
25
+ # Not counted: a project install (npm install, bundle install, uv sync)
26
+ # and the caches every build writes (~/.npm, ~/.cache, go's module
27
+ # cache): they hold downloads, not what runs as the owner.
28
+ module ImplicitWriters
29
+ NPM_CHANGES = %w[install i in add isntall uninstall un unlink remove rm r update up upgrade link ln].freeze
30
+ # program => the word groups of a call that writes outside: its
31
+ # arguments hold a word of every group of one entry (:global: npm's
32
+ # global flag in any spelling, .global?; [] alone: any call).
33
+ BY_VERB = {
34
+ "npm" => [[NPM_CHANGES, :global], [%w[link ln]], [%w[config c], %w[set delete rm edit fix]],
35
+ [%w[set login adduser add-user logout]]],
36
+ "yarn" => [[%w[global]], [%w[link]], [%w[config], %w[set delete unset]]],
37
+ "pnpm" => [[NPM_CHANGES, :global], [%w[config], %w[set delete]], [%w[setup]]],
38
+ "bun" => [[NPM_CHANGES, :global], [%w[link]]],
39
+ "pip" => [[%w[install uninstall]], [%w[config], %w[set unset edit]]],
40
+ "ensurepip" => [[]],
41
+ "easy_install" => [[]],
42
+ "pipenv" => [[%w[install uninstall sync update]]],
43
+ "poetry" => [[%w[self]]],
44
+ "conda" => [[%w[install remove uninstall update upgrade create]]],
45
+ "gem" => [[%w[install i update uninstall cleanup pristine]],
46
+ [%w[sources], %w[-a --add -r --remove -c --clear-all]]],
47
+ "bundle" => [[%w[install], %w[--system]]],
48
+ "defaults" => [[%w[write delete rename import]]],
49
+ "cargo" => [[%w[install uninstall]]],
50
+ "go" => [[%w[install]], [%w[env], %w[-w -u]]],
51
+ "uv" => [[%w[tool], %w[install uninstall upgrade update-shell]], [%w[python], %w[install uninstall]],
52
+ [%w[self], %w[update]], [%w[pip], %w[install uninstall sync], %w[--system]]],
53
+ "deno" => [[%w[install uninstall], :global]],
54
+ "dotnet" => [[%w[tool], %w[install update uninstall], %w[-g --global --tool-path]]],
55
+ "corepack" => [[%w[enable disable]], [%w[install], :global]],
56
+ "pyenv" => [[%w[global install uninstall rehash]]],
57
+ "rbenv" => [[%w[global install uninstall rehash]]],
58
+ "nodenv" => [[%w[global install uninstall rehash]]],
59
+ "asdf" => [[%w[install uninstall global plugin reshim]]],
60
+ "volta" => [[%w[install uninstall setup fetch]]],
61
+ "nvm" => [[%w[install uninstall alias unalias]]],
62
+ "code" => [[%w[--install-extension --uninstall-extension]]],
63
+ "gh" => [[%w[auth], %w[login logout refresh setup-git switch]], [%w[config], %w[set]],
64
+ [%w[extension extensions ext], %w[install upgrade remove]], [%w[alias], %w[set delete import]]],
65
+ # the global config and a launchd/cron schedule; git lfs's global filters
66
+ # (git's first group is its subcommand: .git_outside?)
67
+ "git" => [[%w[maintenance], %w[register start unregister stop]], [%w[lfs], %w[install uninstall update]]],
68
+ "git-maintenance" => [[%w[register start unregister stop]]],
69
+ "git-lfs" => [[%w[install uninstall update]]],
70
+ "scalar" => [[%w[register unregister clone reconfigure delete]]]
71
+ }.freeze
72
+ # program => the verbs (its first argument) that only read: every
73
+ # other one writes outside.
74
+ READ_VERBS = {
75
+ "brew" => %w[list ls info search desc deps uses outdated leaves config doctor --prefix --cellar --repository
76
+ --version -v --help -h help],
77
+ "pipx" => %w[list environment --version --help -h],
78
+ "launchctl" => %w[list print print-cache print-disabled blame procinfo hostinfo examine version help],
79
+ "rustup" => %w[show which --version -V help --help -h completions]
80
+ }.freeze
81
+ # Installers whose program named by a path into the project (a
82
+ # project venv's pip) install there, and the options that say where.
83
+ LOCAL_INSTALLERS = %w[pip ensurepip easy_install].freeze
84
+ PLACE_OPTIONS = /\A(?:-t|--target|--prefix|--root)(?:=|\z)/
85
+ # pip3, pip3.12 → pip.
86
+ ALIASES = { /\Apip\d*(?:\.\d+)?\z/ => "pip" }.freeze
87
+ # npm's global flag: -g, --global, --global=true, --location=global
88
+ # (and --location global, two words: .global?).
89
+ GLOBAL = /\A(?:-g|--global|--global=(?!false\z).*|--location=global)\z/
90
+ # npm reads its config from the environment too.
91
+ NPM_ENV_GLOBAL = /\Anpm_config_(?:global|location)\z/i
92
+
93
+ module_function
94
+
95
+ # The table's name for +program+ (its word's ShellLex.program_name).
96
+ def name(program)
97
+ base = ShellLex.program_name(program)
98
+ ALIASES.each { |pattern, to| return to if base.match?(pattern) }
99
+ base
100
+ end
101
+
102
+ # Whether +program+ (its word) run with +args+ writes outside the
103
+ # folder it runs in with no word naming where. +local+ says the
104
+ # program's word is a path into the project (LOCAL_INSTALLERS);
105
+ # +assigned+ the variables the command sets before it.
106
+ def outside?(program, args, local: false, assigned: {})
107
+ program = name(program)
108
+ if (verbs = READ_VERBS[program])
109
+ return !(args.empty? || verbs.include?(args.first))
110
+ end
111
+ return bundle_config_outside?(args) if program == "bundle" && args.first == "config"
112
+
113
+ entries = BY_VERB[program] or return false
114
+ return git_outside?(entries, args, assigned) if program == "git"
115
+ return false unless entries.any? { |groups| groups.all? { |group| group?(group, args, assigned) } }
116
+ return true unless LOCAL_INSTALLERS.include?(program)
117
+ return true if args.include?("--user")
118
+
119
+ !(local || args.any? { |arg| arg.match?(PLACE_OPTIONS) })
120
+ end
121
+
122
+ # git's entries match from its subcommand on (PairCommand.git_command_starts:
123
+ # git -C . maintenance start), the first group holding the subcommand,
124
+ # not its words anywhere (git add maintenance start, git commit -m lfs -m install).
125
+ def git_outside?(entries, args, assigned)
126
+ PairCommand.git_command_starts(args).grep(Integer).any? do |i|
127
+ entries.any? do |first, *rest|
128
+ first.include?(args[i]) && rest.all? { |group| group?(group, args[(i + 1)..], assigned) }
129
+ end
130
+ end
131
+ end
132
+
133
+ def group?(group, args, assigned)
134
+ group == :global ? global?(args, assigned) : args.intersect?(group)
135
+ end
136
+
137
+ # npm's (pnpm's, bun's, deno's, corepack's) global flag, in any of
138
+ # its spellings or the environment's (npm_config_global=true).
139
+ def global?(args, assigned = {})
140
+ args.any? { |arg| arg.match?(GLOBAL) } || args.each_cons(2).include?(%w[--location global]) ||
141
+ assigned.any? { |key, value| key.match?(NPM_ENV_GLOBAL) && !%w[false local project].include?(value.to_s) }
142
+ end
143
+
144
+ # bundle config set/unset writes ~/.bundle/config unless --local, and
145
+ # so does the old form `bundle config NAME VALUE`; a list or a get
146
+ # (or a NAME alone) only reads.
147
+ def bundle_config_outside?(args)
148
+ rest = args.drop(1)
149
+ return false if rest.include?("--local")
150
+
151
+ words = rest.reject { |arg| arg.start_with?("-") }
152
+ return false if words.empty? || %w[list get].include?(words.first)
153
+
154
+ %w[set unset].include?(words.first) || words.size > 1
155
+ end
156
+ end
157
+ end
158
+ 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.
@@ -95,6 +97,8 @@ module Samagotchi
95
97
  "owner's cookies"
96
98
  LISTENER = "a participant's turn can't listen on a port: what it serves on this machine could catch the " \
97
99
  "owner's browser cookies"
100
+ NET = "a participant's turn runs a command chi can't read for sure (git or chi behind a program it doesn't " \
101
+ "know, or a $(…) kept in a value the shell expands later), so the owner approves it"
98
102
  VERB = "a participant's turn runs only a program named plainly: no $, $(…) or glob in a command's name"
99
103
  UNREADABLE = "a participant's turn runs only commands chi can read: split this command, or write the file " \
100
104
  "with the write tool"
@@ -193,13 +197,15 @@ module Samagotchi
193
197
 
194
198
  # Why the owner is asked (fresh, once): a command that makes a link
195
199
  # (LINK), one that runs mise or direnv (ENV_LOADER), a write to mise's
196
- # config (MISE_CONFIG), a write that may land outside the project (OUTSIDE_WRITE, or
200
+ # config (MISE_CONFIG), a command chi can't read for sure (NET:
201
+ # PairCommand#net_ask?), a write that may land outside the project (OUTSIDE_WRITE, or
197
202
  # NO_PROJECT without one); nil when nothing asks.
198
203
  def ask_reason(verdict)
199
204
  targets = verdict.targets
200
205
  return LINK if targets&.command && command(targets, nil).links?
201
206
  return ENV_LOADER if targets&.command && command(targets, nil).loads_env?
202
207
  return MISE_CONFIG if mise_config_write?(verdict)
208
+ return NET if targets&.command && command(targets, nil).net_ask?
203
209
  return nil unless outside_write?(verdict)
204
210
 
205
211
  project_root ? OUTSIDE_WRITE : NO_PROJECT