samagotchi 0.52.0 → 0.54.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 +58 -1
- data/README.md +1 -1
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +1 -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 +8 -5
- data/docs/internals/engine.md +16 -7
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/releasing.md +6 -3
- data/docs/sessions.md +1 -1
- data/docs/testing.md +5 -3
- data/lib/samagotchi/analytics_file.rb +35 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -0
- data/lib/samagotchi/bundle_command.rb +35 -44
- 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 +0 -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 +132 -135
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -8
- data/lib/samagotchi/host_registry.rb +13 -0
- 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 +184 -190
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +51 -0
- 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 +9 -29
- 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 +46 -57
- 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 +4 -28
- data/lib/samagotchi/session_commands.rb +32 -4
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +45 -257
- data/lib/samagotchi/session_metrics.rb +18 -21
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -1
- data/lib/samagotchi/terminal_ui.rb +25 -3
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -8
- data/lib/samagotchi/web/public/activity.js +2 -0
- data/lib/samagotchi/web/public/app.js +6 -8
- data/lib/samagotchi/web/public/command_complete.js +7 -2
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/index.html +4 -2
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +5 -3
- data/lib/samagotchi/web/public/thinking_chip.js +31 -21
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_html.js +10 -6
- metadata +9 -1
|
@@ -151,26 +151,26 @@ module Samagotchi
|
|
|
151
151
|
def bundle_rules
|
|
152
152
|
require_relative "memory_bundle/provenance"
|
|
153
153
|
rules = []
|
|
154
|
-
MemoryBundle::Provenance.each_installed(holding: :guardrails) do |bundle_name,
|
|
155
|
-
if
|
|
156
|
-
Log.warn(:guardrails, "bundle_rules_invalid", echo: "[samagotchi:guardrails] bundle #{bundle_name}: #{
|
|
157
|
-
@failures.add("rules (bundle #{bundle_name})",
|
|
154
|
+
MemoryBundle::Provenance.each_installed(holding: :guardrails) do |bundle_name, bundle|
|
|
155
|
+
if bundle.error?
|
|
156
|
+
Log.warn(:guardrails, "bundle_rules_invalid", echo: "[samagotchi:guardrails] bundle #{bundle_name}: #{bundle.error}", bundle: bundle_name)
|
|
157
|
+
@failures.add("rules (bundle #{bundle_name})", bundle.error, required: true, group: :rules)
|
|
158
158
|
next
|
|
159
159
|
end
|
|
160
|
-
if (too_new = MemoryBundle::Manifest.requires_chi_failure(
|
|
161
|
-
rules.concat(too_new_bundle_rules(bundle_name,
|
|
160
|
+
if (too_new = MemoryBundle::Manifest.requires_chi_failure(bundle.requires_chi, Samagotchi::VERSION))
|
|
161
|
+
rules.concat(too_new_bundle_rules(bundle_name, bundle, too_new))
|
|
162
162
|
next
|
|
163
163
|
end
|
|
164
164
|
dir = MemoryBundle::Provenance.new(name: bundle_name).guardrails_dir
|
|
165
165
|
loaded = []
|
|
166
166
|
whole = true
|
|
167
|
-
|
|
167
|
+
bundle.guardrails.sort.each do |basename, sha|
|
|
168
168
|
what = "rules #{basename} (bundle #{bundle_name})"
|
|
169
|
-
path = File.join(dir, basename
|
|
169
|
+
path = File.join(dir, basename)
|
|
170
170
|
begin
|
|
171
171
|
raise Guardrails::Rules::ParseError, "the file is missing" unless File.file?(path)
|
|
172
172
|
|
|
173
|
-
unless MemoryBundle::Provenance.sha_matches?(path,
|
|
173
|
+
unless MemoryBundle::Provenance.sha_matches?(path, sha)
|
|
174
174
|
raise Guardrails::Rules::ParseError, "its sha256 differs from the installed one (edited after install? reinstall the bundle)"
|
|
175
175
|
end
|
|
176
176
|
|
|
@@ -256,7 +256,7 @@ module Samagotchi
|
|
|
256
256
|
# load the new ones). Never loaded (a chi older than the bundle it
|
|
257
257
|
# starts with): none, and a required load failure, as the hook loader
|
|
258
258
|
# does for a guardrail hook, so the calls aren't quietly unguarded.
|
|
259
|
-
def too_new_bundle_rules(bundle_name,
|
|
259
|
+
def too_new_bundle_rules(bundle_name, bundle, reason)
|
|
260
260
|
kept = @loaded_bundle_rules[bundle_name]
|
|
261
261
|
unless kept
|
|
262
262
|
Log.warn(:guardrails, "bundle_rules_requires_chi", echo: "[samagotchi:guardrails] bundle #{bundle_name}: its rules are not loaded: #{reason}",
|
|
@@ -266,19 +266,19 @@ module Samagotchi
|
|
|
266
266
|
return []
|
|
267
267
|
end
|
|
268
268
|
|
|
269
|
-
key = [bundle_name,
|
|
269
|
+
key = [bundle_name, bundle.version.to_s]
|
|
270
270
|
return kept if @kept_noticed[key]
|
|
271
271
|
|
|
272
272
|
@kept_noticed[key] = true
|
|
273
|
-
Log.warn(:guardrails, "bundle_rules_kept", bundle: bundle_name, version:
|
|
274
|
-
requires_chi:
|
|
275
|
-
@notify.call(kept_notice(bundle_name,
|
|
273
|
+
Log.warn(:guardrails, "bundle_rules_kept", bundle: bundle_name, version: bundle.version.to_s,
|
|
274
|
+
requires_chi: bundle.requires_chi.to_s)
|
|
275
|
+
@notify.call(kept_notice(bundle_name, bundle))
|
|
276
276
|
kept
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
-
def kept_notice(bundle_name,
|
|
279
|
+
def kept_notice(bundle_name, bundle)
|
|
280
280
|
id = @session_lookup.call&.id || "ID"
|
|
281
|
-
"bundle #{bundle_name} #{
|
|
281
|
+
"bundle #{bundle_name} #{bundle.version} needs chi #{bundle.requires_chi} and this session runs chi #{Samagotchi::VERSION}: " \
|
|
282
282
|
"its new rules are not loaded, the ones loaded before still apply. Restart the session to load them " \
|
|
283
283
|
"(chi sessions stop #{id}, then chi --resume #{id})"
|
|
284
284
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "registry"
|
|
4
4
|
require "digest"
|
|
5
5
|
require_relative "../log"
|
|
6
|
+
require_relative "../memory_bundle/bundle_hook"
|
|
6
7
|
require_relative "../memory_bundle/provenance"
|
|
7
8
|
require_relative "../memory_bundle/manifest"
|
|
8
9
|
require_relative "../version"
|
|
@@ -44,7 +45,8 @@ module Samagotchi
|
|
|
44
45
|
#
|
|
45
46
|
# @param bundle_name [String]
|
|
46
47
|
# @param hooks_dir [String] directory containing the bundle's .rb hook files
|
|
47
|
-
# @param metadata [Hash] basename => { event:, on_error:, priority
|
|
48
|
+
# @param metadata [Hash] basename => { event:, on_error:, priority:,
|
|
49
|
+
# sha256: } (MemoryBundle::BundleHook.parse reads it)
|
|
48
50
|
# @param registry [Samagotchi::Hooks::Registry]
|
|
49
51
|
# @param failures [Guardrails::LoadFailures, nil] collects hooks that
|
|
50
52
|
# failed to load
|
|
@@ -64,14 +66,13 @@ module Samagotchi
|
|
|
64
66
|
basename = raw_basename.to_s
|
|
65
67
|
next if basename.empty?
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
event = meta.call(:event).to_s
|
|
69
|
+
hook = MemoryBundle::BundleHook.parse(raw_meta)
|
|
69
70
|
# A hook with no declared event cannot be auto-registered.
|
|
70
|
-
next if event.empty?
|
|
71
|
+
next if hook.event.empty?
|
|
71
72
|
|
|
72
|
-
event_sym = event.to_sym
|
|
73
|
-
on_error =
|
|
74
|
-
priority =
|
|
73
|
+
event_sym = hook.event.to_sym
|
|
74
|
+
on_error = hook.on_error
|
|
75
|
+
priority = hook.priority
|
|
75
76
|
fail_closed = on_error == "fail_closed" && event_sym == :before_tool_call
|
|
76
77
|
what = "hook #{basename} (bundle #{bundle_name})"
|
|
77
78
|
file = hooks_dir && File.join(hooks_dir, basename)
|
|
@@ -79,7 +80,7 @@ module Samagotchi
|
|
|
79
80
|
failures&.add(what, "the file is missing", required: true) if fail_closed
|
|
80
81
|
next
|
|
81
82
|
end
|
|
82
|
-
if (mismatch = too_old || sha_mismatch(file,
|
|
83
|
+
if (mismatch = too_old || sha_mismatch(file, hook.sha256, required: fail_closed))
|
|
83
84
|
Log.warn(:hooks, "bundle_hook_mismatch", echo: "[samagotchi:hooks] bundle '#{bundle_name}' hook '#{basename}' not loaded: #{mismatch}", bundle: bundle_name, hook: basename.to_s)
|
|
84
85
|
failures&.add(what, mismatch, required: fail_closed)
|
|
85
86
|
next
|
|
@@ -273,6 +273,19 @@ module Samagotchi
|
|
|
273
273
|
ModelTarget.new(model: raw_model, entry: entry, bare_model: bare, client: client_for(entry))
|
|
274
274
|
end
|
|
275
275
|
|
|
276
|
+
# What the alias +raw_model+ became, for an error that names only the
|
|
277
|
+
# host ("splash is an alias: gemma-x on host main"), or nil when it
|
|
278
|
+
# isn't an alias. An alias without a host of its own goes where a bare
|
|
279
|
+
# id goes (#host_for_model), often the default host the user never typed.
|
|
280
|
+
# @return [String, nil]
|
|
281
|
+
def alias_note(raw_model)
|
|
282
|
+
ref = model_ref(raw_model)
|
|
283
|
+
return nil unless ref.alias_name
|
|
284
|
+
|
|
285
|
+
target = resolve(raw_model)
|
|
286
|
+
"#{ref.alias_name} is an alias: #{target.bare_model} on host #{target.entry.name}"
|
|
287
|
+
end
|
|
288
|
+
|
|
276
289
|
# The model id sent for +full_ref+: its alias applied, a known host
|
|
277
290
|
# prefix stripped ("box:gemma" → "gemma").
|
|
278
291
|
def bare_name(full_ref)
|
|
@@ -726,9 +726,11 @@ module Samagotchi
|
|
|
726
726
|
end
|
|
727
727
|
|
|
728
728
|
# Estimate context usage for this iteration's prompt (ContextStatus#observe)
|
|
729
|
-
# and
|
|
730
|
-
#
|
|
731
|
-
#
|
|
729
|
+
# and surface it to stream consumers as a :context_status event, on every
|
|
730
|
+
# request (observe returns the estimate each time it is enabled), so the
|
|
731
|
+
# web and the attached TUI get a fresh number per request. The telemetry
|
|
732
|
+
# is not for the model (it used to be injected as a synthetic system
|
|
733
|
+
# message); the model's own line is still left only on a rise.
|
|
732
734
|
def emit_context_status_event(on_stream_event, context, prompt, iteration_index:, window:, image_tokens: 0)
|
|
733
735
|
event = context.observe(prompt.length, iteration_index: iteration_index, window: window, image_tokens: image_tokens)
|
|
734
736
|
return unless event
|
|
@@ -651,10 +651,11 @@ module Samagotchi
|
|
|
651
651
|
|
|
652
652
|
# Before a request, as the native loop: estimate how full the window
|
|
653
653
|
# is (the server's last count plus what the turn appended since),
|
|
654
|
-
# emit :context_status on
|
|
655
|
-
#
|
|
656
|
-
#
|
|
657
|
-
#
|
|
654
|
+
# emit :context_status on every request (observe returns the estimate
|
|
655
|
+
# each time it is enabled), and put the model's own line on the tail
|
|
656
|
+
# on a rise that asks it for a change. The estimate counts the
|
|
657
|
+
# conversation's text and tool calls, not the tool schemas (the
|
|
658
|
+
# server's count does).
|
|
658
659
|
def observe_context(iteration, window)
|
|
659
660
|
return unless window
|
|
660
661
|
|
|
@@ -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
|