samagotchi 0.156.0 → 0.158.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -1
  3. data/README.md +1 -1
  4. data/bin/chi +4 -1
  5. data/docs/architecture.md +18 -12
  6. data/docs/bundle-authoring.md +5 -5
  7. data/docs/bundles.md +4 -1
  8. data/docs/cli.md +29 -24
  9. data/docs/configuration.md +43 -9
  10. data/docs/context.md +5 -3
  11. data/docs/guardrails.md +12 -7
  12. data/docs/hooks.md +53 -83
  13. data/docs/internals/bridge.md +84 -7
  14. data/docs/internals/bundles.md +69 -46
  15. data/docs/internals/context-telemetry.md +4 -3
  16. data/docs/internals/engine.md +23 -7
  17. data/docs/internals/participants.md +3 -2
  18. data/docs/internals/remote-sessions.md +5 -2
  19. data/docs/internals/web-frontend.md +3 -2
  20. data/docs/internals/working-set.md +1 -1
  21. data/docs/memory.md +51 -10
  22. data/docs/plugins.md +48 -11
  23. data/docs/releasing.md +6 -5
  24. data/docs/sessions.md +13 -13
  25. data/docs/sub-agent.md +17 -2
  26. data/docs/testing.md +56 -5
  27. data/lib/samagotchi/answer_command.rb +2 -1
  28. data/lib/samagotchi/archive_store.rb +4 -3
  29. data/lib/samagotchi/bootstrap_command.rb +3 -2
  30. data/lib/samagotchi/bridge/card_places.rb +5 -7
  31. data/lib/samagotchi/bridge/card_store.rb +3 -0
  32. data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
  33. data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
  34. data/lib/samagotchi/bridge.rb +31 -6
  35. data/lib/samagotchi/broadcast/recipients.rb +8 -8
  36. data/lib/samagotchi/broadcast/triage_model.rb +1 -1
  37. data/lib/samagotchi/bundle_command/check.rb +41 -10
  38. data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
  39. data/lib/samagotchi/bundle_command/try/run.rb +4 -1
  40. data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
  41. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  42. data/lib/samagotchi/bundle_command.rb +20 -45
  43. data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
  44. data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
  45. data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
  46. data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
  48. data/lib/samagotchi/bundles/system/manifest.yml +4 -4
  49. data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
  50. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  51. data/lib/samagotchi/child_move.rb +6 -6
  52. data/lib/samagotchi/child_reports.rb +2 -2
  53. data/lib/samagotchi/children_status.rb +15 -21
  54. data/lib/samagotchi/cli/parent_wait.rb +8 -22
  55. data/lib/samagotchi/config.rb +3 -0
  56. data/lib/samagotchi/context_command.rb +7 -5
  57. data/lib/samagotchi/context_sources.rb +9 -6
  58. data/lib/samagotchi/context_window.rb +9 -12
  59. data/lib/samagotchi/effective_model.rb +57 -22
  60. data/lib/samagotchi/engine/plugins.rb +1 -1
  61. data/lib/samagotchi/engine/recap.rb +8 -4
  62. data/lib/samagotchi/engine.rb +30 -9
  63. data/lib/samagotchi/events.rb +3 -2
  64. data/lib/samagotchi/extension_load.rb +26 -44
  65. data/lib/samagotchi/guardrails/load_failures.rb +3 -2
  66. data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
  67. data/lib/samagotchi/guardrails/review_paths.rb +1 -5
  68. data/lib/samagotchi/guardrails_command.rb +1 -1
  69. data/lib/samagotchi/hook_routes.rb +2 -2
  70. data/lib/samagotchi/hooks.rb +2 -3
  71. data/lib/samagotchi/host_address.rb +52 -0
  72. data/lib/samagotchi/host_registry.rb +57 -45
  73. data/lib/samagotchi/kernel_loop.rb +7 -3
  74. data/lib/samagotchi/llm/chat_loop.rb +6 -3
  75. data/lib/samagotchi/llm_context_strategy.rb +27 -63
  76. data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
  77. data/lib/samagotchi/memory_bundle/builder.rb +12 -35
  78. data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
  79. data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
  80. data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
  81. data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
  82. data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
  83. data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
  84. data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
  85. data/lib/samagotchi/memory_bundle/installer.rb +212 -178
  86. data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
  87. data/lib/samagotchi/memory_bundle/merger.rb +69 -65
  88. data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
  89. data/lib/samagotchi/memory_bundle/profile.rb +5 -5
  90. data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
  91. data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
  92. data/lib/samagotchi/memory_bundle/shas.rb +5 -8
  93. data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
  94. data/lib/samagotchi/memory_bundle/status.rb +26 -30
  95. data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
  96. data/lib/samagotchi/memory_bundle/trash.rb +28 -18
  97. data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
  98. data/lib/samagotchi/memory_orphans.rb +4 -4
  99. data/lib/samagotchi/memory_paths.rb +102 -13
  100. data/lib/samagotchi/memory_usage.rb +1 -1
  101. data/lib/samagotchi/model_lookup.rb +75 -0
  102. data/lib/samagotchi/model_profile.rb +1 -0
  103. data/lib/samagotchi/models_command.rb +1 -1
  104. data/lib/samagotchi/note_command.rb +1 -1
  105. data/lib/samagotchi/participant_mode.rb +2 -1
  106. data/lib/samagotchi/plugin/api.rb +42 -10
  107. data/lib/samagotchi/plugin/loader.rb +75 -15
  108. data/lib/samagotchi/plugin/sessions.rb +1 -1
  109. data/lib/samagotchi/project_command.rb +167 -0
  110. data/lib/samagotchi/project_init.rb +69 -0
  111. data/lib/samagotchi/project_scope.rb +6 -4
  112. data/lib/samagotchi/prompt_files.rb +10 -7
  113. data/lib/samagotchi/reasoning_history.rb +14 -20
  114. data/lib/samagotchi/recap_place.rb +26 -0
  115. data/lib/samagotchi/reply_wait/liveness.rb +38 -0
  116. data/lib/samagotchi/reply_wait/poll.rb +44 -15
  117. data/lib/samagotchi/reply_wait/target.rb +113 -0
  118. data/lib/samagotchi/reply_wait.rb +22 -2
  119. data/lib/samagotchi/run_options.rb +1 -0
  120. data/lib/samagotchi/sampling_settings.rb +9 -16
  121. data/lib/samagotchi/self_report.rb +34 -35
  122. data/lib/samagotchi/send_command.rb +17 -23
  123. data/lib/samagotchi/session.rb +349 -441
  124. data/lib/samagotchi/session_access/contract.rb +1 -1
  125. data/lib/samagotchi/session_access/local.rb +5 -6
  126. data/lib/samagotchi/session_access/remote.rb +5 -3
  127. data/lib/samagotchi/session_field.rb +134 -0
  128. data/lib/samagotchi/session_inbox.rb +49 -1
  129. data/lib/samagotchi/session_manager/emptiness.rb +2 -1
  130. data/lib/samagotchi/session_manager/listing.rb +29 -100
  131. data/lib/samagotchi/session_manager.rb +2 -1
  132. data/lib/samagotchi/session_metrics.rb +7 -4
  133. data/lib/samagotchi/session_paths.rb +57 -0
  134. data/lib/samagotchi/session_retention.rb +2 -2
  135. data/lib/samagotchi/session_row.rb +200 -0
  136. data/lib/samagotchi/session_setup.rb +15 -12
  137. data/lib/samagotchi/sessions_command.rb +11 -129
  138. data/lib/samagotchi/steer.rb +10 -0
  139. data/lib/samagotchi/steer_cut_after.rb +7 -14
  140. data/lib/samagotchi/system_prompt.rb +20 -11
  141. data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
  142. data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
  143. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  144. data/lib/samagotchi/terminal_ui.rb +3 -1
  145. data/lib/samagotchi/testing/bundle.rb +5 -12
  146. data/lib/samagotchi/testing/install.rb +5 -4
  147. data/lib/samagotchi/testing/recorder.rb +1 -1
  148. data/lib/samagotchi/testing.rb +1 -1
  149. data/lib/samagotchi/thinking.rb +16 -21
  150. data/lib/samagotchi/tool_declarations.rb +1 -1
  151. data/lib/samagotchi/tool_response.rb +5 -3
  152. data/lib/samagotchi/tool_runner.rb +18 -1
  153. data/lib/samagotchi/toolkit_binding.rb +1 -4
  154. data/lib/samagotchi/toolkit_command.rb +1 -4
  155. data/lib/samagotchi/tools/delegate.rb +60 -51
  156. data/lib/samagotchi/tools/delegate_relay.rb +5 -5
  157. data/lib/samagotchi/tools/delegate_result.rb +1 -1
  158. data/lib/samagotchi/tools/delegate_wait.rb +41 -17
  159. data/lib/samagotchi/tools/list_sessions.rb +7 -7
  160. data/lib/samagotchi/update_command.rb +11 -7
  161. data/lib/samagotchi/verify_fresh.rb +7 -11
  162. data/lib/samagotchi/version.rb +1 -1
  163. data/lib/samagotchi/vision_support.rb +11 -14
  164. data/lib/samagotchi/web/app.rb +66 -21
  165. data/lib/samagotchi/web/message_display.rb +57 -7
  166. data/lib/samagotchi/web/participant_access.rb +1 -1
  167. data/lib/samagotchi/web/project_list.rb +3 -2
  168. data/lib/samagotchi/web/public/activity.js +16 -7
  169. data/lib/samagotchi/web/public/app.js +29 -8
  170. data/lib/samagotchi/web/public/bubbles.js +8 -3
  171. data/lib/samagotchi/web/public/cards_place.js +15 -0
  172. data/lib/samagotchi/web/public/commands.js +5 -1
  173. data/lib/samagotchi/web/public/data.js +4 -2
  174. data/lib/samagotchi/web/public/history_shape.js +220 -0
  175. data/lib/samagotchi/web/public/index.html +6 -1
  176. data/lib/samagotchi/web/public/project_picker.js +86 -4
  177. data/lib/samagotchi/web/public/recap_stamp.js +17 -0
  178. data/lib/samagotchi/web/public/reload_check.js +71 -0
  179. data/lib/samagotchi/web/public/stage_model.js +7 -5
  180. data/lib/samagotchi/web/public/stage_view.js +7 -3
  181. data/lib/samagotchi/web/public/stream_handlers.js +8 -2
  182. data/lib/samagotchi/web/public/timing.js +43 -2
  183. data/lib/samagotchi/web/public/turn_block.js +215 -30
  184. data/lib/samagotchi/web/public/turn_drawing.js +43 -0
  185. data/lib/samagotchi/web/public/turn_events.js +10 -1
  186. data/lib/samagotchi/web/public/turn_pieces.js +29 -0
  187. data/lib/samagotchi/web/reload_check_route.rb +95 -0
  188. data/lib/samagotchi/web/server.rb +4 -2
  189. data/lib/samagotchi/web/session_access_routes.rb +3 -3
  190. data/lib/samagotchi/web/session_control.rb +1 -1
  191. data/lib/samagotchi/web/session_hub.rb +9 -17
  192. data/lib/samagotchi/web/session_show.rb +64 -2
  193. data/lib/samagotchi/web/session_summary.rb +36 -24
  194. data/lib/samagotchi/worker.rb +28 -5
  195. data/lib/samagotchi/worker_link.rb +3 -0
  196. metadata +22 -5
  197. data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
  198. data/lib/samagotchi/send_command/wait_target.rb +0 -85
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "fileutils"
4
4
  require_relative "file_digest"
