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
|
@@ -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:
|
|
17
|
-
#
|
|
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#
|
|
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
|
-
#
|
|
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(:
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
root = File.realpath(bundle_dir)
|
|
87
|
+
!File.realpath(src).start_with?("#{root}/")
|
|
88
|
+
rescue SystemCallError
|
|
89
|
+
true
|
|
105
90
|
end
|
|
106
91
|
|
|
107
|
-
|
|
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
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
109
|
+
return if @dry_run
|
|
153
110
|
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]] =
|
|
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]] =
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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}"] =
|
|
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
|
-
|
|
298
|
-
|
|
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
|
-
#
|
|
320
|
-
|
|
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
|
|
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
|
|
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, :
|
|
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
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
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,
|
|
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,
|
|
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
|
|
8
|
-
#
|
|
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.
|
|
12
|
-
#
|
|
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
|