samagotchi 0.87.0 → 0.89.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 +35 -1
- data/docs/bundle-authoring.md +19 -0
- data/docs/cli.md +44 -5
- data/docs/guardrails.md +6 -1
- data/docs/internals/bundles.md +10 -0
- data/docs/sessions.md +4 -4
- data/docs/sub-agent.md +9 -0
- data/lib/samagotchi/bundle_command/test_run.rb +2 -2
- data/lib/samagotchi/bundle_command/try/run.rb +162 -0
- data/lib/samagotchi/bundle_command/try/sandbox.rb +159 -0
- data/lib/samagotchi/bundle_command/try/trace.rb +152 -0
- data/lib/samagotchi/bundle_command.rb +216 -10
- data/lib/samagotchi/bundle_session_settings.rb +2 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +100 -7
- data/lib/samagotchi/context_providers.rb +1 -2
- data/lib/samagotchi/guardrails/parent_policy.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +2 -0
- data/lib/samagotchi/installed_gem.rb +11 -0
- data/lib/samagotchi/memory_bundle/asset_installer.rb +25 -13
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +6 -3
- data/lib/samagotchi/memory_bundle/installer.rb +22 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +21 -6
- data/lib/samagotchi/relay.rb +34 -2
- data/lib/samagotchi/relay_desk.rb +13 -0
- data/lib/samagotchi/session_manager.rb +2 -10
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +1 -1
- data/lib/samagotchi/terminal_ui/question_prompt.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +69 -15
- data/lib/samagotchi/tools/delegate_wait.rb +3 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/notify.js +6 -2
- data/lib/samagotchi/web/public/question_card.js +2 -0
- data/lib/samagotchi/web/public/question_cards.js +25 -18
- data/lib/samagotchi/worker.rb +51 -9
- metadata +24 -1
|
@@ -134,8 +134,7 @@ module Samagotchi
|
|
|
134
134
|
|
|
135
135
|
record.scripts.each do |file, sha|
|
|
136
136
|
path = File.join(provenance.scripts_dir, file)
|
|
137
|
-
|
|
138
|
-
next if File.file?(path) && MemoryBundle::Provenance.file_sha(path) == recorded
|
|
137
|
+
next if File.file?(path) && MemoryBundle::Provenance.sha_matches?(path, sha)
|
|
139
138
|
|
|
140
139
|
raise Invalid, "bundle #{source.provider}'s scripts/#{file} differs from the installed one (reinstall the bundle)"
|
|
141
140
|
end
|
|
@@ -105,6 +105,13 @@ module Samagotchi
|
|
|
105
105
|
STRATEGIES.key?(word) ? word : HARD
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Whether a session's own value is set but not a level (a hand-edited
|
|
109
|
+
# file: read as hard); the worker says so once when it loads.
|
|
110
|
+
def unknown_level?(value)
|
|
111
|
+
word = value.to_s.strip.downcase
|
|
112
|
+
!word.empty? && word != UNSET && !STRATEGIES.key?(word)
|
|
113
|
+
end
|
|
114
|
+
|
|
108
115
|
# What a parent may answer on a session whose own value is
|
|
109
116
|
# +session_value+: config.yml's (+config+) unless the session's is
|
|
110
117
|
# stricter. Config's own object when it binds, so its label stays.
|
|
@@ -243,6 +250,19 @@ module Samagotchi
|
|
|
243
250
|
end
|
|
244
251
|
end
|
|
245
252
|
|
|
253
|
+
# Whether a parent may usefully answer +pending+ under +strategy+:
|
|
254
|
+
# some option it may pick is not a deny (free text alone is a deny
|
|
255
|
+
# with a reason). A delegate's question it may is reported to the
|
|
256
|
+
# parent's model; one it may not goes to the parent's user
|
|
257
|
+
# (Tools::DelegateRelay.relay?). Who may answer what stays #refusal's.
|
|
258
|
+
def parent_decides?(pending, strategy:)
|
|
259
|
+
deny = deny_labels(pending)
|
|
260
|
+
allowed_options(pending, strategy: strategy).any? { |label| !deny.include?(label) }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The deny-tagged options' labels.
|
|
264
|
+
def deny_labels(pending) = Array(pending.options).zip(effects_of(pending)).filter_map { |label, tag| label.to_s if tag == DENY }
|
|
265
|
+
|
|
246
266
|
# The question's deny-tagged option's label, or nil.
|
|
247
267
|
def deny_label(pending)
|
|
248
268
|
index = effects_of(pending).index(DENY)
|
|
@@ -126,6 +126,8 @@ module Samagotchi
|
|
|
126
126
|
# Why the file doesn't match the sha256 recorded at install, or nil.
|
|
127
127
|
# With none recorded, only a required hook counts as a mismatch.
|
|
128
128
|
def sha_mismatch(file, recorded, required:)
|
|
129
|
+
return nil if MemoryBundle::Provenance.unchecked?(recorded)
|
|
130
|
+
|
|
129
131
|
expected = MemoryBundle::Provenance.recorded_sha(recorded)
|
|
130
132
|
return (required ? "no sha256 recorded for it (reinstall the bundle)" : nil) if expected.empty?
|
|
131
133
|
|
|
@@ -29,6 +29,17 @@ module Samagotchi
|
|
|
29
29
|
File.file?(path) ? path : nil
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# What Bundler changed in ENV, mapped back to the values from before it
|
|
33
|
+
# (nil: unset); {} outside Bundler. Child chi commands use this in gem
|
|
34
|
+
# mode so they load the same installed gem without inheriting this app's
|
|
35
|
+
# bundle.
|
|
36
|
+
def unbundled_env_changes(env = ENV.to_h)
|
|
37
|
+
return {} unless defined?(::Bundler) && ::Bundler.respond_to?(:unbundled_env)
|
|
38
|
+
|
|
39
|
+
clean = ::Bundler.unbundled_env
|
|
40
|
+
(env.keys | clean.keys).each_with_object({}) { |key, changes| changes[key] = clean[key] unless clean[key] == env[key] }
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def real_path(path)
|
|
33
44
|
File.realpath(path)
|
|
34
45
|
rescue SystemCallError
|
|
@@ -17,6 +17,8 @@ module Samagotchi
|
|
|
17
17
|
# scripts/*. Each replaces what an earlier version installed. Installer
|
|
18
18
|
# runs it after the memories; it adds to the installer's results and
|
|
19
19
|
# warnings, and #run returns what the record (Provenance#write) keeps.
|
|
20
|
+
# A dev install (+dev+, `chi bundle install --dev`) links each file to
|
|
21
|
+
# the source instead of copying it, and warns of no checksum mismatch.
|
|
20
22
|
class AssetInstaller
|
|
21
23
|
# What was installed: the paths Provenance#write records (each
|
|
22
24
|
# empty / nil on a dry run).
|
|
@@ -32,7 +34,7 @@ module Samagotchi
|
|
|
32
34
|
# @param results, warnings, conflicts the Installer's (results and
|
|
33
35
|
# warnings are added to)
|
|
34
36
|
def initialize(name:, source_dir:, manifest:, provenance:, existing:, results:, warnings:, conflicts:,
|
|
35
|
-
force: false, strict: true, upgrade: false, dry_run: false)
|
|
37
|
+
force: false, strict: true, upgrade: false, dry_run: false, dev: false)
|
|
36
38
|
@name = name
|
|
37
39
|
@bundle_dir = source_dir
|
|
38
40
|
@manifest = manifest
|
|
@@ -45,6 +47,7 @@ module Samagotchi
|
|
|
45
47
|
@strict = strict
|
|
46
48
|
@upgrade = upgrade
|
|
47
49
|
@dry_run = dry_run
|
|
50
|
+
@dev = dev
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# @return [Installed]
|
|
@@ -52,8 +55,8 @@ module Samagotchi
|
|
|
52
55
|
# script the bundle doesn't have
|
|
53
56
|
def run
|
|
54
57
|
install_hooks
|
|
55
|
-
prune_dropped_hooks if @upgrade && @existing && !@dry_run
|
|
56
|
-
verify_hook_checksums if @manifest && @strict
|
|
58
|
+
prune_dropped_hooks if (@upgrade || (@dev && @existing&.dev)) && @existing && !@dry_run
|
|
59
|
+
verify_hook_checksums if @manifest && @strict && !@dev
|
|
57
60
|
incoming_rules = Dir.glob(File.join(@bundle_dir, "guardrails", "*.yml")).sort
|
|
58
61
|
.reject { |src| linked_out?(src, "guardrails/#{File.basename(src)}") }
|
|
59
62
|
guardrail_files = install_guardrails(incoming_rules)
|
|
@@ -109,7 +112,7 @@ module Samagotchi
|
|
|
109
112
|
|
|
110
113
|
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
111
114
|
unchanged = File.exist?(dest) && FileUtils.identical?(src, dest)
|
|
112
|
-
|
|
115
|
+
place(src, dest)
|
|
113
116
|
@results[basename] = if unchanged
|
|
114
117
|
{ status: "skipped", reason: "already up to date" }
|
|
115
118
|
elsif @upgrade && @existing
|
|
@@ -132,7 +135,7 @@ module Samagotchi
|
|
|
132
135
|
else
|
|
133
136
|
FileUtils.mkdir_p(@provenance.hooks_dir)
|
|
134
137
|
dest = File.join(@provenance.hooks_dir, basename)
|
|
135
|
-
|
|
138
|
+
place(src, dest)
|
|
136
139
|
@results[basename] = { status: "installed" }
|
|
137
140
|
@hook_files[basename] = dest
|
|
138
141
|
end
|
|
@@ -140,13 +143,13 @@ module Samagotchi
|
|
|
140
143
|
end
|
|
141
144
|
|
|
142
145
|
# An upgrade removes the hooks the previous version shipped and this
|
|
143
|
-
# one doesn't (a local file too, with a warning).
|
|
146
|
+
# one doesn't (a local file too, with a warning; a dev link silently).
|
|
144
147
|
def prune_dropped_hooks
|
|
145
148
|
@existing.hooks.each_key do |old_key_str|
|
|
146
149
|
next if @bundle_hooks.include?(old_key_str)
|
|
147
150
|
|
|
148
151
|
old_dest = File.join(@provenance.hooks_dir, old_key_str)
|
|
149
|
-
if File.exist?(old_dest) && !@force
|
|
152
|
+
if File.exist?(old_dest) && !@force && !File.symlink?(old_dest)
|
|
150
153
|
@warnings << "Bundle no longer includes hook #{old_key_str} but local file exists — removing"
|
|
151
154
|
end
|
|
152
155
|
FileUtils.rm_f(old_dest)
|
|
@@ -193,7 +196,7 @@ module Samagotchi
|
|
|
193
196
|
FileUtils.mkdir_p(target)
|
|
194
197
|
incoming_rules.to_h do |src|
|
|
195
198
|
dest = File.join(target, File.basename(src))
|
|
196
|
-
|
|
199
|
+
place(src, dest)
|
|
197
200
|
@results["guardrails/#{File.basename(src)}"] =
|
|
198
201
|
unchanged.include?(src) ? { status: "skipped", reason: "already up to date" } : { status: "installed" }
|
|
199
202
|
[File.basename(src), dest]
|
|
@@ -227,7 +230,7 @@ module Samagotchi
|
|
|
227
230
|
unchanged = File.file?(dest) && FileUtils.identical?(src, dest)
|
|
228
231
|
FileUtils.rm_rf(@provenance.plugin_dir)
|
|
229
232
|
FileUtils.mkdir_p(@provenance.plugin_dir)
|
|
230
|
-
|
|
233
|
+
place(src, dest)
|
|
231
234
|
@results[plugin[:file]] = if unchanged
|
|
232
235
|
{ status: "skipped", reason: "already up to date" }
|
|
233
236
|
elsif @upgrade && @provenance.installed?
|
|
@@ -237,7 +240,7 @@ module Samagotchi
|
|
|
237
240
|
end
|
|
238
241
|
expected = @manifest.checksum_for_plugin
|
|
239
242
|
actual = Digest::SHA256.hexdigest(File.binread(dest))
|
|
240
|
-
if @strict && expected && actual != expected
|
|
243
|
+
if @strict && !@dev && expected && actual != expected
|
|
241
244
|
@warnings << "Checksum mismatch for plugin #{plugin[:file]}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
242
245
|
end
|
|
243
246
|
dest
|
|
@@ -253,6 +256,8 @@ module Samagotchi
|
|
|
253
256
|
src = File.join(@bundle_dir, "scripts", file)
|
|
254
257
|
raise Installer::InstallError, "the manifest names scripts/#{file}, which the bundle doesn't have" unless File.file?(src)
|
|
255
258
|
raise Installer::InstallError, "scripts/#{file} is a symlink (not installed)" if linked?(src)
|
|
259
|
+
|
|
260
|
+
@warnings << "scripts/#{file} is not executable; chmod +x #{src}" if @dev && !File.executable?(src)
|
|
256
261
|
end
|
|
257
262
|
return {} if @dry_run
|
|
258
263
|
|
|
@@ -266,21 +271,28 @@ module Samagotchi
|
|
|
266
271
|
FileUtils.mkdir_p(@provenance.scripts_dir)
|
|
267
272
|
scripts.to_h do |file, declared|
|
|
268
273
|
dest = File.join(@provenance.scripts_dir, file)
|
|
269
|
-
|
|
274
|
+
place(File.join(@bundle_dir, "scripts", file), dest)
|
|
270
275
|
@results["scripts/#{file}"] = if unchanged.include?(file) then { status: "skipped", reason: "already up to date" }
|
|
271
276
|
elsif @upgrade && @provenance.installed? then { status: "updated" }
|
|
272
277
|
else { status: "installed" }
|
|
273
278
|
end
|
|
274
|
-
FileUtils.chmod(0o755, dest)
|
|
279
|
+
FileUtils.chmod(0o755, dest) unless @dev
|
|
275
280
|
actual = Digest::SHA256.hexdigest(File.binread(dest))
|
|
276
281
|
expected = declared.to_s.delete_prefix("sha256:")
|
|
277
|
-
if @strict && !expected.empty? && actual != expected
|
|
282
|
+
if @strict && !@dev && !expected.empty? && actual != expected
|
|
278
283
|
@warnings << "Checksum mismatch for script #{file}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
279
284
|
end
|
|
280
285
|
[file, dest]
|
|
281
286
|
end
|
|
282
287
|
end
|
|
283
288
|
|
|
289
|
+
# Copies +src+ to +dest+, or (a dev install) links +dest+ to it. An
|
|
290
|
+
# existing link goes first: a copy over it would write into the source.
|
|
291
|
+
def place(src, dest)
|
|
292
|
+
FileUtils.rm_f(dest) if File.symlink?(dest)
|
|
293
|
+
@dev ? File.symlink(File.realpath(src), dest) : FileUtils.cp(src, dest)
|
|
294
|
+
end
|
|
295
|
+
|
|
284
296
|
# Whether the bundle file +src+ reads through a symlink: it is one, or
|
|
285
297
|
# it (or its dir, hooks/ say) resolves outside the bundle dir. A tar,
|
|
286
298
|
# zip or git repo keeps links; copying through one would install any
|
|
@@ -22,13 +22,15 @@ module Samagotchi
|
|
|
22
22
|
# includes a profile's recorded members, nil for a plain bundle
|
|
23
23
|
# session_settings
|
|
24
24
|
# the keys a session may set (chi --set), [] when none
|
|
25
|
+
# dev a dev install (`chi bundle install --dev`): its source is
|
|
26
|
+
# the folder it links to and its files carry Provenance::DEV_SHA
|
|
25
27
|
# error why manifest.json couldn't be read (the rest empty), else nil
|
|
26
28
|
class InstalledBundle < Data.define(:name, :version, :scope, :source, :installed_at, :trust_level, :source_commit,
|
|
27
29
|
:requires_chi, :files, :hooks, :guardrails, :scripts, :plugin, :needs,
|
|
28
|
-
:includes, :context_providers, :session_settings, :error)
|
|
30
|
+
:includes, :context_providers, :session_settings, :dev, :error)
|
|
29
31
|
OPTIONAL = { version: nil, scope: nil, source: nil, installed_at: nil, trust_level: nil, source_commit: nil,
|
|
30
32
|
requires_chi: nil, files: {}, hooks: {}, guardrails: {}, scripts: {}, plugin: nil, needs: nil,
|
|
31
|
-
includes: nil, context_providers: nil, session_settings: [], error: nil }.freeze
|
|
33
|
+
includes: nil, context_providers: nil, session_settings: [], dev: false, error: nil }.freeze
|
|
32
34
|
|
|
33
35
|
def initialize(name:, **fields)
|
|
34
36
|
super(name: name, **OPTIONAL, **fields)
|
|
@@ -60,7 +62,8 @@ module Samagotchi
|
|
|
60
62
|
scripts: mapping(h["scripts"]) { |meta| sha(meta) },
|
|
61
63
|
plugin: PluginRef.parse(h["plugin"]), needs: h["needs"], context_providers: h["context_providers"],
|
|
62
64
|
includes: h["includes"].is_a?(Array) ? h["includes"].map(&:to_s) : nil,
|
|
63
|
-
session_settings: h["session_settings"].is_a?(Array) ? h["session_settings"].map(&:to_s) : []
|
|
65
|
+
session_settings: h["session_settings"].is_a?(Array) ? h["session_settings"].map(&:to_s) : [],
|
|
66
|
+
dev: h["dev"] == true)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def self.string(value) = value&.to_s
|
|
@@ -40,7 +40,7 @@ module Samagotchi
|
|
|
40
40
|
# installs from a dir in it only, unless force); ShippedUpdate,
|
|
41
41
|
# Profile and SystemBundle pass the one they install from.
|
|
42
42
|
def initialize(source:, name:, scope: nil, force: false, strict: true, upgrade: false, dry_run: false, prepared: nil,
|
|
43
|
-
shipped_dir: SourceNormalizer::SHIPPED_DIR)
|
|
43
|
+
shipped_dir: SourceNormalizer::SHIPPED_DIR, dev: false)
|
|
44
44
|
@source = source
|
|
45
45
|
@shipped_dir = shipped_dir
|
|
46
46
|
@prepared = prepared
|
|
@@ -50,6 +50,7 @@ module Samagotchi
|
|
|
50
50
|
@strict = strict
|
|
51
51
|
@upgrade = upgrade
|
|
52
52
|
@dry_run = dry_run
|
|
53
|
+
@dev = dev
|
|
53
54
|
@results = {}
|
|
54
55
|
@warnings = []
|
|
55
56
|
@placeholder_warnings = []
|
|
@@ -98,7 +99,7 @@ module Samagotchi
|
|
|
98
99
|
installed = AssetInstaller.new(
|
|
99
100
|
name: @name, source_dir: @bundle_dir, manifest: @manifest, provenance: @provenance, existing: @existing,
|
|
100
101
|
results: @results, warnings: @warnings, conflicts: @conflicts,
|
|
101
|
-
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run
|
|
102
|
+
force: @force, strict: @strict, upgrade: @upgrade, dry_run: @dry_run, dev: @dev
|
|
102
103
|
).run
|
|
103
104
|
|
|
104
105
|
# Files the previous version shipped and this one doesn't.
|
|
@@ -172,10 +173,14 @@ module Samagotchi
|
|
|
172
173
|
def check_source
|
|
173
174
|
refuse_case_twin
|
|
174
175
|
refuse_shipped_name
|
|
176
|
+
refuse_dev_mismatch
|
|
175
177
|
now = incoming_identity
|
|
176
178
|
return if now.nil?
|
|
177
179
|
|
|
178
180
|
was = @existing && SourceIdentity.of(@existing.source)
|
|
181
|
+
if @dev && @existing&.dev && was != now
|
|
182
|
+
raise InstallError, "#{@name} is a dev install of #{@existing.source}; uninstall it before installing from #{@source}"
|
|
183
|
+
end
|
|
179
184
|
return if was.nil? || was == now
|
|
180
185
|
return @source_change = [@existing.source, @source.to_s] if @upgrade
|
|
181
186
|
|
|
@@ -191,6 +196,19 @@ module Samagotchi
|
|
|
191
196
|
raise InstallError, "#{@name} differs only in case from the installed #{twin}" if twin
|
|
192
197
|
end
|
|
193
198
|
|
|
199
|
+
# A dev install (--dev, linked to its source) and a normal one don't
|
|
200
|
+
# replace each other: neither an upgrade nor a plain install over a
|
|
201
|
+
# dev bundle, nor --dev over a normal one.
|
|
202
|
+
def refuse_dev_mismatch
|
|
203
|
+
return if @existing.nil? || @existing.dev == @dev
|
|
204
|
+
|
|
205
|
+
if @existing.dev
|
|
206
|
+
raise InstallError, "#{@name} is a dev install of #{@existing.source}: it follows its source already " \
|
|
207
|
+
"(to leave dev mode: chi bundle uninstall #{@name}, then chi bundle install without --dev)"
|
|
208
|
+
end
|
|
209
|
+
raise InstallError, "#{@name} is installed normally; `chi bundle uninstall #{@name}` first, then chi bundle install --dev"
|
|
210
|
+
end
|
|
211
|
+
|
|
194
212
|
# A shipped name (in any case) from anything but this chi's own
|
|
195
213
|
# shipped dir. A recorded source may be an older gem's (SourceIdentity
|
|
196
214
|
# matches the path); an incoming one must be a dir in SHIPPED_DIR, so
|
|
@@ -421,7 +439,8 @@ module Samagotchi
|
|
|
421
439
|
scripts_files: installed.script_files,
|
|
422
440
|
context_providers: @manifest.context_providers,
|
|
423
441
|
file_descriptions: @manifest.file_descriptions,
|
|
424
|
-
session_settings: @manifest.session_settings
|
|
442
|
+
session_settings: @manifest.session_settings,
|
|
443
|
+
dev: @dev
|
|
425
444
|
)
|
|
426
445
|
end
|
|
427
446
|
|
|
@@ -116,9 +116,20 @@ module Samagotchi
|
|
|
116
116
|
Digest::SHA256.hexdigest(File.binread(path))
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
#
|
|
119
|
+
# What a dev install (`chi bundle install --dev`) records instead of
|
|
120
|
+
# a sha256: its files are links to the source, which change under it.
|
|
121
|
+
# Not a hex digest, so no file can be mistaken for it.
|
|
122
|
+
DEV_SHA = "sha256:dev"
|
|
123
|
+
|
|
124
|
+
# Whether +recorded+ is DEV_SHA: the file isn't checked.
|
|
125
|
+
def self.unchecked?(recorded)
|
|
126
|
+
recorded_sha(recorded) == recorded_sha(DEV_SHA)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Whether the file's content is the one recorded (either format); a
|
|
130
|
+
# dev install's (DEV_SHA) always is.
|
|
120
131
|
def self.sha_matches?(path, recorded)
|
|
121
|
-
file_sha(path) == recorded_sha(recorded)
|
|
132
|
+
unchecked?(recorded) || file_sha(path) == recorded_sha(recorded)
|
|
122
133
|
end
|
|
123
134
|
|
|
124
135
|
# Writes provenance for an installation: merges into existing manifest.json
|
|
@@ -139,9 +150,11 @@ module Samagotchi
|
|
|
139
150
|
# @param scripts_files [Hash] file name => installed script; the
|
|
140
151
|
# sha256 of each is recorded (ContextFetch checks it before a run)
|
|
141
152
|
# @param context_providers [Array<ContextProviders::Provider>]
|
|
153
|
+
# @param dev [Boolean] a dev install: its files are links to the
|
|
154
|
+
# source, recorded as DEV_SHA (no sha256 check) with "dev": true
|
|
142
155
|
def write(files:, scope:, version:, source_path:, hooks: {}, trust_level: nil, source_commit: nil, hooks_files: {},
|
|
143
156
|
guardrails_files: {}, plugin_file: nil, requires_chi: nil, needs: nil, conflicts: [], includes: nil,
|
|
144
|
-
scripts_files: {}, context_providers: [], file_descriptions: {}, session_settings: [])
|
|
157
|
+
scripts_files: {}, context_providers: [], file_descriptions: {}, session_settings: [], dev: false)
|
|
145
158
|
FileUtils.mkdir_p(@bundle_dir)
|
|
146
159
|
bases_dir = File.join(@bundle_dir, "bases")
|
|
147
160
|
FileUtils.mkdir_p(bases_dir)
|
|
@@ -196,6 +209,7 @@ module Samagotchi
|
|
|
196
209
|
normalized_hooks.each do |k, hook|
|
|
197
210
|
src = hooks_files[k] || hooks_files[k.to_sym]
|
|
198
211
|
hook = hook.with(sha256: BundleHook.of_file(src.to_s).sha256) if src && File.exist?(src.to_s)
|
|
212
|
+
hook = hook.with(sha256: DEV_SHA) if dev
|
|
199
213
|
hooks_map[k] = hook.to_record
|
|
200
214
|
next unless src && File.exist?(src.to_s)
|
|
201
215
|
|
|
@@ -216,9 +230,10 @@ module Samagotchi
|
|
|
216
230
|
"hooks" => hooks_map
|
|
217
231
|
}
|
|
218
232
|
manifest_data["trust_level"] = trust_level.to_s if trust_level && !trust_level.to_s.empty?
|
|
233
|
+
manifest_data["dev"] = true if dev
|
|
219
234
|
if guardrails_files.is_a?(Hash) && !guardrails_files.empty?
|
|
220
235
|
manifest_data["guardrails"] = guardrails_files.sort.to_h do |basename, path|
|
|
221
|
-
[basename.to_s, { "sha256" => "sha256:#{Digest::SHA256.hexdigest(File.read(path.to_s))}" }]
|
|
236
|
+
[basename.to_s, { "sha256" => dev ? DEV_SHA : "sha256:#{Digest::SHA256.hexdigest(File.read(path.to_s))}" }]
|
|
222
237
|
end
|
|
223
238
|
end
|
|
224
239
|
manifest_data["source_commit"] = source_commit.to_s if source_commit && !source_commit.to_s.empty?
|
|
@@ -226,7 +241,7 @@ module Samagotchi
|
|
|
226
241
|
if plugin_file
|
|
227
242
|
content = File.binread(plugin_file.to_s)
|
|
228
243
|
manifest_data["plugin"] = { "file" => File.basename(plugin_file.to_s),
|
|
229
|
-
"sha256" => "sha256:#{Digest::SHA256.hexdigest(content)}" }
|
|
244
|
+
"sha256" => dev ? DEV_SHA : "sha256:#{Digest::SHA256.hexdigest(content)}" }
|
|
230
245
|
FileUtils.mkdir_p(File.join(bases_dir, "plugin"))
|
|
231
246
|
File.binwrite(plugin_base_path(File.basename(plugin_file.to_s)), content)
|
|
232
247
|
end
|
|
@@ -235,7 +250,7 @@ module Samagotchi
|
|
|
235
250
|
manifest_data["includes"] = includes.map(&:to_s) if includes.is_a?(Array)
|
|
236
251
|
unless scripts_files.empty?
|
|
237
252
|
manifest_data["scripts"] = scripts_files.sort.to_h do |file, path|
|
|
238
|
-
[file.to_s, { "sha256" => "sha256:#{Digest::SHA256.hexdigest(File.binread(path.to_s))}" }]
|
|
253
|
+
[file.to_s, { "sha256" => dev ? DEV_SHA : "sha256:#{Digest::SHA256.hexdigest(File.binread(path.to_s))}" }]
|
|
239
254
|
end
|
|
240
255
|
end
|
|
241
256
|
manifest_data["context_providers"] = context_providers.map(&:to_h) unless context_providers.empty?
|
data/lib/samagotchi/relay.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Samagotchi
|
|
|
16
16
|
# How much of the child's task the card keeps, and the question line.
|
|
17
17
|
TASK_CHARS = 200
|
|
18
18
|
TASK_LINE_CHARS = 80
|
|
19
|
+
# A hook's question (QuestionDesk#ask_for_hook), relayed as such (P4).
|
|
20
|
+
HOOK_KIND = "hook"
|
|
19
21
|
|
|
20
22
|
module_function
|
|
21
23
|
|
|
@@ -33,6 +35,14 @@ module Samagotchi
|
|
|
33
35
|
asked = inner ? inner[:asked].to_s : pending.question.to_s
|
|
34
36
|
task = inner ? inner[:task].to_s : task_of(child)
|
|
35
37
|
who = chain.reverse.join(" → ")
|
|
38
|
+
relay = { id: relay_id, child_id: child.id, child_short: short, child_question_id: pending.id.to_s,
|
|
39
|
+
task: task, chain: chain, asked: asked, more: more.to_i }
|
|
40
|
+
if pending.kind.to_s == HOOK_KIND
|
|
41
|
+
# The asking hook's own header, kept down a chain (a relay of a relay).
|
|
42
|
+
relay[:header] = inner ? inner[:header].to_s : pending.header.to_s
|
|
43
|
+
return hook_card(pending, who, more, relay)
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
{
|
|
37
47
|
question: question_text(who, task, asked),
|
|
38
48
|
options: relabel(Array(pending.options), facts[:scopes], who),
|
|
@@ -43,11 +53,33 @@ module Samagotchi
|
|
|
43
53
|
**{ effects: pending.effects, user_only: pending.user_only }.compact,
|
|
44
54
|
kind: Guardrails::Approval::KIND,
|
|
45
55
|
approval: facts,
|
|
46
|
-
relay:
|
|
47
|
-
task: task, chain: chain, asked: asked, more: more.to_i }
|
|
56
|
+
relay: relay
|
|
48
57
|
}
|
|
49
58
|
end
|
|
50
59
|
|
|
60
|
+
# A hook's question (P4: one its parent can't usefully answer): its
|
|
61
|
+
# kind, hook, options and effects as the child asked them, and no
|
|
62
|
+
# approval facts (ParentApprovals.approval? stays false on it, so the
|
|
63
|
+
# policy judges it by its own tags, as on the child).
|
|
64
|
+
def hook_card(pending, who, more, relay)
|
|
65
|
+
{
|
|
66
|
+
question: question_text(who, relay[:task], relay[:asked]),
|
|
67
|
+
options: Array(pending.options).map(&:to_s),
|
|
68
|
+
header: hook_header(who, relay[:header], more),
|
|
69
|
+
multi_select: !!pending.multi_select,
|
|
70
|
+
allow_freeform: !!pending.allow_freeform,
|
|
71
|
+
**{ effects: pending.effects, user_only: pending.user_only, hook: pending.hook }.compact,
|
|
72
|
+
kind: HOOK_KIND,
|
|
73
|
+
relay: relay
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# delegate ab12 asks: Cost cap (+1 more delegate waiting)
|
|
78
|
+
def hook_header(who, header, more)
|
|
79
|
+
text = "delegate #{who} asks: #{header.to_s.strip.empty? ? "a question" : header}"
|
|
80
|
+
more.to_i.positive? ? "#{text} (+#{more} more delegate#{"s" if more.to_i > 1} waiting)" : text
|
|
81
|
+
end
|
|
82
|
+
|
|
51
83
|
# delegate ab12 ("fix the flaky spec in …") asks:
|
|
52
84
|
# execute: git push origin main
|
|
53
85
|
# in /path/repo (repo samagotchi, branch main)
|
|
@@ -63,6 +63,19 @@ module Samagotchi
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Whether the parent's user (or a parent agent) answered or dismissed a
|
|
67
|
+
# relay of this child's question: it isn't relayed again (a refused
|
|
68
|
+
# answer leaves it on the child's card). A closed one (a Stop, the
|
|
69
|
+
# child gone) is not settled. Forgotten after KEEP_SECONDS or a restart.
|
|
70
|
+
def settled?(child_id, child_question_id)
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
prune
|
|
73
|
+
@relays.each_value.any? do |relay|
|
|
74
|
+
relay[:state] == "answered" && relay[:child_id] == child_id.to_s && relay[:child_question_id] == child_question_id.to_s
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
66
79
|
# Whether a relay is open, or settled recently enough that its child
|
|
67
80
|
# may still ask about it: a restart would drop them.
|
|
68
81
|
def active?
|
|
@@ -263,21 +263,13 @@ module Samagotchi
|
|
|
263
263
|
# A gem-mode worker activates the newest installed chi itself: under
|
|
264
264
|
# `bundle exec` (a Gemfile that has samagotchi) Bundler's RUBYOPT would
|
|
265
265
|
# hold it to the bundle's version, and its badge would never clear.
|
|
266
|
-
child_env.merge!(unbundled_env_changes) if InstalledGem.spec
|
|
266
|
+
child_env.merge!(InstalledGem.unbundled_env_changes) if InstalledGem.spec
|
|
267
267
|
opts[:env] = child_env unless child_env.empty?
|
|
268
268
|
opts
|
|
269
269
|
end
|
|
270
270
|
|
|
271
|
-
# What Bundler changed in ENV (RUBYOPT, BUNDLE_*), mapped back to the
|
|
272
|
-
# values from before it (nil: unset); {} outside Bundler.
|
|
273
|
-
private_class_method def self.unbundled_env_changes(env = ENV.to_h)
|
|
274
|
-
return {} unless defined?(::Bundler) && ::Bundler.respond_to?(:unbundled_env)
|
|
275
|
-
|
|
276
|
-
clean = ::Bundler.unbundled_env
|
|
277
|
-
(env.keys | clean.keys).each_with_object({}) { |key, changes| changes[key] = clean[key] unless clean[key] == env[key] }
|
|
278
|
-
end
|
|
279
|
-
|
|
280
271
|
GUARDRAILS_ENV_PREFIX = "SAMAGOTCHI_GUARDRAILS_"
|
|
272
|
+
WORKER_ENV_PREFIX = "SAMAGOTCHI_WORKER_"
|
|
281
273
|
# Tools::Builtins.parent_env's (Tools::TaskRuntime::MARKER_ENV_KEYS).
|
|
282
274
|
SESSION_MARKER_ENV = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
283
275
|
SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
|
|
@@ -195,7 +195,7 @@ module Samagotchi
|
|
|
195
195
|
|
|
196
196
|
verb = flags.size == 1 ? "applies" : "apply"
|
|
197
197
|
names = flags.size > 2 ? "#{flags[0..-2].join(", ")} and #{flags.last}" : flags.join(" and ")
|
|
198
|
-
err.puts "(#{names} #{verb} to a new session; #{session_id}
|
|
198
|
+
err.puts "(#{names} #{verb} to a new session; #{session_id} keeps its own)"
|
|
199
199
|
end
|
|
200
200
|
|
|
201
201
|
# @return [BridgeClient, nil] the running worker's, or nil when none runs
|
|
@@ -96,7 +96,8 @@ module Samagotchi
|
|
|
96
96
|
case reason.to_s
|
|
97
97
|
when "answered_on_child" then "(answered in #{@delegate})"
|
|
98
98
|
when "child_gone" then "(#{@delegate}'s worker is gone)"
|
|
99
|
-
|
|
99
|
+
# A relayed hook question's dismissal: the hook decides what it means.
|
|
100
|
+
when "dismissed" then approval? ? "(denied)" : "(dismissed)"
|
|
100
101
|
else "(left open in #{@delegate})"
|
|
101
102
|
end
|
|
102
103
|
end
|
|
@@ -737,7 +737,7 @@ module Samagotchi
|
|
|
737
737
|
return if @toolkit_flags.empty?
|
|
738
738
|
|
|
739
739
|
names = @toolkit_flags.size > 1 ? "#{@toolkit_flags[0..-2].join(", ")} and #{@toolkit_flags.last}" : @toolkit_flags.first
|
|
740
|
-
warn "(#{names} #{@toolkit_flags.size == 1 ? "applies" : "apply"} to a new session; #{session.id}
|
|
740
|
+
warn "(#{names} #{@toolkit_flags.size == 1 ? "applies" : "apply"} to a new session; #{session.id} keeps its own)"
|
|
741
741
|
end
|
|
742
742
|
private :warn_toolkit_flags_ignored
|
|
743
743
|
|