5
- require_relative "bundle_hook"
6
5
  require_relative "manifest"
7
6
  require_relative "provenance"
8
7
  require_relative "../version"
@@ -13,22 +12,25 @@ module Samagotchi
13
12
 
14
13
  # The install of a bundle's parts that live in its own dir
15
14
  # (<bundles>/<name>/, Provenance#bundle_dir) rather than in the
16
- # memories dir: hooks/*.rb, guardrails/*.yml, the plugin file and
17
- # scripts/*. Each replaces what an earlier version installed. Installer
15
+ # memories dir: guardrails/*.yml, the plugin file and scripts/*. Each
16
+ # replaces what an earlier version installed. An older chi's record
17
+ # with class-file hooks has them removed (#retire_hooks): chi no longer
18
+ # loads them, and the Installer refuses a source that has any. Installer
18
19
  # runs it after the memories; it adds to the installer's results and
19
20
  # warnings, and #run returns what the record (Provenance#write) keeps.
20
21
  # A dev install (+dev+, `chi bundle install --dev`) links each file to
21
22
  # the source instead of copying it, and warns of no checksum mismatch.
22
23
  # An edited file it replaces or removes is copied to the trash first,
23
- # under its subfolder (Trash#copy_edit); a link never is.
24
+ # under its subfolder (Trash#copy_edits); a link never is.
24
25
  class AssetInstaller
