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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
|
@@ -5,7 +5,6 @@ require "digest"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require_relative "../context_providers"
|
|
7
7
|
require_relative "../version"
|
|
8
|
-
require_relative "bundle_hook"
|
|
9
8
|
|
|
10
9
|
module Samagotchi
|
|
11
10
|
autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
|
|
@@ -26,6 +25,8 @@ module Samagotchi
|
|
|
26
25
|
# plugin: # optional — the bundle's plugin.rb (docs/plugins.md)
|
|
27
26
|
# file: plugin.rb
|
|
28
27
|
# sha256: sha256:0a1b...
|
|
28
|
+
# required: true # optional: a guardrail; when it fails to load
|
|
29
|
+
# # chi denies every tool call (docs/hooks.md)
|
|
29
30
|
# requires_chi: ">= 0.1.28" # optional — a Gem::Requirement for chi's VERSION
|
|
30
31
|
# requires_chi_next: true # optional, shipped bundles — ignored here:
|
|
31
32
|
# # `rake release:bump` raises requires_chi to
|
|
@@ -64,6 +65,26 @@ module Samagotchi
|
|
|
64
65
|
class Manifest
|
|
65
66
|
class ValidationError < StandardError; end
|
|
66
67
|
|
|
68
|
+
# Class-file bundle hooks (hooks/*.rb, the manifest's hooks:) are
|
|
69
|
+
# retired: chi installs and loads none. WORDS is the one wording of
|
|
70
|
+
# it, wherever chi meets one (the Engine's first-turn line, install,
|
|
71
|
+
# chi bundle check and status, chi update's row). A strict read of a
|
|
72
|
+
# manifest with a hooks: entry raises it (.for_key's message); a
|
|
73
|
+
# tolerant read (a kept manifest.yml) ignores the key.
|
|
74
|
+
class HooksRetired < ValidationError
|
|
75
|
+
WORDS = "class-file bundle hooks are retired: chi loads no hooks/*.rb; a bundle hooks events from its " \
|
|
76
|
+
"plugin.rb with chi.on, see docs/plugins.md"
|
|
77
|
+
|
|
78
|
+
# The manifest's hooks: key.
|
|
79
|
+
def self.for_key = new("hooks: #{WORDS}")
|
|
80
|
+
|
|
81
|
+
# A source's hooks/*.rb (+files+, relative), with or without the key.
|
|
82
|
+
def self.for_files(files) = new("#{files.join(", ")}: #{WORDS}")
|
|
83
|
+
|
|
84
|
+
# The source's hooks/*.rb, relative and sorted ([] for none).
|
|
85
|
+
def self.files_in(dir) = Dir.glob(File.join(dir, "hooks", "*.rb")).map { |f| "hooks/#{File.basename(f)}" }.sort
|
|
86
|
+
end
|
|
87
|
+
|
|
67
88
|
# A plugin file is a plain .rb name in the bundle's top directory.
|
|
68
89
|
PLUGIN_FILE = /\A[A-Za-z0-9_][A-Za-z0-9_.-]*\.rb\z/
|
|
69
90
|
# A script is a plain file name in the bundle's scripts/.
|
|
@@ -86,7 +107,7 @@ module Samagotchi
|
|
|
86
107
|
# admits a chi that never saw the key. A key the first tag (v0.2.0)
|
|
87
108
|
# already reads has no entry. `files.description` is a files: entry given
|
|
88
109
|
# as a mapping, `*.types` a session_settings:/settings: given as a mapping
|
|
89
|
-
# of keys to types
|
|
110
|
+
# of keys to types. A key
|
|
90
111
|
# whose release is not out yet waits here as a comment naming it, the way
|
|
91
112
|
# Plugin::Api::SINCE does, until `release:bump` gives it its version
|
|
92
113
|
# (docs/releasing.md).
|
|
@@ -98,15 +119,14 @@ module Samagotchi
|
|
|
98
119
|
"session_settings" => "0.84.0",
|
|
99
120
|
"session_settings.types" => "0.92.0",
|
|
100
121
|
"settings.types" => "0.92.0",
|
|
101
|
-
"hooks.events" => "0.98.0",
|
|
102
122
|
"settings" => "0.143.0",
|
|
103
|
-
"block_renderers" => "0.151.0"
|
|
123
|
+
"block_renderers" => "0.151.0",
|
|
124
|
+
"plugin.required" => "0.157.0"
|
|
104
125
|
}.freeze
|
|
105
126
|
|
|
106
127
|
# files: name → "sha256:<hex>" (both forms); file_descriptions: name →
|
|
107
|
-
# the description a mapping gave, for the files that have one
|
|
108
|
-
|
|
109
|
-
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :hooks, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
128
|
+
# the description a mapping gave, for the files that have one.
|
|
129
|
+
attr_reader :name, :version, :scope, :description, :files, :file_descriptions, :trust_level, :plugin, :requires_chi, :needs, :includes,
|
|
110
130
|
:scripts, :context_providers, :block_renderers, :session_settings, :session_setting_types, :settings, :setting_types, :unknown_keys
|
|
111
131
|
|
|
112
132
|
def initialize(path:)
|
|
@@ -128,7 +148,8 @@ module Samagotchi
|
|
|
128
148
|
@scope = normalize_scope(raw["scope"])
|
|
129
149
|
@description = (raw["description"] || "").to_s
|
|
130
150
|
@files = parse_files(raw["files"] || {})
|
|
131
|
-
|
|
151
|
+
raise HooksRetired.for_key if self.class.lists_hooks?(raw["hooks"])
|
|
152
|
+
|
|
132
153
|
@file_descriptions = parse_file_descriptions(raw["files"])
|
|
133
154
|
@trust_level = (raw["trust_level"] || "experimental").to_s
|
|
134
155
|
@plugin = parse_plugin(raw["plugin"])
|
|
@@ -167,8 +188,8 @@ module Samagotchi
|
|
|
167
188
|
# chi (a newer chi tightened it, or an older one wrote it) costs only
|
|
168
189
|
# that key, which reads as empty with its message in the problems:
|
|
169
190
|
# name, version (never checked: identity is the record's), scope;
|
|
170
|
-
# files and their descriptions, scripts, plugin
|
|
171
|
-
#
|
|
191
|
+
# files and their descriptions, scripts, plugin (hooks: is ignored,
|
|
192
|
+
# HooksRetired); needs, context_providers; block_renderers
|
|
172
193
|
# (against +fallback_scripts+ when scripts: failed); includes (after
|
|
173
194
|
# what its check reads); session_settings with its types, settings
|
|
174
195
|
# with theirs (one raw value each, so one problem); requires_chi,
|
|
@@ -183,6 +204,10 @@ module Samagotchi
|
|
|
183
204
|
[manifest, manifest.send(:read_tolerantly, path, fallback_scripts)]
|
|
184
205
|
end
|
|
185
206
|
|
|
207
|
+
# Whether a manifest's hooks: value lists anything (none, {} and []
|
|
208
|
+
# don't: a manifest may keep an empty key).
|
|
209
|
+
def self.lists_hooks?(raw) = !(raw.nil? || ((raw.is_a?(Hash) || raw.is_a?(Array)) && raw.empty?))
|
|
210
|
+
|
|
186
211
|
def self.read(dir:)
|
|
187
212
|
manifest_path = File.join(dir, "manifest.yml")
|
|
188
213
|
raise ValidationError, "manifest.yml not found in #{dir}" unless File.exist?(manifest_path)
|
|
@@ -201,13 +226,6 @@ module Samagotchi
|
|
|
201
226
|
hex unless hex.nil? || hex.empty?
|
|
202
227
|
end
|
|
203
228
|
|
|
204
|
-
# Returns sha256 hex for a hook basename (e.g. "guardrails.rb"), nil
|
|
205
|
-
# when it declares none (nothing to verify).
|
|
206
|
-
def checksum_for_hook(basename)
|
|
207
|
-
hex = @hooks[basename]&.hex
|
|
208
|
-
hex unless hex.nil? || hex.empty?
|
|
209
|
-
end
|
|
210
|
-
|
|
211
229
|
# @return [String, nil] the plugin's sha256 hex, "sha256:" stripped
|
|
212
230
|
def checksum_for_plugin
|
|
213
231
|
sha = @plugin && @plugin[:sha256].to_s
|
|
@@ -239,8 +257,8 @@ module Samagotchi
|
|
|
239
257
|
|
|
240
258
|
# The SINCE keys this manifest actually uses, each with the chi release
|
|
241
259
|
# that first read it: the plain keys, and the shapes only a newer chi
|
|
242
|
-
# reads — a files: entry given as a mapping, a
|
|
243
|
-
#
|
|
260
|
+
# reads — a files: entry given as a mapping, a session_settings:/settings:
|
|
261
|
+
# given as a mapping of keys to types.
|
|
244
262
|
# @return [Hash{String => String}]
|
|
245
263
|
def floor_keys
|
|
246
264
|
raw = @raw
|
|
@@ -249,20 +267,18 @@ module Samagotchi
|
|
|
249
267
|
found["scripts"] = SINCE["scripts"] unless @scripts.empty?
|
|
250
268
|
found["context_providers"] = SINCE["context_providers"] unless @context_providers.empty?
|
|
251
269
|
found["block_renderers"] = SINCE["block_renderers"] unless @block_renderers.empty?
|
|
270
|
+
found["plugin.required"] = SINCE["plugin.required"] if @plugin&.dig(:required) && SINCE.key?("plugin.required")
|
|
252
271
|
found["session_settings"] = SINCE["session_settings"] unless @session_settings.empty?
|
|
253
272
|
found["settings"] = SINCE["settings"] unless @settings.nil?
|
|
254
273
|
found["files.description"] = SINCE["files.description"] if raw["files"].is_a?(Hash) && raw["files"].values.any?(Hash)
|
|
255
274
|
found["session_settings.types"] = SINCE["session_settings.types"] if raw["session_settings"].is_a?(Hash)
|
|
256
275
|
found["settings.types"] = SINCE["settings.types"] if raw["settings"].is_a?(Hash)
|
|
257
|
-
if raw["hooks"].is_a?(Hash) && raw["hooks"].values.any? { |entry| entry.is_a?(Hash) && entry.key?("events") }
|
|
258
|
-
found["hooks.events"] = SINCE["hooks.events"]
|
|
259
|
-
end
|
|
260
276
|
found
|
|
261
277
|
end
|
|
262
278
|
|
|
263
279
|
# Writes a fresh manifest.yml from computed checksums at `dir`.
|
|
264
280
|
# +file_descriptions+ (name → text) writes those files as mappings.
|
|
265
|
-
def self.write(dir:, name:, version:, files:, scope: nil, description: "",
|
|
281
|
+
def self.write(dir:, name:, version:, files:, scope: nil, description: "", trust_level: nil,
|
|
266
282
|
plugin: nil, requires_chi: nil, needs: nil, scripts: {}, context_providers: [], block_renderers: [], file_descriptions: {},
|
|
267
283
|
session_settings: [], session_setting_types: {}, settings: nil, setting_types: {})
|
|
268
284
|
FileUtils.mkdir_p(dir)
|
|
@@ -276,11 +292,11 @@ module Samagotchi
|
|
|
276
292
|
text = file_descriptions[key].to_s.strip
|
|
277
293
|
[key, text.empty? ? sha : { "sha256" => sha, "description" => text }]
|
|
278
294
|
end
|
|
279
|
-
manifest["hooks"] = hooks.to_h { |k, v| [k.to_s, BundleHook.parse(v).to_record] } if hooks && !hooks.empty?
|
|
280
295
|
manifest["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
|
|
281
296
|
if plugin
|
|
282
297
|
sha = plugin[:sha256].to_s
|
|
283
298
|
manifest["plugin"] = { "file" => plugin[:file].to_s, "sha256" => sha.start_with?("sha256:") ? sha : "sha256:#{sha}" }
|
|
299
|
+
manifest["plugin"]["required"] = true if plugin[:required]
|
|
284
300
|
end
|
|
285
301
|
manifest["requires_chi"] = requires_chi.to_s if requires_chi && !requires_chi.to_s.empty?
|
|
286
302
|
needs = parse_needs(needs)
|
|
@@ -384,7 +400,7 @@ module Samagotchi
|
|
|
384
400
|
end
|
|
385
401
|
@scripts = tolerate(problems, "scripts", {}) { parse_scripts(raw["scripts"]) }
|
|
386
402
|
@plugin = tolerate(problems, "plugin", nil) { parse_plugin(raw["plugin"]) }
|
|
387
|
-
|
|
403
|
+
# An old kept manifest's hooks: is no problem: it isn't read (HooksRetired).
|
|
388
404
|
@needs = tolerate(problems, "needs", []) { self.class.parse_needs(raw["needs"]) }
|
|
389
405
|
@context_providers = tolerate(problems, "context_providers", []) { parse_context_providers(raw["context_providers"]) }
|
|
390
406
|
script_names = problems.key?("scripts") ? fallback_scripts.map(&:to_s) : @scripts.keys
|
|
@@ -471,8 +487,10 @@ module Samagotchi
|
|
|
471
487
|
end
|
|
472
488
|
end
|
|
473
489
|
|
|
474
|
-
# plugin: {file:, sha256:} → {file:, sha256:} (sha "" when
|
|
475
|
-
#
|
|
490
|
+
# plugin: {file:, sha256:, required:} → {file:, sha256:} (sha "" when
|
|
491
|
+
# not given) with required: true when it says so, or nil; a file that
|
|
492
|
+
# isn't a plain .rb name, or a required: that isn't true or false, is a
|
|
493
|
+
# validation error.
|
|
476
494
|
def parse_plugin(raw)
|
|
477
495
|
return nil if raw.nil?
|
|
478
496
|
raise ValidationError, "plugin: must be a mapping with file: and sha256:" unless raw.is_a?(Hash)
|
|
@@ -484,7 +502,10 @@ module Samagotchi
|
|
|
484
502
|
|
|
485
503
|
sha = (raw["sha256"] || raw[:sha256]).to_s.strip
|
|
486
504
|
sha = "sha256:#{sha}" unless sha.empty? || sha.start_with?("sha256:")
|
|
487
|
-
|
|
505
|
+
required = raw.key?("required") ? raw["required"] : raw[:required]
|
|
506
|
+
raise ValidationError, "plugin required: must be true or false, not #{required.inspect}" unless [nil, true, false].include?(required)
|
|
507
|
+
|
|
508
|
+
required ? { file: file, sha256: sha, required: true } : { file: file, sha256: sha }
|
|
488
509
|
end
|
|
489
510
|
|
|
490
511
|
# scripts: {file => sha256} → the same, "sha256:" prefixed; {} when absent.
|
|
@@ -536,8 +557,8 @@ module Samagotchi
|
|
|
536
557
|
|
|
537
558
|
name
|
|
538
559
|
end.uniq
|
|
539
|
-
if !names.empty? && (!@files.empty? ||
|
|
540
|
-
raise ValidationError, "a bundle with includes: holds only its includes (no files,
|
|
560
|
+
if !names.empty? && (!@files.empty? || @plugin || !@scripts.empty?)
|
|
561
|
+
raise ValidationError, "a bundle with includes: holds only its includes (no files, plugin or scripts)"
|
|
541
562
|
end
|
|
542
563
|
|
|
543
564
|
names
|
|
@@ -549,34 +570,6 @@ module Samagotchi
|
|
|
549
570
|
text = raw.to_s.strip
|
|
550
571
|
text.empty? ? nil : text
|
|
551
572
|
end
|
|
552
|
-
|
|
553
|
-
def parse_hooks(raw)
|
|
554
|
-
return {} unless raw.is_a?(Hash)
|
|
555
|
-
|
|
556
|
-
raw.each_with_object({}) do |(k, v), acc|
|
|
557
|
-
next unless k.is_a?(String) && !k.empty?
|
|
558
|
-
next unless v.is_a?(Hash)
|
|
559
|
-
|
|
560
|
-
check_hook_events(k, v)
|
|
561
|
-
acc[k] = BundleHook.parse(v)
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
HOOK_EVENT_NAME = /\A[a-z][a-z0-9_]*\z/
|
|
566
|
-
|
|
567
|
-
# A hook's events: list holds event names (one instance registered
|
|
568
|
-
# for each); it and event: are one or the other.
|
|
569
|
-
def check_hook_events(file, entry)
|
|
570
|
-
return unless entry.key?("events")
|
|
571
|
-
|
|
572
|
-
raw = entry["events"]
|
|
573
|
-
raise ValidationError, "hooks: #{file}: give event: or events:, not both" if entry.key?("event")
|
|
574
|
-
raise ValidationError, "hooks: #{file}: events: must be a list of event names" unless raw.is_a?(Array) && !raw.empty?
|
|
575
|
-
|
|
576
|
-
raw.each do |item|
|
|
577
|
-
raise ValidationError, "hooks: #{file}: events: #{item.inspect} is not an event name" unless item.is_a?(String) && item.strip.match?(HOOK_EVENT_NAME)
|
|
578
|
-
end
|
|
579
|
-
end
|
|
580
573
|
end
|
|
581
574
|
end
|
|
582
575
|
end
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require_relative "../atomic_file"
|
|
5
5
|
require_relative "file_digest"
|
|
6
6
|
require_relative "installed_bundle"
|
|
7
|
+
require_relative "file_actions"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
9
10
|
module MemoryBundle
|
|
@@ -13,8 +14,8 @@ module Samagotchi
|
|
|
13
14
|
|
|
14
15
|
# 3-way merge helper for bundle upgrades.
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# and
|
|
17
|
+
# An upgrade's merge: what FileState and FileActions' UPGRADE table say of
|
|
18
|
+
# a file, as an Outcome, and the text merge where both sides changed it.
|
|
18
19
|
module Merger
|
|
19
20
|
# What an upgrade does to one file. status names the case; base is where
|
|
20
21
|
# the file's next base comes from:
|
|
@@ -26,15 +27,13 @@ module Samagotchi
|
|
|
26
27
|
# edits, so it must never become the base (the edit is what
|
|
27
28
|
# differs from the base, and the next upgrade would
|
|
28
29
|
# fast-forward over it).
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
# one), #next_base if its base is a new kind, and its status word in
|
|
32
|
-
# Installer#preview_memory, #verify_memory_checksums' list and #summary.
|
|
30
|
+
# The same words as FileActions::Action#next_base: an Outcome is what an
|
|
31
|
+
# UPGRADE cell stands for to a plan, and a :decide cell's answer.
|
|
33
32
|
#
|
|
34
33
|
# merge is Upgrade#outcome's TextMerge::Result, so a merge runs git once
|
|
35
34
|
# and its answer travels with the outcome: a :merged outcome's text, a
|
|
36
35
|
# :conflict's why (nil for a real one). current_sha is the digest of the
|
|
37
|
-
# file that merge read: write_merged checks it.
|
|
36
|
+
# file that merge read: write_merged checks it. The constants leave both nil.
|
|
38
37
|
Outcome = Data.define(:status, :base, :merge, :current_sha) do
|
|
39
38
|
def initialize(status:, base:, merge: nil, current_sha: nil) = super
|
|
40
39
|
|
|
@@ -65,39 +64,38 @@ module Samagotchi
|
|
|
65
64
|
def conflicts = behind.map(&:file)
|
|
66
65
|
end
|
|
67
66
|
|
|
67
|
+
# One file's upgrade, read once (Upgrade#decision): its FileState, the
|
|
68
|
+
# action the upgrade takes (a :decide cell already answered) and the
|
|
69
|
+
# Outcome that action stands for.
|
|
70
|
+
Decision = Data.define(:state, :action, :outcome)
|
|
71
|
+
|
|
68
72
|
# One bundle's upgrade as seen before it runs: what it does to a file,
|
|
69
73
|
# nothing written. The one way to ask: Installer's merge and dry run,
|
|
70
74
|
# ShippedUpdate's plan and SystemBundle's dry run. Which files to ask
|
|
71
75
|
# about is the caller's, and they differ (see #forecast).
|
|
72
76
|
Upgrade = Data.define(:provenance, :target_dir, :source_dir) do
|
|
73
|
-
# What the upgrade does to the file
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
# @return [
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
legacy = InstalledBundle.legacy_mark?(provenance.record&.files&.fetch(file_key, nil))
|
|
83
|
-
# Asked only when the guard would otherwise fire.
|
|
84
|
-
unproved = -> { legacy && !ShippedHistory.shipped?(provenance.name, file_key, paths(file_key)[:base]) }
|
|
85
|
-
return Merger.merge_outcome(paths(file_key), unproved: unproved) if classified.conflict?
|
|
86
|
-
|
|
87
|
-
# The guard's own case at its plainest: the file is the base, which
|
|
88
|
-
# may be the user's text. One that only gains lines loses nothing, and
|
|
89
|
-
# save_marked_file keeps a copy.
|
|
90
|
-
sides = paths(file_key)
|
|
91
|
-
return classified unless legacy && Merger.removes_lines?(File.binread(sides[:current]), File.binread(sides[:incoming]))
|
|
92
|
-
|
|
93
|
-
unproved.call ? CONFLICT.with(merge: TextMerge.failed(FileStanding::LEGACY_BASE)) : classified
|
|
77
|
+
# What the upgrade does to the file, with what it was read from: its
|
|
78
|
+
# FileState, the UPGRADE action (a :decide cell answered by #outcome's
|
|
79
|
+
# merge or legacy guard, through FileActions.decided) and the Outcome.
|
|
80
|
+
# @param state [FileState] the file's, when the caller has read it
|
|
81
|
+
# @return [Decision]
|
|
82
|
+
def decision(file_key, state = state(file_key))
|
|
83
|
+
action = FileActions.for(:upgrade, state)
|
|
84
|
+
outcome = outcome_of(file_key, state, action)
|
|
85
|
+
Decision.new(state: state, action: action.do == :decide ? FileActions.decided(outcome) : action, outcome: outcome)
|
|
94
86
|
end
|
|
95
87
|
|
|
96
|
-
#
|
|
88
|
+
# What the upgrade does to the file: its FileState's UPGRADE cell, and
|
|
89
|
+
# where both sides changed it, the text merge (git runs here, once per
|
|
90
|
+
# call). A mark from an older chi fast-forwards only if no line of the
|
|
91
|
+
# file goes or its base is proved: see .merge_outcome.
|
|
97
92
|
# @return [Outcome]
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
def outcome(file_key) = decision(file_key).outcome
|
|
94
|
+
|
|
95
|
+
# The file as the upgrade meets it (FileState, with the source).
|
|
96
|
+
# @return [FileState]
|
|
97
|
+
def state(file_key)
|
|
98
|
+
FileState.of(file_key, record: provenance.record, provenance: provenance, target_dir: target_dir, source_dir: source_dir)
|
|
101
99
|
end
|
|
102
100
|
|
|
103
101
|
# The ones among +file_keys+ that would merge or end in a conflict. The
|
|
@@ -138,38 +136,50 @@ module Samagotchi
|
|
|
138
136
|
{ base: provenance.base_path(file_key), current: File.join(target_dir, file_key),
|
|
139
137
|
incoming: File.join(source_dir, file_key) }
|
|
140
138
|
end
|
|
141
|
-
end
|
|
142
139
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# The Outcome a cell stands for; a :decide cell's is the text merge
|
|
143
|
+
# (:diverged) or the legacy guard ([:pristine, :legacy]).
|
|
144
|
+
def outcome_of(file_key, state, action)
|
|
145
|
+
return NO_INCOMING if state.digests.incoming.nil?
|
|
146
|
+
|
|
147
|
+
case action.do
|
|
148
|
+
when :decide then decide(file_key, state)
|
|
149
|
+
when :write_incoming then action.word == :installed ? INSTALL : FAST_FORWARD
|
|
150
|
+
when :keep
|
|
151
|
+
return no_base(state) if action.behind?
|
|
152
|
+
|
|
153
|
+
action.word == :kept ? KEEP : NOOP
|
|
154
|
+
# A file the bundle doesn't own, the user's: there is no base to merge from.
|
|
155
|
+
when :skip then no_base(state)
|
|
156
|
+
# It can't be read: the read raises, as the merge would.
|
|
157
|
+
when :refuse then FileDigest.hex(paths(file_key)[:current])
|
|
158
|
+
else raise ArgumentError, "no outcome for #{action.do.inspect} (#{file_key})"
|
|
159
|
+
end
|
|
154
160
|
end
|
|
155
161
|
|
|
156
|
-
# No base
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
# No base to merge from: the file stays behind. With none on disk
|
|
163
|
+
# TextMerge::NO_BASE says why; one the checksum contradicts (the file
|
|
164
|
+
# equals it, yet is edited) may be the user's own text, as an older
|
|
165
|
+
# chi's can (FileStanding::LEGACY_BASE).
|
|
166
|
+
def no_base(state)
|
|
167
|
+
why = state.digests.base.nil? ? TextMerge::NO_BASE : FileStanding::LEGACY_BASE
|
|
168
|
+
CONFLICT.with(merge: TextMerge.failed(why))
|
|
159
169
|
end
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
CONFLICT
|
|
171
|
+
def decide(file_key, state)
|
|
172
|
+
# Asked only when the guard would otherwise fire.
|
|
173
|
+
unproved = -> { state.mark == :legacy && !ShippedHistory.shipped?(provenance.name, file_key, paths(file_key)[:base]) }
|
|
174
|
+
return Merger.merge_outcome(paths(file_key), unproved: unproved) unless state.kind == :pristine
|
|
175
|
+
|
|
176
|
+
# The guard's own case at its plainest: the file is the base, which
|
|
177
|
+
# may be the user's text. One that only gains lines loses nothing, and
|
|
178
|
+
# its save_first keeps a copy (Installer#save_first).
|
|
179
|
+
sides = paths(file_key)
|
|
180
|
+
return FAST_FORWARD unless Merger.removes_lines?(File.binread(sides[:current]), File.binread(sides[:incoming]))
|
|
181
|
+
|
|
182
|
+
unproved.call ? CONFLICT.with(merge: TextMerge.failed(FileStanding::LEGACY_BASE)) : FAST_FORWARD
|
|
173
183
|
end
|
|
174
184
|
end
|
|
175
185
|
|
|
@@ -243,12 +253,6 @@ module Samagotchi
|
|
|
243
253
|
def self.digest(path)
|
|
244
254
|
FileDigest.hex(path)
|
|
245
255
|
end
|
|
246
|
-
|
|
247
|
-
def self.current_modified?(base_path, current_path)
|
|
248
|
-
return true unless base_path && File.exist?(base_path) && File.exist?(current_path)
|
|
249
|
-
|
|
250
|
-
FileDigest.hex(base_path) != FileDigest.hex(current_path)
|
|
251
|
-
end
|
|
252
256
|
end
|
|
253
257
|
end
|
|
254
258
|
end
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
module Samagotchi
|
|
4
4
|
module MemoryBundle
|
|
5
5
|
# The plugin an installed bundle's record (manifest.json) names: its
|
|
6
|
-
# file under plugin
|
|
6
|
+
# file under plugin/, the sha256 recorded at install ("" when none
|
|
7
7
|
# is; either "sha256:<hex>" or a bare hex, Provenance.recorded_sha
|
|
8
|
-
# reads both)
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# reads both), and whether its manifest called it required (a guardrail
|
|
9
|
+
# plugin: plugin: required: true; an older chi ignores the key).
|
|
10
|
+
PluginRef = Data.define(:file, :sha256, :required) do
|
|
11
|
+
def initialize(file:, sha256: "", required: false)
|
|
11
12
|
super
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -20,7 +21,7 @@ module Samagotchi
|
|
|
20
21
|
|
|
21
22
|
h = raw.transform_keys(&:to_s)
|
|
22
23
|
file = h["file"].to_s
|
|
23
|
-
file.empty? ? nil : new(file: file, sha256: h["sha256"].to_s)
|
|
24
|
+
file.empty? ? nil : new(file: file, sha256: h["sha256"].to_s, required: h["required"] == true)
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
end
|
|
@@ -27,7 +27,7 @@ module Samagotchi
|
|
|
27
27
|
# installed: members installed now; already: members found installed
|
|
28
28
|
# (recorded, not reinstalled); skipped and failed: name => why;
|
|
29
29
|
# kept_own: member installed now => its memory files the user already
|
|
30
|
-
# had, left as they are and not owned (
|
|
30
|
+
# had, left as they are and not owned (the INSTALL rows of a file not owned);
|
|
31
31
|
# trash_copies: "member: line" for what --force copied to the trash
|
|
32
32
|
# (Installer#trash_copy_line).
|
|
33
33
|
InstallResult = Struct.new(:name, :version, :installed, :already, :skipped, :failed, :kept_own, :trash_copies, keyword_init: true) do
|
|
@@ -140,11 +140,11 @@ module Samagotchi
|
|
|
140
140
|
private_class_method :install_member
|
|
141
141
|
|
|
142
142
|
# An Installer result for a file the user already had, with other
|
|
143
|
-
# bytes:
|
|
144
|
-
# with the bundle's bytes (
|
|
145
|
-
# of the bundle's is missing (it stays the user's all the same).
|
|
143
|
+
# bytes: kept as the user's own (:kept_own), or a dry run's
|
|
144
|
+
# :would_skip. One with the bundle's bytes (:up_to_date) isn't named:
|
|
145
|
+
# nothing of the bundle's is missing (it stays the user's all the same).
|
|
146
146
|
def kept_own?(entry)
|
|
147
|
-
|
|
147
|
+
%i[would_skip kept_own].include?(entry.word)
|
|
148
148
|
end
|
|
149
149
|
private_class_method :kept_own?
|
|
150
150
|
|