samagotchi 0.53.0 → 0.55.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 +52 -1
- data/README.md +1 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +30 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +27 -6
- data/docs/internals/engine.md +55 -24
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -6
- data/lib/samagotchi/bundle_command.rb +31 -36
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +2 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +117 -134
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +170 -178
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +9 -253
- data/lib/samagotchi/session_metrics.rb +3 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +12 -2
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +26 -3
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +35 -18
- data/lib/samagotchi/web/public/app.js +240 -1136
- data/lib/samagotchi/web/public/commands.js +236 -0
- data/lib/samagotchi/web/public/composer.js +101 -0
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +9 -2
- data/lib/samagotchi/web/public/info_bar.js +78 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +7 -4
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +52 -0
- data/lib/samagotchi/web/public/stream_handlers.js +324 -0
- data/lib/samagotchi/web/public/thinking_chip.js +6 -7
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +15 -7
- data/lib/samagotchi/web/server.rb +4 -4
- metadata +17 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "bundle_hook"
|
|
6
|
+
require_relative "manifest"
|
|
7
|
+
require_relative "provenance"
|
|
8
|
+
require_relative "../version"
|
|
9
|
+
|
|
10
|
+
module Samagotchi
|
|
11
|
+
module MemoryBundle
|
|
12
|
+
autoload :Installer, File.expand_path("installer", __dir__)
|
|
13
|
+
|
|
14
|
+
# The install of a bundle's parts that live in its own dir
|
|
15
|
+
# (<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
|
|
18
|
+
# runs it after the memories; it adds to the installer's results and
|
|
19
|
+
# warnings, and #run returns what the record (Provenance#write) keeps.
|
|
20
|
+
class AssetInstaller
|
|
21
|
+
# What was installed: the paths Provenance#write records (each
|
|
22
|
+
# empty / nil on a dry run).
|
|
23
|
+
# hooks every hook basename the bundle ships
|
|
24
|
+
# hook_files, guardrail_files, script_files
|
|
25
|
+
# basename => installed path
|
|
26
|
+
# plugin_file the installed plugin file, nil without one
|
|
27
|
+
Installed = Data.define(:hooks, :hook_files, :guardrail_files, :plugin_file, :script_files)
|
|
28
|
+
|
|
29
|
+
# @param source_dir [String] the normalized bundle source
|
|
30
|
+
# @param manifest [Manifest, nil] nil for a non-strict install without one
|
|
31
|
+
# @param existing [InstalledBundle, nil] the record before this install
|
|
32
|
+
# @param results, warnings, conflicts the Installer's (results and
|
|
33
|
+
# warnings are added to)
|
|
34
|
+
def initialize(name:, source_dir:, manifest:, provenance:, existing:, results:, warnings:, conflicts:,
|
|
35
|
+
force: false, strict: true, upgrade: false, dry_run: false)
|
|
36
|
+
@name = name
|
|
37
|
+
@bundle_dir = source_dir
|
|
38
|
+
@manifest = manifest
|
|
39
|
+
@provenance = provenance
|
|
40
|
+
@existing = existing
|
|
41
|
+
@results = results
|
|
42
|
+
@warnings = warnings
|
|
43
|
+
@conflicts = conflicts
|
|
44
|
+
@force = force
|
|
45
|
+
@strict = strict
|
|
46
|
+
@upgrade = upgrade
|
|
47
|
+
@dry_run = dry_run
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Installed]
|
|
51
|
+
# @raise [Installer::InstallError] the manifest names a plugin or
|
|
52
|
+
# script the bundle doesn't have
|
|
53
|
+
def run
|
|
54
|
+
install_hooks
|
|
55
|
+
prune_dropped_hooks if @upgrade && @existing && !@dry_run
|
|
56
|
+
verify_hook_checksums if @manifest && @strict
|
|
57
|
+
incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
|
|
58
|
+
guardrail_files = install_guardrails(incoming_rules)
|
|
59
|
+
plugin_file = install_plugin
|
|
60
|
+
script_files = install_scripts
|
|
61
|
+
warn_hooks_requires_chi
|
|
62
|
+
warn_rules_requires_chi(incoming_rules)
|
|
63
|
+
Installed.new(hooks: @bundle_hooks, hook_files: @hook_files, guardrail_files: guardrail_files,
|
|
64
|
+
plugin_file: plugin_file, script_files: script_files)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Copies the bundle's hooks/*.rb into <bundle_dir>/hooks/: the ones
|
|
70
|
+
# its manifest lists, else (no hooks: in it) every hooks/*.rb, which is
|
|
71
|
+
# then added to manifest.hooks with no event (the loader skips it, the
|
|
72
|
+
# record keeps it). Without a manifest every hooks/*.rb is copied.
|
|
73
|
+
# Sets @bundle_hooks (every hook the bundle ships) and @hook_files
|
|
74
|
+
# (basename => installed path, for the record).
|
|
75
|
+
def install_hooks
|
|
76
|
+
@bundle_hooks = []
|
|
77
|
+
@hook_files = {}
|
|
78
|
+
return install_unlisted_hooks unless @manifest
|
|
79
|
+
|
|
80
|
+
hook_entries = @manifest.hooks
|
|
81
|
+
if hook_entries.empty?
|
|
82
|
+
Dir.glob(File.join(@bundle_dir, "hooks", "*.rb")).each { |p| hook_entries[File.basename(p)] = BundleHook.new }
|
|
83
|
+
end
|
|
84
|
+
hook_entries.each_key do |basename|
|
|
85
|
+
next unless basename.is_a?(String) && !basename.empty?
|
|
86
|
+
|
|
87
|
+
@bundle_hooks << basename
|
|
88
|
+
src = File.join(@bundle_dir, "hooks", basename)
|
|
89
|
+
install_hook(basename, src) if File.exist?(src)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# One listed hook; an upgrade overwrites a local edit with a warning.
|
|
94
|
+
def install_hook(basename, src)
|
|
95
|
+
dest = File.join(@provenance.hooks_dir, basename)
|
|
96
|
+
if @dry_run
|
|
97
|
+
@results[basename] = { status: File.exist?(dest) && !@force ? "would_skip" : "would_install" }
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
prev_sha = @existing&.hooks&.dig(basename)&.sha256
|
|
102
|
+
if @upgrade && File.exist?(dest) && !@force && !Provenance.recorded_sha(prev_sha).empty? && !Provenance.sha_matches?(dest, prev_sha)
|
|
103
|
+
@warnings << "Hook #{basename} was locally modified; overwriting"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
107
|
+
unchanged = File.exist?(dest) && FileUtils.identical?(src, dest)
|
|
108
|
+
FileUtils.cp(src, dest)
|
|
109
|
+
@results[basename] = if unchanged
|
|
110
|
+
{ status: "skipped", reason: "already up to date" }
|
|
111
|
+
elsif @upgrade && @existing
|
|
112
|
+
{ status: "updated" }
|
|
113
|
+
else
|
|
114
|
+
{ status: "installed" }
|
|
115
|
+
end
|
|
116
|
+
@hook_files[basename] = dest
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# No manifest (a non-strict install): every hooks/*.rb is copied.
|
|
120
|
+
def install_unlisted_hooks
|
|
121
|
+
Dir.glob(File.join(@bundle_dir, "hooks", "*.rb")).each do |src|
|
|
122
|
+
basename = File.basename(src)
|
|
123
|
+
@bundle_hooks << basename
|
|
124
|
+
if @dry_run
|
|
125
|
+
@results[basename] = { status: "would_install" }
|
|
126
|
+
else
|
|
127
|
+
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
128
|
+
dest = File.join(@provenance.hooks_dir, basename)
|
|
129
|
+
FileUtils.cp(src, dest)
|
|
130
|
+
@results[basename] = { status: "installed" }
|
|
131
|
+
@hook_files[basename] = dest
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# An upgrade removes the hooks the previous version shipped and this
|
|
137
|
+
# one doesn't (a local file too, with a warning).
|
|
138
|
+
def prune_dropped_hooks
|
|
139
|
+
@existing.hooks.each_key do |old_key_str|
|
|
140
|
+
next if @bundle_hooks.include?(old_key_str)
|
|
141
|
+
|
|
142
|
+
old_dest = File.join(@provenance.hooks_dir, old_key_str)
|
|
143
|
+
if File.exist?(old_dest) && !@force
|
|
144
|
+
@warnings << "Bundle no longer includes hook #{old_key_str} but local file exists — removing"
|
|
145
|
+
end
|
|
146
|
+
FileUtils.rm_f(old_dest)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Strict mode: an installed hook whose sha256 differs from the one its
|
|
151
|
+
# manifest declares warns (as verify_memory_checksums does).
|
|
152
|
+
def verify_hook_checksums
|
|
153
|
+
@bundle_hooks.each do |basename|
|
|
154
|
+
next if @conflicts.key?(basename)
|
|
155
|
+
|
|
156
|
+
expected = @manifest.checksum_for_hook(basename)
|
|
157
|
+
next unless expected
|
|
158
|
+
|
|
159
|
+
dest = File.join(@provenance.hooks_dir, basename)
|
|
160
|
+
next unless File.exist?(dest)
|
|
161
|
+
|
|
162
|
+
actual = Digest::SHA256.hexdigest(File.read(dest))
|
|
163
|
+
if actual != expected
|
|
164
|
+
@warnings << "Checksum mismatch for hook #{basename}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The bundle's guardrails/*.yml into <bundle_dir>/guardrails/: the
|
|
170
|
+
# set replaces what an earlier version installed. Same bytes as
|
|
171
|
+
# installed are reported up to date, not "Installed".
|
|
172
|
+
# @return [Hash{String => String}] basename => installed path ({} on a dry run)
|
|
173
|
+
def install_guardrails(incoming_rules)
|
|
174
|
+
if @dry_run
|
|
175
|
+
incoming_rules.each { |src| @results["guardrails/#{File.basename(src)}"] = { status: "would_install" } }
|
|
176
|
+
return {}
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
target = @provenance.guardrails_dir
|
|
180
|
+
unchanged = incoming_rules.select do |src|
|
|
181
|
+
dest = File.join(target, File.basename(src))
|
|
182
|
+
File.file?(dest) && FileUtils.identical?(src, dest)
|
|
183
|
+
end
|
|
184
|
+
FileUtils.rm_rf(target)
|
|
185
|
+
return {} if incoming_rules.empty?
|
|
186
|
+
|
|
187
|
+
FileUtils.mkdir_p(target)
|
|
188
|
+
incoming_rules.to_h do |src|
|
|
189
|
+
dest = File.join(target, File.basename(src))
|
|
190
|
+
FileUtils.cp(src, dest)
|
|
191
|
+
@results["guardrails/#{File.basename(src)}"] =
|
|
192
|
+
unchanged.include?(src) ? { status: "skipped", reason: "already up to date" } : { status: "installed" }
|
|
193
|
+
[File.basename(src), dest]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Copy the manifest's plugin file into the bundle's plugin/ dir,
|
|
198
|
+
# warning when its sha256 differs from the declared one or this chi
|
|
199
|
+
# doesn't meet requires_chi (the Engine then won't load it).
|
|
200
|
+
# @return [String, nil] the installed file (nil: none, or a dry run)
|
|
201
|
+
def install_plugin
|
|
202
|
+
plugin = @manifest&.plugin
|
|
203
|
+
unless plugin
|
|
204
|
+
FileUtils.rm_rf(@provenance.plugin_dir) unless @dry_run
|
|
205
|
+
return nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
src = File.join(@bundle_dir, plugin[:file])
|
|
209
|
+
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
210
|
+
|
|
211
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, Samagotchi::VERSION))
|
|
212
|
+
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
213
|
+
end
|
|
214
|
+
if @dry_run
|
|
215
|
+
@results[plugin[:file]] = { status: "would_install" }
|
|
216
|
+
return nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
dest = File.join(@provenance.plugin_dir, plugin[:file])
|
|
220
|
+
unchanged = File.file?(dest) && FileUtils.identical?(src, dest)
|
|
221
|
+
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
222
|
+
FileUtils.mkdir_p(@provenance.plugin_dir)
|
|
223
|
+
FileUtils.cp(src, dest)
|
|
224
|
+
@results[plugin[:file]] = if unchanged
|
|
225
|
+
{ status: "skipped", reason: "already up to date" }
|
|
226
|
+
elsif @upgrade && @provenance.installed?
|
|
227
|
+
{ status: "updated" }
|
|
228
|
+
else
|
|
229
|
+
{ status: "installed" }
|
|
230
|
+
end
|
|
231
|
+
expected = @manifest.checksum_for_plugin
|
|
232
|
+
actual = Digest::SHA256.hexdigest(File.binread(dest))
|
|
233
|
+
if @strict && expected && actual != expected
|
|
234
|
+
@warnings << "Checksum mismatch for plugin #{plugin[:file]}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
235
|
+
end
|
|
236
|
+
dest
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The manifest's scripts, copied into <bundle_dir>/scripts/ (the
|
|
240
|
+
# folder replaced: a script the new version dropped goes). A declared
|
|
241
|
+
# sha256 that differs warns, as a plugin's does.
|
|
242
|
+
# @return [Hash{String => String}] file name => installed path
|
|
243
|
+
def install_scripts
|
|
244
|
+
scripts = @manifest&.scripts || {}
|
|
245
|
+
scripts.each_key do |file|
|
|
246
|
+
next if File.file?(File.join(@bundle_dir, "scripts", file))
|
|
247
|
+
|
|
248
|
+
raise Installer::InstallError, "the manifest names scripts/#{file}, which the bundle doesn't have"
|
|
249
|
+
end
|
|
250
|
+
return {} if @dry_run
|
|
251
|
+
|
|
252
|
+
unchanged = scripts.keys.select do |file|
|
|
253
|
+
dest = File.join(@provenance.scripts_dir, file)
|
|
254
|
+
File.file?(dest) && FileUtils.identical?(File.join(@bundle_dir, "scripts", file), dest)
|
|
255
|
+
end
|
|
256
|
+
FileUtils.rm_rf(@provenance.scripts_dir)
|
|
257
|
+
return {} if scripts.empty?
|
|
258
|
+
|
|
259
|
+
FileUtils.mkdir_p(@provenance.scripts_dir)
|
|
260
|
+
scripts.to_h do |file, declared|
|
|
261
|
+
dest = File.join(@provenance.scripts_dir, file)
|
|
262
|
+
FileUtils.cp(File.join(@bundle_dir, "scripts", file), dest)
|
|
263
|
+
@results["scripts/#{file}"] = if unchanged.include?(file) then { status: "skipped", reason: "already up to date" }
|
|
264
|
+
elsif @upgrade && @provenance.installed? then { status: "updated" }
|
|
265
|
+
else { status: "installed" }
|
|
266
|
+
end
|
|
267
|
+
FileUtils.chmod(0o755, dest)
|
|
268
|
+
actual = Digest::SHA256.hexdigest(File.binread(dest))
|
|
269
|
+
expected = declared.to_s.delete_prefix("sha256:")
|
|
270
|
+
if @strict && !expected.empty? && actual != expected
|
|
271
|
+
@warnings << "Checksum mismatch for script #{file}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
272
|
+
end
|
|
273
|
+
[file, dest]
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The hook loader skips every hook of a bundle whose requires_chi this
|
|
278
|
+
# chi doesn't meet (Hooks::BundleLoader); say so at install, as
|
|
279
|
+
# install_plugin does for the plugin.
|
|
280
|
+
def warn_hooks_requires_chi
|
|
281
|
+
return if @manifest.nil? || @bundle_hooks.empty?
|
|
282
|
+
|
|
283
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, Samagotchi::VERSION))
|
|
284
|
+
@warnings << "Bundle #{@name}: its hooks won't load: #{failure}"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# GuardrailWiring#bundle_rules doesn't read the rules of a bundle whose
|
|
289
|
+
# requires_chi this chi doesn't meet, and a session that never loaded
|
|
290
|
+
# them records a required load failure (the gate refuses guarded calls
|
|
291
|
+
# until chi is updated): say so at install, as the hooks warning does.
|
|
292
|
+
def warn_rules_requires_chi(rule_files)
|
|
293
|
+
return if @manifest.nil? || rule_files.empty?
|
|
294
|
+
|
|
295
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, Samagotchi::VERSION))
|
|
296
|
+
@warnings << "Bundle #{@name}: its guardrail rules won't load: #{failure}; until chi is updated " \
|
|
297
|
+
"(chi update), guarded tool calls are refused"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|