25
26
  # What was installed: the paths Provenance#write records (each
26
27
  # empty / nil on a dry run).
27
- # hooks every hook basename the bundle ships
28
- # hook_files, guardrail_files, script_files
29
- # basename => installed path
28
+ # guardrail_files, script_files basename => installed path
30
29
  # plugin_file the installed plugin file, nil without one
31
- Installed = Data.define(:hooks, :hook_files, :guardrail_files, :plugin_file, :script_files)
30
+ Installed = Data.define(:guardrail_files, :plugin_file, :script_files)
31
+
32
+ # The retire's summary words (Installer#summary): why the hooks went.
33
+ RETIRED = "chi no longer loads class-file bundle hooks"
32
34
 
33
35
  # @param source_dir [String] the normalized bundle source
34
36
  # @param manifest [Manifest, nil] nil for a non-strict install without one
@@ -61,145 +63,69 @@ module Samagotchi
61
63
  # @raise [Installer::InstallError] the manifest names a plugin or
62
64
  # script the bundle doesn't have
63
65
  def run
64
- install_hooks
65
- prune_dropped_hooks if (@upgrade || (@dev && @existing&.dev)) && @existing && !@dry_run
66
- verify_hook_checksums if @manifest && @strict && !@dev
67
66
  incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
68
67
  .reject { |src| linked_out?(src, "guardrails/#{File.basename(src)}") }
69
68
  guardrail_files = install_guardrails(incoming_rules)
70
69
  plugin_file = install_plugin
71
70
  script_files = install_scripts
72
- warn_hooks_requires_chi
71
+ # After the plugin and scripts, which raise for a file the manifest
72
+ # names and the bundle lacks: a refused install leaves hooks/ as it was.
73
+ retire_hooks
73
74
  warn_rules_requires_chi(incoming_rules)
74
- Installed.new(hooks: @bundle_hooks, hook_files: @hook_files, guardrail_files: guardrail_files,
75
- plugin_file: plugin_file, script_files: script_files)
75
+ Installed.new(guardrail_files: guardrail_files, plugin_file: plugin_file, script_files: script_files)
76
76
  end
77
77
 
78
- private
79
-
80
- # Copies the bundle's hooks/*.rb into <bundle_dir>/hooks/: the ones
81
- # its manifest lists, else (no hooks: in it) every hooks/*.rb, which is
82
- # then added to manifest.hooks with no event (the loader skips it, the
83
- # record keeps it). Without a manifest every hooks/*.rb is copied.
84
- # Sets @bundle_hooks (every hook the bundle ships) and @hook_files
85
- # (basename => installed path, for the record).
86
- def install_hooks
87
- @bundle_hooks = []
88
- @hook_files = {}
89
- return install_unlisted_hooks unless @manifest
90
-
91
- hook_entries = @manifest.hooks
92
- if hook_entries.empty?
93
- Dir.glob(File.join(@bundle_dir, "hooks", "*.rb")).each { |p| hook_entries[File.basename(p)] = BundleHook.new }
94
- end
95
- hook_entries.keys.each do |basename|
96
- next unless basename.is_a?(String) && !basename.empty?
97
-
98
- src = File.join(@bundle_dir, "hooks", basename)
99
- # A linked hook isn't the bundle's: not installed, counted or recorded.
100
- next hook_entries.delete(basename) if (File.symlink?(src) || File.exist?(src)) && linked_out?(src, "hooks/#{basename}")
78
+ # Whether the bundle file +src+ reads through a symlink: it is one, or
79
+ # it (or its dir, hooks/ say) resolves outside +bundle_dir+. A tar,
80
+ # zip or git repo keeps links; copying through one would install any
81
+ # file the user can read. MemoryFiles skips linked memories the same
82
+ # way. ShippedUpdate's plan reads it too.
83
+ def self.linked?(src, bundle_dir)
84
+ return true if File.symlink?(src)
101
85
 
102
- @bundle_hooks << basename
103
- install_hook(basename, src) if File.exist?(src)
104
- end
86
+ root = File.realpath(bundle_dir)
87
+ !File.realpath(src).start_with?("#{root}/")
88
+ rescue SystemCallError
89
+ true
105
90
  end
106
91
 
107
- # One listed hook; an upgrade overwrites a local edit with a warning.
108
- def install_hook(basename, src)
109
- dest = File.join(@provenance.hooks_dir, basename)
110
- if @dry_run
111
- @results[basename] = { status: File.exist?(dest) && !@force ? "would_skip" : "would_install" }
112
- return
113
- end
114
-
115
- prev_sha = @existing&.hooks&.dig(basename)&.sha256
116
- if @upgrade && File.exist?(dest) && !@force && !Provenance.recorded_sha(prev_sha).empty? && !Provenance.sha_matches?(dest, prev_sha)
117
- @warnings << "Hook #{basename} was locally modified; overwriting"
118
- end
119
-
120
- FileUtils.mkdir_p(@provenance.hooks_dir)
121
- unchanged = File.exist?(dest) && FileUtils.identical?(src, dest)
122
- @trash.copy_edit(dest, as: "hooks/#{basename}", recorded: prev_sha, incoming: src)
123
- place(src, dest)
124
- @results[basename] = if unchanged
125
- { status: "skipped", reason: "already up to date" }
126
- elsif @upgrade && @existing
127
- { status: "updated" }
128
- else
129
- { status: "installed" }
130
- end
131
- @hook_files[basename] = dest
132
- end
92
+ private
133
93
 
134
- # No manifest (a non-strict install): every hooks/*.rb is copied.
135
- def install_unlisted_hooks
136
- Dir.glob(File.join(@bundle_dir, "hooks", "*.rb")).each do |src|
137
- basename = File.basename(src)
138
- next if linked_out?(src, "hooks/#{basename}")
139
-
140
- @bundle_hooks << basename
141
- if @dry_run
142
- @results[basename] = { status: "would_install" }
143
- else
144
- FileUtils.mkdir_p(@provenance.hooks_dir)
145
- dest = File.join(@provenance.hooks_dir, basename)
146
- @trash.copy_edit(dest, as: "hooks/#{basename}", recorded: @existing&.hooks&.dig(basename)&.sha256, incoming: src)
147
- place(src, dest)
148
- @results[basename] = { status: "installed" }
149
- @hook_files[basename] = dest
150
- end
94
+ # The hooks/ an older chi installed (class-file hooks, which no longer
95
+ # load): every file in it goes, an edited one (or one the record
96
+ # doesn't know) copied to the trash first, a dev link never (it points
97
+ # at the source, which stays). Each is a :removed result (a dry run's
98
+ # :would_remove, through #planned) with the RETIRED note; an empty
99
+ # hooks/ dir goes without a word. The record keeps no hooks
100
+ # (Provenance#write).
101
+ def retire_hooks
102
+ dir = @provenance.hooks_dir
103
+ return unless File.directory?(dir) || File.symlink?(dir)
104
+
105
+ files = File.symlink?(dir) ? [] : Dir.glob("**/*", File::FNM_DOTMATCH, base: dir).sort.reject { |f| File.directory?(File.join(dir, f)) }
106
+ files.each do |file|
107
+ @results["hooks/#{file}"] = @dry_run ? planned(retired) : retired
151
108
  end
152
- end
109
+ return if @dry_run
153
110
 
154
- # An upgrade removes the hooks the previous version shipped and this
155
- # one doesn't (a local file too, with a warning; a dev link silently).
156
- def prune_dropped_hooks
157
- @existing.hooks.each_key do |old_key_str|
158
- next if @bundle_hooks.include?(old_key_str)
159
-
160
- old_dest = File.join(@provenance.hooks_dir, old_key_str)
161
- if File.exist?(old_dest) && !@force && !File.symlink?(old_dest)
162
- @warnings << "Bundle no longer includes hook #{old_key_str} but local file exists — removing"
163
- end
164
- @trash.copy_edit(old_dest, as: "hooks/#{old_key_str}", recorded: @existing.hooks[old_key_str]&.sha256)
165
- FileUtils.rm_f(old_dest)
166
- end
111
+ @trash.copy_edits(dir, "hooks", @existing&.hooks&.transform_values(&:sha256))
112
+ FileUtils.rm_rf(dir)
167
113
  end
168
114
 
169
- # Strict mode: an installed hook whose sha256 differs from the one its
170
- # manifest declares warns (as verify_memory_checksums does).
171
- def verify_hook_checksums
172
- @bundle_hooks.each do |basename|
173
- next if @conflicts.key?(basename)
174
-
175
- expected = @manifest.checksum_for_hook(basename)
176
- next unless expected
177
-
178
- dest = File.join(@provenance.hooks_dir, basename)
179
- next unless File.exist?(dest)
180
-
181
- actual = FileDigest.hex(dest)
182
- if actual != expected
183
- @warnings << "Checksum mismatch for hook #{basename}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
184
- end
185
- end
186
- end
115
+ def retired = Installer::FileResult.new(word: :removed, note: RETIRED)
187
116
 
188
117
  # The bundle's guardrails/*.yml into <bundle_dir>/guardrails/: the
189
118
  # set replaces what an earlier version installed. Same bytes as
190
119
  # installed are reported up to date, not "Installed".
191
120
  # @return [Hash{String => String}] basename => installed path ({} on a dry run)
192
121
  def install_guardrails(incoming_rules)
122
+ target = @provenance.guardrails_dir
123
+ results = incoming_rules.to_h { |src| [src, result_for(src, File.join(target, File.basename(src)), updated: false)] }
193
124
  if @dry_run
194
- incoming_rules.each { |src| @results["guardrails/#{File.basename(src)}"] = { status: "would_install" } }
125
+ results.each { |src, result| @results["guardrails/#{File.basename(src)}"] = planned(result) }
195
126
  return {}
196
127
  end
197
128
 
198
- target = @provenance.guardrails_dir
199
- unchanged = incoming_rules.select do |src|
200
- dest = File.join(target, File.basename(src))
201
- File.file?(dest) && FileUtils.identical?(src, dest)
202
- end
203
129
  @trash.copy_edits(target, "guardrails", @existing&.guardrails, incoming_rules.to_h { |src| [File.basename(src), src] })
204
130
  FileUtils.rm_rf(target)
205
131
  return {} if incoming_rules.empty?
@@ -208,8 +134,7 @@ module Samagotchi
208
134
  incoming_rules.to_h do |src|
209
135
  dest = File.join(target, File.basename(src))
210
136
  place(src, dest)
211
- @results["guardrails/#{File.basename(src)}"] =
212
- unchanged.include?(src) ? { status: "skipped", reason: "already up to date" } : { status: "installed" }
137
+ @results["guardrails/#{File.basename(src)}"] = results[src]
213
138
  [File.basename(src), dest]
214
139
  end
215
140
  end
@@ -235,29 +160,20 @@ module Samagotchi
235
160
  if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
236
161
  @warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
237
162
  end
163
+ dest = File.join(@provenance.plugin_dir, plugin[:file])
164
+ result = result_for(src, dest, updated: @upgrade && @provenance.installed?)
238
165
  if @dry_run
239
- @results[plugin[:file]] = { status: "would_install" }
166
+ @results[plugin[:file]] = planned(result)
240
167
  return nil
241
168
  end
242
169
 
243
- dest = File.join(@provenance.plugin_dir, plugin[:file])
244
- unchanged = File.file?(dest) && FileUtils.identical?(src, dest)
245
170
  @trash.copy_edits(@provenance.plugin_dir, "plugin", @existing&.plugin_shas, { plugin[:file] => src })
246
171
  FileUtils.rm_rf(@provenance.plugin_dir)
247
172
  FileUtils.mkdir_p(@provenance.plugin_dir)
248
173
  place(src, dest)
249
- @results[plugin[:file]] = if unchanged
250
- { status: "skipped", reason: "already up to date" }
251
- elsif @upgrade && @provenance.installed?
252
- { status: "updated" }
253
- else
254
- { status: "installed" }
255
- end
256
- expected = @manifest.checksum_for_plugin
257
- actual = FileDigest.hex(dest)
258
- if @strict && !@dev && expected && actual != expected
259
- @warnings << "Checksum mismatch for plugin #{plugin[:file]}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
260
- end
174
+ @results[plugin[:file]] = result
175
+ warning = FileDigest.mismatch("plugin #{plugin[:file]}", dest, @manifest.checksum_for_plugin) if @strict && !@dev
176
+ @warnings << warning if warning
261
177
  dest
262
178
  end
263
179
 
@@ -274,12 +190,15 @@ module Samagotchi
274
190
 
275
191
  @warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src) && !page_only?(file)
276
192
  end
277
- return {} if @dry_run
278
-
279
- unchanged = scripts.keys.select do |file|
280
- dest = File.join(@provenance.scripts_dir, file)
281
- File.file?(dest) && FileUtils.identical?(File.join(@bundle_dir, "scripts", file), dest)
193
+ results = scripts.keys.to_h do |file|
194
+ [file, result_for(File.join(@bundle_dir, "scripts", file), File.join(@provenance.scripts_dir, file),
195
+ updated: @upgrade && @provenance.installed?)]
282
196
  end
197
+ if @dry_run
198
+ results.each { |file, result| @results["scripts/#{file}"] = planned(result) }
199
+ return {}
200
+ end
201
+
283
202
  @trash.copy_edits(@provenance.scripts_dir, "scripts", @existing&.scripts,
284
203
  scripts.keys.to_h { |file| [file, File.join(@bundle_dir, "scripts", file)] })
285
204
  FileUtils.rm_rf(@provenance.scripts_dir)
@@ -289,20 +208,38 @@ module Samagotchi
289
208
  scripts.to_h do |file, declared|
290
209
  dest = File.join(@provenance.scripts_dir, file)
291
210
  place(File.join(@bundle_dir, "scripts", file), dest)
292
- @results["scripts/#{file}"] = if unchanged.include?(file) then { status: "skipped", reason: "already up to date" }
293
- elsif @upgrade && @provenance.installed? then { status: "updated" }
294
- else { status: "installed" }
295
- end
211
+ @results["scripts/#{file}"] = results[file]
296
212
  FileUtils.chmod(0o755, dest) unless @dev
297
- actual = FileDigest.hex(dest)
298
- expected = declared.to_s.delete_prefix("sha256:")
299
- if @strict && !@dev && !expected.empty? && actual != expected
300
- @warnings << "Checksum mismatch for script #{file}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
301
- end
213
+ warning = FileDigest.mismatch("script #{file}", dest, declared.to_s.delete_prefix("sha256:")) if @strict && !@dev
214
+ @warnings << warning if warning
302
215
  [file, dest]
303
216
  end
304
217
  end
305
218
 
219
+ # What the real run reports for +src+ over +dest+: up to date when the
220
+ # bytes are the same, else updated (+updated+: an upgrade over an
221
+ # install, of a file that is there) or installed. A dry run records #planned of it, so the two
222
+ # can't drift.
223
+ def result_for(src, dest, updated:)
224
+ word = if File.file?(dest) && FileUtils.identical?(src, dest) then :up_to_date
225
+ elsif updated && File.exist?(dest) then :updated
226
+ else :installed
227
+ end
228
+ Installer::FileResult.new(word: word)
229
+ end
230
+
231
+ # A dry run's word for a result: would_install, would_update,
232
+ # would_remove, or the up to date result as it is (nothing would be
233
+ # written).
234
+ def planned(result)
235
+ case result.word
236
+ when :installed then result.with(word: :would_install)
237
+ when :updated then result.with(word: :would_update)
238
+ when :removed then result.with(word: :would_remove)
239
+ else result
240
+ end
241
+ end
242
+
306
243
  # A file only a block renderer's load names: the page loads it, nothing runs it.
307
244
  def page_only?(file)
308
245
  @manifest.block_renderers.any? { |renderer| renderer.load.include?(file) } &&
@@ -316,19 +253,8 @@ module Samagotchi
316
253
  @dev ? File.symlink(File.realpath(src), dest) : FileUtils.cp(src, dest)
317
254
  end
318
255
 
319
- # Whether the bundle file +src+ reads through a symlink: it is one, or
320
- # it (or its dir, hooks/ say) resolves outside the bundle dir. A tar,
321
- # zip or git repo keeps links; copying through one would install any
322
- # file the user can read. MemoryFiles skips linked memories the same
323
- # way.
324
- def linked?(src)
325
- return true if File.symlink?(src)
326
-
327
- root = File.realpath(@bundle_dir)
328
- !File.realpath(src).start_with?("#{root}/")
329
- rescue SystemCallError
330
- true
331
- end
256
+ # AssetInstaller.linked? for this bundle's source.
257
+ def linked?(src) = self.class.linked?(src, @bundle_dir)
332
258
 
333
259
  # linked? with a "<label> is a symlink (not installed)" warning.
334
260
  def linked_out?(src, label)
@@ -338,21 +264,10 @@ module Samagotchi
338
264
  true
339
265
  end
340
266
 
341
- # The hook loader skips every hook of a bundle whose requires_chi this
342
- # chi doesn't meet (Hooks::BundleLoader); say so at install, as
343
- # install_plugin does for the plugin.
344
- def warn_hooks_requires_chi
345
- return if @manifest.nil? || @bundle_hooks.empty?
346
-
347
- if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
348
- @warnings << "Bundle #{@name}: its hooks won't load: #{failure}"
349
- end
350
- end
351
-
352
267
  # GuardrailWiring#bundle_rules doesn't read the rules of a bundle whose
353
268
  # requires_chi this chi doesn't meet, and a session that never loaded
354
269
  # them records a required load failure (the gate refuses guarded calls
355
- # until chi is updated): say so at install, as the hooks warning does.
270
+ # until chi is updated): say so at install, as install_plugin does.
356
271
  def warn_rules_requires_chi(rule_files)
357
272
  return if @manifest.nil? || rule_files.empty?
358
273
 
@@ -4,7 +4,6 @@ require "digest"
4
4
  require_relative "file_digest"
5
5
  require "fileutils"
6
6
  require "tmpdir"
7
- require_relative "bundle_hook"
8
7
  require_relative "manifest"
9
8
  require_relative "placeholder"
10
9
  require_relative "../memory_paths"
@@ -12,7 +11,9 @@ require_relative "../model_overlay"
12
11
 
13
12
  module Samagotchi
14
13
  module MemoryBundle
15
- # Builds local memories and installed hooks into a shareable bundle.
14
+ # Builds local memories and an installed bundle's rules, plugin and
15
+ # scripts into a shareable bundle (class-file hooks are retired: an older
16
+ # record's hooks/ isn't built).
16
17
  #
17
18
  # Inverse of Installer: reads from the resolved scope directory,
18
19
  # computes SHA256 checksums, writes manifest.yml via Manifest.write,
@@ -27,11 +28,10 @@ module Samagotchi
27
28
  # record and its dir), else from the scope dir:
28
29
  # record the InstalledBundle, nil when none is installed (or
29
30
  # it doesn't read)
30
- # hooks basename => BundleHook; hook_files basename => path
31
31
  # rules guardrails/*.yml paths
32
32
  # plugin the installed plugin file, nil without one
33
33
  # scripts file name => path; context_providers, block_renderers the parsed lists
34
- InstalledParts = Data.define(:record, :hooks, :hook_files, :rules, :plugin, :scripts, :context_providers, :block_renderers)
34
+ InstalledParts = Data.define(:record, :rules, :plugin, :scripts, :context_providers, :block_renderers)
35
35
 
36
36
  attr_reader :built_files, :warnings, :placeholder_warnings, :out_path, :staging_dir
37
37
 
@@ -138,12 +138,10 @@ module Samagotchi
138
138
  end
139
139
  end
140
140
 
141
- # The installed bundle's hooks (a recorded one missing on disk left
142
- # out; one with no recorded sha gets its file's), guardrail rules,
143
- # plugin, scripts, context providers and block renderers (its
144
- # InstalledManifest's). Without hooks or rules in a
145
- # record, the scope dir's hooks/*.rb and guardrails/*.yml (building
146
- # from a raw bundle dir that is also the memories dir).
141
+ # The installed bundle's guardrail rules, plugin, scripts, context
142
+ # providers and block renderers (its InstalledManifest's). Without
143
+ # rules in a record, the scope dir's guardrails/*.yml (building from a
144
+ # raw bundle dir that is also the memories dir).
147
145
  # @return [InstalledParts]
148
146
  def installed_parts(name, target_dir)
149
147
  prov = nil
@@ -156,23 +154,6 @@ module Samagotchi
156
154
  record = nil
157
155
  end
158
156
 
159
- hooks = {}
160
- hook_files = {}
161
- if record&.hooks&.any?
162
- record.hooks.each do |basename, hook|
163
- src = File.join(prov.hooks_dir, basename)
164
- next unless File.exist?(src)
165
-
166
- hooks[basename] = hook.sha256.empty? ? hook.with(sha256: BundleHook.of_file(src).sha256) : hook
167
- hook_files[basename] = src
168
- end
169
- else
170
- Dir.glob(File.join(target_dir, "hooks", "*.rb")).sort.each do |src|
171
- hooks[File.basename(src)] = BundleHook.of_file(src)
172
- hook_files[File.basename(src)] = src
173
- end
174
- end
175
-
176
157
  rules_dir = record&.guardrails&.any? ? prov.guardrails_dir : File.join(target_dir, "guardrails")
177
158
  plugin = record ? prov.plugin_path(record) : nil
178
159
  manifest = record&.manifest
@@ -181,7 +162,7 @@ module Samagotchi
181
162
  raise BuildError, "the installed #{name}'s record: #{problem}" if problem
182
163
 
183
164
  InstalledParts.new(
184
- record: record, hooks: hooks, hook_files: hook_files,
165
+ record: record,
185
166
  rules: Dir.exist?(rules_dir) ? Dir.glob(File.join(rules_dir, "*.yml")).sort : [],
186
167
  plugin: plugin && File.file?(plugin) ? plugin : nil,
187
168
  scripts: installed_scripts(prov, record),
@@ -190,14 +171,10 @@ module Samagotchi
190
171
  )
191
172
  end
192
173
 
193
- # The memories, hooks/, guardrails/, the plugin and scripts/ into the
174
+ # The memories, guardrails/, the plugin and scripts/ into the
194
175
  # staging dir, laid out as Installer reads a bundle.
195
176
  def stage_files(staging, candidates, parts)
196
177
  candidates.each { |src| FileUtils.cp(src, File.join(staging, File.basename(src))) }
197
- unless parts.hook_files.empty?
198
- FileUtils.mkdir_p(File.join(staging, "hooks"))
199
- parts.hook_files.each { |basename, src| FileUtils.cp(src, File.join(staging, "hooks", basename)) }
200
- end
201
178
  unless parts.rules.empty?
202
179
  FileUtils.mkdir_p(File.join(staging, "guardrails"))
203
180
  parts.rules.each { |src| FileUtils.cp(src, File.join(staging, "guardrails", File.basename(src))) }
@@ -225,9 +202,9 @@ module Samagotchi
225
202
  scope: @scope,
226
203
  description: description,
227
204
  files: files_map,
228
- hooks: parts.hooks.empty? ? nil : parts.hooks,
229
205
  trust_level: trust_level,
230
- plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin) },
206
+ plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin),
207
+ required: manifest&.plugin_required },
231
208
  requires_chi: manifest&.requires_chi,
232
209
  needs: manifest&.needs,
233
210
  scripts: parts.scripts.transform_values { |src| FileDigest.sha256(src) },
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "file_digest"
4
-
5
3
  module Samagotchi
6
4
  module MemoryBundle
7
- # One hook's metadata, as a bundle's manifest.yml declares it and its
8
- # installed record (manifest.json) keeps it: the file's sha256
5
+ # One class-file hook's metadata, as an older chi's installed record
6
+ # (manifest.json) keeps it (chi no longer installs or loads them:
7
+ # Manifest::HooksRetired; it reads them to retire, uninstall and name
8
+ # them, and the trash selection takes #sha256): the file's sha256
9
9
  # ("sha256:<hex>", "" when none is recorded), the events it registers
10
10
  # for (manifest `event:` for one, `events:` a list; none: it isn't
11
- # loaded), on_error (skip, log or fail_closed) and priority. One
12
- # instance of the hook serves every event it lists. The one place the
13
- # defaults live.
11
+ # loaded), on_error (skip, log or fail_closed) and priority. The one
12
+ # place the defaults live.
14
13
  class BundleHook < Data.define(:sha256, :events, :on_error, :priority)
15
14
  DEFAULT_ON_ERROR = "skip"
16
15
  DEFAULT_PRIORITY = 100
@@ -37,35 +36,11 @@ module Samagotchi
37
36
  priority: (h["priority"] || DEFAULT_PRIORITY).to_i)
38
37
  end
39
38
 
40
- # A hook known only by its file (no declared event): its sha, the defaults.
41
- # Callers pass a file that is there (FileDigest is nil for one that isn't).
42
- def self.of_file(path)
43
- new(sha256: FileDigest.sha256(path))
44
- end
45
-
46
39
  # "sha256:<hex>" from either form, "" for none.
47
40
  def self.prefixed(sha)
48
41
  sha = sha.to_s.strip
49
42
  sha.empty? || sha.start_with?("sha256:") ? sha : "sha256:#{sha}"
50
43
  end
51
-
52
- # The sha's hex, "" when none is recorded.
53
- def hex = sha256.delete_prefix("sha256:")
54
-
55
- # Whether the hook fails closed: on_error fail_closed applies to its
56
- # before_tool_call registration only.
57
- def fail_closed?(event) = on_error == "fail_closed" && event.to_s == "before_tool_call"
58
-
59
- # How `chi bundle status` / `diff` name its events: `event=x`, or
60
- # `events=x,y` for more than one.
61
- def events_field = events.size > 1 ? "events=#{events.join(",")}" : "event=#{events.first}"
62
-
63
- # The string-keyed form manifest.yml and manifest.json hold: `event`
64
- # for one event (as an older chi reads it), `events` for more.
65
- def to_record
66
- listed = events.size > 1 ? { "events" => events.dup } : { "event" => events.first.to_s }
67
- { "sha256" => sha256, **listed, "on_error" => on_error, "priority" => priority }
68
- end
69
44
  end
70
45
  end
71
46
  end