samagotchi 0.151.0 → 0.152.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 +29 -1
- data/docs/bundle-authoring.md +4 -4
- data/docs/cli.md +32 -7
- data/docs/guardrails.md +4 -0
- data/docs/internals/block-renderers.md +3 -1
- data/docs/internals/bundles.md +48 -14
- data/docs/internals/objectives.md +6 -4
- data/docs/memory.md +6 -1
- data/docs/plugins.md +51 -14
- data/docs/releasing.md +7 -2
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/block_renderers.rb +9 -8
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +27 -4
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/memory_bundle/asset_installer.rb +11 -8
- data/lib/samagotchi/memory_bundle/builder.rb +36 -30
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -46
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -15
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +90 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +146 -6
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +217 -18
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +2 -1
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- metadata +4 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest"
|
|
4
3
|
require "time"
|
|
4
|
+
require_relative "file_digest"
|
|
5
5
|
|
|
6
6
|
module Samagotchi
|
|
7
7
|
module MemoryBundle
|
|
@@ -156,7 +156,7 @@ module Samagotchi
|
|
|
156
156
|
# read isn't known to be level. No digest (an older mark): it isn't known
|
|
157
157
|
# either, and a retry would have settled it.
|
|
158
158
|
def differs?(path, incoming_sha)
|
|
159
|
-
incoming_sha.nil? ||
|
|
159
|
+
incoming_sha.nil? || FileDigest.hex(path) != incoming_sha
|
|
160
160
|
rescue SystemCallError
|
|
161
161
|
true
|
|
162
162
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest"
|
|
4
3
|
require "json"
|
|
4
|
+
require_relative "file_digest"
|
|
5
5
|
require_relative "../memory_paths"
|
|
6
6
|
require_relative "bundle_hook"
|
|
7
7
|
require_relative "plugin_ref"
|
|
@@ -10,11 +10,15 @@ module Samagotchi
|
|
|
10
10
|
module MemoryBundle
|
|
11
11
|
# Only what reports on an edited file needs it.
|
|
12
12
|
autoload :FileStanding, File.expand_path("file_standing", __dir__)
|
|
13
|
+
autoload :InstalledManifest, File.expand_path("installed_manifest", __dir__)
|
|
13
14
|
|
|
14
15
|
# An installed bundle's record, as Provenance#write keeps it in
|
|
15
|
-
# <bundles>/<name>/manifest.json
|
|
16
|
-
# lacks keys a newer chi writes,
|
|
17
|
-
# wrong shape reads as empty (nil
|
|
16
|
+
# <bundles>/<name>/manifest.json: identity, shas, files and marks.
|
|
17
|
+
# Reading is tolerant: an older record lacks keys a newer chi writes,
|
|
18
|
+
# and a key that is missing or of the wrong shape reads as empty (nil
|
|
19
|
+
# for the optional strings and lists). Its manifest data (requires_chi,
|
|
20
|
+
# trust_level, needs, providers, renderers, settings…) is read through
|
|
21
|
+
# #manifest, never off the record.
|
|
18
22
|
#
|
|
19
23
|
# files file key ("x.md") => { checksum:, conflict:, incoming_sha:, legacy_base:, why:, missed:, missed_places:,
|
|
20
24
|
# description: } (symbol keys);
|
|
@@ -26,32 +30,36 @@ module Samagotchi
|
|
|
26
30
|
# guardrails basename => recorded sha256 ("" when none)
|
|
27
31
|
# scripts file name => recorded sha256 ("" when none)
|
|
28
32
|
# plugin PluginRef, nil without one
|
|
29
|
-
# needs, context_providers, block_renderers
|
|
30
|
-
# the lists as recorded (nil when absent): Manifest.parse_needs,
|
|
31
|
-
# ContextProviders.parse_list and BlockRenderers.parse_list check them
|
|
32
33
|
# includes a profile's recorded members, nil for a plain bundle
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# manifest
|
|
39
|
-
# setting_types
|
|
40
|
-
# {key => type} for the typed ones, {} when none
|
|
34
|
+
# mirror the record's frozen copy of the manifest keys (MIRROR_KEYS,
|
|
35
|
+
# string keys, the values as recorded), which only
|
|
36
|
+
# InstalledManifest reads: requires_chi, trust_level, needs,
|
|
37
|
+
# context_providers, block_renderers, session_settings,
|
|
38
|
+
# session_setting_types, settings, setting_types. A key a
|
|
39
|
+
# later manifest gains is never mirrored
|
|
41
40
|
# dev a dev install (`chi bundle install --dev`): its source is
|
|
42
41
|
# the folder it links to and its files carry Provenance::DEV_SHA
|
|
42
|
+
# manifest_sha the kept manifest.yml's recorded sha256 (Provenance::DEV_SHA
|
|
43
|
+
# for a dev install's link), nil on a record written before
|
|
44
|
+
# chi kept it, or rewritten since by a chi that doesn't
|
|
45
|
+
# installed_by the chi VERSION that wrote the record with its kept
|
|
46
|
+
# manifest, nil when manifest_sha is
|
|
47
|
+
# dir its .bundles/<name> folder (.read, Provenance.each_installed),
|
|
48
|
+
# nil for a record parsed alone or one that doesn't read
|
|
43
49
|
# error why manifest.json couldn't be read (the rest empty), else nil
|
|
44
|
-
class InstalledBundle < Data.define(:name, :version, :scope, :source, :installed_at, :
|
|
45
|
-
:
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
class InstalledBundle < Data.define(:name, :version, :scope, :source, :installed_at, :source_commit, :files, :hooks,
|
|
51
|
+
:guardrails, :scripts, :plugin, :includes, :dev, :mirror, :manifest_sha,
|
|
52
|
+
:installed_by, :dir, :error)
|
|
53
|
+
OPTIONAL = { version: nil, scope: nil, source: nil, installed_at: nil, source_commit: nil, files: {}, hooks: {},
|
|
54
|
+
guardrails: {}, scripts: {}, plugin: nil, includes: nil, dev: false, mirror: {}, manifest_sha: nil,
|
|
55
|
+
installed_by: nil, dir: nil, error: nil }.freeze
|
|
56
|
+
|
|
57
|
+
# The manifest keys a record mirrors, as Provenance#write records them.
|
|
58
|
+
MIRROR_KEYS = %w[requires_chi trust_level needs context_providers block_renderers session_settings
|
|
59
|
+
session_setting_types settings setting_types].freeze
|
|
53
60
|
|
|
54
61
|
def initialize(name:, **fields)
|
|
62
|
+
fields[:mirror] = fields[:mirror].to_h.transform_keys(&:to_s).slice(*MIRROR_KEYS).freeze if fields.key?(:mirror)
|
|
55
63
|
super(name: name, **OPTIONAL, **fields)
|
|
56
64
|
end
|
|
57
65
|
|
|
@@ -66,7 +74,7 @@ module Samagotchi
|
|
|
66
74
|
def self.read(name, path)
|
|
67
75
|
return nil unless File.exist?(path)
|
|
68
76
|
|
|
69
|
-
parse(name, JSON.parse(File.read(path)))
|
|
77
|
+
parse(name, JSON.parse(File.read(path)), File.dirname(path))
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
# One that couldn't be read: +message+ says why.
|
|
@@ -74,25 +82,21 @@ module Samagotchi
|
|
|
74
82
|
|
|
75
83
|
# @param raw [Hash] the parsed manifest.json (string or symbol keys);
|
|
76
84
|
# anything else reads as unreadable
|
|
77
|
-
|
|
85
|
+
# @param dir [String, nil] its .bundles/<name> folder
|
|
86
|
+
def self.parse(name, raw, dir = nil)
|
|
78
87
|
return unreadable(name, "manifest.json is not an object") unless raw.is_a?(Hash)
|
|
79
88
|
|
|
80
89
|
h = raw.transform_keys(&:to_s)
|
|
81
90
|
new(name: name, version: string(h["version"]), scope: string(h["scope"]), source: string(h["source"]),
|
|
82
|
-
installed_at: string(h["installed_at"]),
|
|
83
|
-
source_commit: string(h["source_commit"]), requires_chi: string(h["requires_chi"]),
|
|
91
|
+
installed_at: string(h["installed_at"]), source_commit: string(h["source_commit"]),
|
|
84
92
|
files: mapping(h["files"]) { |entry| entry.is_a?(Hash) ? entry.transform_keys(&:to_sym) : {} },
|
|
85
93
|
hooks: mapping(h["hooks"]) { |meta| BundleHook.parse(meta) },
|
|
86
94
|
guardrails: mapping(h["guardrails"]) { |meta| sha(meta) },
|
|
87
95
|
scripts: mapping(h["scripts"]) { |meta| sha(meta) },
|
|
88
|
-
plugin: PluginRef.parse(h["plugin"]),
|
|
89
|
-
block_renderers: h["block_renderers"],
|
|
96
|
+
plugin: PluginRef.parse(h["plugin"]),
|
|
90
97
|
includes: h["includes"].is_a?(Array) ? h["includes"].map(&:to_s) : nil,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
settings: h["settings"].is_a?(Array) ? h["settings"].map(&:to_s) : nil,
|
|
94
|
-
setting_types: mapping(h["setting_types"]) { |type| type.to_s },
|
|
95
|
-
dev: h["dev"] == true)
|
|
98
|
+
dev: h["dev"] == true, mirror: h.slice(*MIRROR_KEYS).compact,
|
|
99
|
+
manifest_sha: string(h["manifest_sha"]), installed_by: string(h["installed_by"]), dir: dir)
|
|
96
100
|
end
|
|
97
101
|
|
|
98
102
|
def self.string(value) = value&.to_s
|
|
@@ -106,12 +110,24 @@ module Samagotchi
|
|
|
106
110
|
|
|
107
111
|
def error? = !error.nil?
|
|
108
112
|
|
|
113
|
+
# Its kept manifest.yml (Provenance#kept_manifest_path), nil with no dir.
|
|
114
|
+
def kept_manifest_path = dir && File.join(dir, "manifest.yml")
|
|
115
|
+
|
|
116
|
+
# Whether it lacks a kept manifest.yml chi can read (Provenance#adopt_manifest
|
|
117
|
+
# keeps one): an older chi's record has no manifest_sha (with or without a
|
|
118
|
+
# file there, InstalledManifest's legacy and :stale), or the file it
|
|
119
|
+
# recorded is gone (:missing). false with no dir.
|
|
120
|
+
def manifest_unkept? = !dir.nil? && (manifest_sha.to_s.empty? || !File.file?(kept_manifest_path))
|
|
121
|
+
|
|
122
|
+
# What its manifest says (requires_chi, hook metadata, needs, providers,
|
|
123
|
+
# renderers, settings…): the one way to read manifest data of an
|
|
124
|
+
# installed bundle.
|
|
125
|
+
# @return [InstalledManifest]
|
|
126
|
+
def manifest = InstalledManifest.of(self)
|
|
127
|
+
|
|
109
128
|
# The scope its files went to: "system" when none is recorded.
|
|
110
129
|
def effective_scope = scope.to_s.strip.empty? ? "system" : scope
|
|
111
130
|
|
|
112
|
-
# A bundle with no trust_level counts as experimental.
|
|
113
|
-
def experimental? = (trust_level || "experimental") == "experimental"
|
|
114
|
-
|
|
115
131
|
# A profile (meta bundle): it records its members.
|
|
116
132
|
def profile? = !includes.nil?
|
|
117
133
|
|
|
@@ -155,18 +171,11 @@ module Samagotchi
|
|
|
155
171
|
end
|
|
156
172
|
|
|
157
173
|
def source_sha(key)
|
|
158
|
-
|
|
159
|
-
File.file?(path) ? Digest::SHA256.file(path).hexdigest : nil
|
|
174
|
+
FileDigest.hex(File.join(source.to_s, key))
|
|
160
175
|
rescue SystemCallError
|
|
161
176
|
nil
|
|
162
177
|
end
|
|
163
178
|
private :source_sha
|
|
164
|
-
|
|
165
|
-
def needs? = needs.is_a?(Array) && !needs.empty?
|
|
166
|
-
|
|
167
|
-
def context_providers? = context_providers.is_a?(Array) && !context_providers.empty?
|
|
168
|
-
|
|
169
|
-
def block_renderers? = block_renderers.is_a?(Array) && !block_renderers.empty?
|
|
170
179
|
end
|
|
171
180
|
end
|
|
172
181
|
end
|
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require "json"
|
|
5
4
|
require "tmpdir"
|
|
5
|
+
require "yaml"
|
|
6
6
|
require_relative "bundle_hook"
|
|
7
7
|
require_relative "manifest"
|
|
8
8
|
require_relative "provenance"
|
|
9
9
|
|
|
10
10
|
module Samagotchi
|
|
11
11
|
module MemoryBundle
|
|
12
|
-
# An installed bundle laid out as a source folder again:
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
12
|
+
# An installed bundle laid out as a source folder again: the plugin, hooks,
|
|
13
|
+
# rules and scripts as they are installed (a dev install's links
|
|
14
|
+
# followed), the memories as their bases (the bundle's bytes, not the
|
|
15
|
+
# user's edits). A bundle's source may be gone or be a URL; this is what
|
|
16
|
+
# `chi update` can check of what really runs.
|
|
17
|
+
#
|
|
18
|
+
# manifest.yml: its files:, hooks:, plugin: and scripts: are the record's
|
|
19
|
+
# and the copy's (what is installed: a memory the install left out, or one
|
|
20
|
+
# with no base, and a manifest hook the record lacks aren't listed, so the
|
|
21
|
+
# check finds no missing-file for them). Every other key is the kept
|
|
22
|
+
# manifest.yml's when it is read (InstalledManifest origin :kept), so the
|
|
23
|
+
# check sees its unknown keys too; else the record's mirror's.
|
|
17
24
|
#
|
|
18
25
|
# The shas are the files' own, so an edited plugin is checked as written.
|
|
19
26
|
# A plugin, hook or script the record lists that isn't there stays
|
|
@@ -33,23 +40,38 @@ module Samagotchi
|
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def write(bundle, provenance, dir)
|
|
43
|
+
manifest = bundle.manifest
|
|
36
44
|
Manifest.write(
|
|
37
|
-
dir: dir, name: bundle.name, version: bundle.version.to_s.empty? ? "0" : bundle.version, trust_level:
|
|
45
|
+
dir: dir, name: bundle.name, version: bundle.version.to_s.empty? ? "0" : bundle.version, trust_level: manifest.trust_level,
|
|
38
46
|
files: memories(bundle, provenance, dir),
|
|
39
|
-
file_descriptions:
|
|
47
|
+
file_descriptions: manifest.file_descriptions,
|
|
40
48
|
hooks: hooks(bundle, provenance, dir), plugin: plugin(bundle, provenance, dir),
|
|
41
49
|
scripts: copies(bundle.scripts, provenance.scripts_dir, File.join(dir, "scripts")),
|
|
42
|
-
requires_chi:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
session_settings: bundle.session_settings, session_setting_types: bundle.session_setting_types,
|
|
47
|
-
settings: bundle.settings, setting_types: bundle.setting_types
|
|
50
|
+
requires_chi: manifest.requires_chi, needs: manifest.needs,
|
|
51
|
+
context_providers: manifest.context_providers, block_renderers: manifest.block_renderers,
|
|
52
|
+
session_settings: manifest.session_settings, session_setting_types: manifest.session_setting_types,
|
|
53
|
+
settings: manifest.settings, setting_types: manifest.setting_types
|
|
48
54
|
)
|
|
55
|
+
keep_manifest_keys(bundle.kept_manifest_path, File.join(dir, "manifest.yml"), manifest.key_problems.keys) if manifest.origin == :kept
|
|
49
56
|
copies(bundle.guardrails, provenance.guardrails_dir, File.join(dir, "guardrails"))
|
|
50
57
|
end
|
|
51
58
|
private_class_method :write
|
|
52
59
|
|
|
60
|
+
# What is installed, which the copy's manifest.yml takes from the record.
|
|
61
|
+
INSTALLED_KEYS = %w[name version files hooks plugin scripts].freeze
|
|
62
|
+
|
|
63
|
+
# The written manifest's INSTALLED_KEYS and +failed+ keys (the value
|
|
64
|
+
# InstalledManifest fell back to: InstalledCheck already names the
|
|
65
|
+
# problem, and the raw value would name it again), then the kept
|
|
66
|
+
# manifest's other keys. A kept file of comments only holds none.
|
|
67
|
+
def keep_manifest_keys(kept, written, failed)
|
|
68
|
+
ours = YAML.load_file(written)
|
|
69
|
+
theirs = YAML.load_file(kept) || {}
|
|
70
|
+
own = INSTALLED_KEYS + failed
|
|
71
|
+
File.write(written, YAML.dump(ours.slice(*own).merge(theirs.except(*own))))
|
|
72
|
+
end
|
|
73
|
+
private_class_method :keep_manifest_keys
|
|
74
|
+
|
|
53
75
|
# A memory with no base has nothing to install, so it isn't listed.
|
|
54
76
|
def memories(bundle, provenance, dir)
|
|
55
77
|
bundle.files.keys.select { |key| File.file?(provenance.base_path(key)) }.to_h do |key|
|
|
@@ -58,8 +80,9 @@ module Samagotchi
|
|
|
58
80
|
end
|
|
59
81
|
private_class_method :memories
|
|
60
82
|
|
|
83
|
+
# The recorded hooks with the metadata they load with (InstalledManifest#hooks).
|
|
61
84
|
def hooks(bundle, provenance, dir)
|
|
62
|
-
bundle.hooks.to_h do |file, hook|
|
|
85
|
+
bundle.manifest.hooks.to_h do |file, hook|
|
|
63
86
|
sha = copy(File.join(provenance.hooks_dir, file), File.join(dir, "hooks", file), hook.sha256)
|
|
64
87
|
[file, hook.with(sha256: sha)]
|
|
65
88
|
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../context_providers"
|
|
4
|
+
require_relative "../version"
|
|
5
|
+
require_relative "manifest"
|
|
6
|
+
require_relative "provenance"
|
|
7
|
+
|
|
8
|
+
module Samagotchi
|
|
9
|
+
autoload :BlockRenderers, File.expand_path("../block_renderers", __dir__)
|
|
10
|
+
|
|
11
|
+
module MemoryBundle
|
|
12
|
+
# The one answer to "this installed bundle's manifest": what its
|
|
13
|
+
# manifest.yml says, as the readers of an installed bundle take it
|
|
14
|
+
# (InstalledBundle#manifest). Identity and state stay on the record
|
|
15
|
+
# (InstalledBundle: name, version, source, the plugin's file, every
|
|
16
|
+
# sha, files, marks, a profile's members); manifest data is read here.
|
|
17
|
+
#
|
|
18
|
+
# Read from the bundle's kept manifest.yml (.bundles/<name>/manifest.yml,
|
|
19
|
+
# Provenance#write's manifest_from:) when the record has its sha and the
|
|
20
|
+
# file still has it (a dev install's link is never checked): origin
|
|
21
|
+
# :kept, so a key the record never mirrored (one an older chi didn't
|
|
22
|
+
# know) is read. Otherwise from the record's frozen mirror of the
|
|
23
|
+
# manifest keys (origin :record), in four cases:
|
|
24
|
+
# no kept file, no manifest_sha a record from before chi kept it; no problem
|
|
25
|
+
# no kept file, a manifest_sha problems["manifest.yml"] = :missing (it was
|
|
26
|
+
# removed, or something else is in its place)
|
|
27
|
+
# kept, no manifest_sha problems["manifest.yml"] = :stale (a chi
|
|
28
|
+
# without the kept manifest rewrote the record;
|
|
29
|
+
# the file may be an earlier version's)
|
|
30
|
+
# kept, another sha, or not a problems["manifest.yml"] = :edited
|
|
31
|
+
# YAML mapping
|
|
32
|
+
# kept, its sha origin :kept
|
|
33
|
+
# Each key is parsed on its own (Manifest.tolerant for the kept file): one
|
|
34
|
+
# that doesn't parse reads as the mirror's value if that parses, else as
|
|
35
|
+
# empty, and its message is in +problems+ (key => message), so one bad key
|
|
36
|
+
# never costs the rest. A reader that logged such a key logs problems[key].
|
|
37
|
+
# Built once per record and kept file (a process-wide cache keyed by the
|
|
38
|
+
# record and the file's stat): chi web asks per request.
|
|
39
|
+
#
|
|
40
|
+
# requires_chi, trust_level the strings as recorded, nil when none
|
|
41
|
+
# description the kept manifest's, nil from the record
|
|
42
|
+
# hooks basename => BundleHook: each recorded hook,
|
|
43
|
+
# the kept manifest's events, on_error and
|
|
44
|
+
# priority with the record's sha
|
|
45
|
+
# needs Manifest.parse_needs'
|
|
46
|
+
# script_names the scripts/ files it lists
|
|
47
|
+
# context_providers Array<ContextProviders::Provider>
|
|
48
|
+
# block_renderers Array<BlockRenderers::Renderer>
|
|
49
|
+
# session_settings, session_setting_types, settings, setting_types
|
|
50
|
+
# as Manifest reads them (settings nil when
|
|
51
|
+
# the manifest said nothing of them)
|
|
52
|
+
# file_descriptions file key => the memory's description
|
|
53
|
+
# includes a profile's recorded members (the record's
|
|
54
|
+
# identity, never the kept file's), nil for a plain bundle
|
|
55
|
+
# unknown_keys the kept manifest's keys this chi doesn't know
|
|
56
|
+
# floor_keys the kept manifest's Manifest#floor_keys (key =>
|
|
57
|
+
# the first chi that read it), {} from the record
|
|
58
|
+
# origin :kept or :record
|
|
59
|
+
class InstalledManifest < Data.define(:name, :version, :requires_chi, :trust_level, :description, :hooks, :needs,
|
|
60
|
+
:script_names, :context_providers, :block_renderers, :session_settings,
|
|
61
|
+
:session_setting_types, :settings, :setting_types, :file_descriptions,
|
|
62
|
+
:includes, :unknown_keys, :floor_keys, :problems, :origin)
|
|
63
|
+
# The key problems names the kept file's standing under.
|
|
64
|
+
KEPT_FILE = "manifest.yml"
|
|
65
|
+
# How many manifests the cache holds before it starts again.
|
|
66
|
+
CACHE_LIMIT = 256
|
|
67
|
+
@cache = {}
|
|
68
|
+
@cache_lock = Mutex.new
|
|
69
|
+
|
|
70
|
+
# @param bundle [InstalledBundle]
|
|
71
|
+
# @return [InstalledManifest]
|
|
72
|
+
def self.of(bundle)
|
|
73
|
+
key = [bundle, kept_stat(bundle.kept_manifest_path)]
|
|
74
|
+
cached = @cache_lock.synchronize { @cache[key] }
|
|
75
|
+
return cached if cached
|
|
76
|
+
|
|
77
|
+
built = deep_freeze(build(bundle))
|
|
78
|
+
@cache_lock.synchronize do
|
|
79
|
+
@cache.clear if @cache.size >= CACHE_LIMIT
|
|
80
|
+
@cache[key] = built
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Callers share the cached value, so nothing in it can change under another.
|
|
85
|
+
def self.deep_freeze(value)
|
|
86
|
+
case value
|
|
87
|
+
when Hash then value.each { |key, item| deep_freeze(key) && deep_freeze(item) }
|
|
88
|
+
when Array then value.each { |item| deep_freeze(item) }
|
|
89
|
+
when Data then value.to_h.each_value { |item| deep_freeze(item) }
|
|
90
|
+
end
|
|
91
|
+
value.freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# What says the kept file changed: nil when there is none.
|
|
95
|
+
def self.kept_stat(path)
|
|
96
|
+
stat = path && File.stat(path)
|
|
97
|
+
stat && [stat.mtime.to_r, stat.size, stat.ino, stat.dev]
|
|
98
|
+
rescue SystemCallError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.build(bundle)
|
|
103
|
+
mirrored = from_record(bundle)
|
|
104
|
+
path = bundle.kept_manifest_path
|
|
105
|
+
return mirrored unless path
|
|
106
|
+
|
|
107
|
+
recorded = !bundle.manifest_sha.to_s.empty?
|
|
108
|
+
return recorded ? standing(mirrored, :missing) : mirrored unless File.file?(path)
|
|
109
|
+
return standing(mirrored, :stale) unless recorded
|
|
110
|
+
return standing(mirrored, :edited) unless Provenance.sha_matches?(path, bundle.manifest_sha)
|
|
111
|
+
|
|
112
|
+
kept, problems = Manifest.tolerant(path: path, fallback_scripts: bundle.scripts.keys)
|
|
113
|
+
from_kept(bundle, kept, problems, mirrored)
|
|
114
|
+
rescue Manifest::ValidationError, SystemCallError, EncodingError
|
|
115
|
+
standing(mirrored, :edited)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.standing(mirrored, why) = mirrored.with(problems: mirrored.problems.merge(KEPT_FILE => why).freeze)
|
|
119
|
+
|
|
120
|
+
# The kept manifest's values; a key that failed takes the mirror's.
|
|
121
|
+
def self.from_kept(bundle, kept, problems, mirrored)
|
|
122
|
+
pick = ->(key, ours, theirs) { problems.key?(key) ? theirs : ours }
|
|
123
|
+
session = pick.call("session_settings", kept, mirrored)
|
|
124
|
+
settings = pick.call("settings", kept, mirrored)
|
|
125
|
+
new(name: bundle.name, version: bundle.version,
|
|
126
|
+
requires_chi: pick.call("requires_chi", kept.requires_chi, mirrored.requires_chi),
|
|
127
|
+
trust_level: kept.trust_level, description: kept.description.empty? ? nil : kept.description,
|
|
128
|
+
hooks: pick.call("hooks", kept_hooks(bundle.hooks, kept.hooks), bundle.hooks),
|
|
129
|
+
needs: pick.call("needs", kept.needs, mirrored.needs),
|
|
130
|
+
script_names: pick.call("scripts", kept.scripts.keys, mirrored.script_names),
|
|
131
|
+
context_providers: pick.call("context_providers", kept.context_providers, mirrored.context_providers),
|
|
132
|
+
block_renderers: pick.call("block_renderers", kept.block_renderers, mirrored.block_renderers),
|
|
133
|
+
session_settings: session.session_settings, session_setting_types: session.session_setting_types,
|
|
134
|
+
settings: settings.settings, setting_types: settings.setting_types,
|
|
135
|
+
file_descriptions: pick.call("files", kept.file_descriptions, mirrored.file_descriptions),
|
|
136
|
+
includes: bundle.includes,
|
|
137
|
+
unknown_keys: kept.unknown_keys.map(&:to_s), floor_keys: floors(kept), problems: problems.freeze, origin: :kept)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Manifest#floor_keys of the tolerant read; a shape it can't tell is no floor.
|
|
141
|
+
def self.floors(kept)
|
|
142
|
+
kept.floor_keys
|
|
143
|
+
rescue StandardError
|
|
144
|
+
{}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Each recorded hook (what is installed): the kept manifest's metadata
|
|
148
|
+
# for it with the record's sha; one the manifest doesn't list (a found
|
|
149
|
+
# hooks/*.rb) as recorded. A manifest hook the record lacks isn't installed.
|
|
150
|
+
def self.kept_hooks(recorded, declared)
|
|
151
|
+
recorded.to_h do |file, hook|
|
|
152
|
+
[file, declared.key?(file) ? declared[file].with(sha256: hook.sha256) : hook]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.from_record(bundle)
|
|
157
|
+
raw = recorded(bundle)
|
|
158
|
+
problems = {}
|
|
159
|
+
new(name: bundle.name, version: bundle.version,
|
|
160
|
+
requires_chi: raw["requires_chi"]&.to_s, trust_level: raw["trust_level"]&.to_s, description: nil,
|
|
161
|
+
hooks: bundle.hooks,
|
|
162
|
+
needs: parsed(problems, "needs", [], Manifest::ValidationError) { Manifest.parse_needs(raw["needs"]) },
|
|
163
|
+
script_names: bundle.scripts.keys,
|
|
164
|
+
context_providers: parsed(problems, "context_providers", []) { ContextProviders.parse_list(raw["context_providers"]) },
|
|
165
|
+
block_renderers: parsed(problems, "block_renderers", []) { BlockRenderers.parse_list(raw["block_renderers"]) },
|
|
166
|
+
session_settings: list(raw["session_settings"]) || [],
|
|
167
|
+
session_setting_types: types(raw["session_setting_types"]),
|
|
168
|
+
settings: list(raw["settings"]),
|
|
169
|
+
setting_types: types(raw["setting_types"]),
|
|
170
|
+
file_descriptions: file_descriptions(bundle.files),
|
|
171
|
+
includes: bundle.includes, unknown_keys: [], floor_keys: {}, problems: problems.freeze, origin: :record)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The record's values of the manifest keys (its frozen mirror), string
|
|
175
|
+
# keys: what a bundle with no usable kept manifest, or no dir (a record
|
|
176
|
+
# parsed alone), reads.
|
|
177
|
+
def self.recorded(bundle) = bundle.mirror
|
|
178
|
+
|
|
179
|
+
# The block's value, else +empty+ with the key's problem noted.
|
|
180
|
+
# ArgumentError: ContextProviders::Invalid and BlockRenderers::Invalid
|
|
181
|
+
# are ones; EncodingError: a record's string whose bytes aren't UTF-8
|
|
182
|
+
# (JSON keeps them) raises Encoding::CompatibilityError when parsed.
|
|
183
|
+
def self.parsed(problems, key, empty, *errors)
|
|
184
|
+
yield
|
|
185
|
+
rescue ArgumentError, EncodingError, *errors => e
|
|
186
|
+
problems[key] = e.message
|
|
187
|
+
empty
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.list(value) = value.is_a?(Array) ? value.map(&:to_s) : nil
|
|
191
|
+
|
|
192
|
+
def self.types(value) = value.is_a?(Hash) ? value.to_h { |key, type| [key.to_s, type.to_s] } : {}
|
|
193
|
+
|
|
194
|
+
def self.file_descriptions(files)
|
|
195
|
+
files.each_with_object({}) do |(key, entry), acc|
|
|
196
|
+
text = entry[:description].to_s
|
|
197
|
+
acc[key] = text unless text.empty?
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
private_class_method :deep_freeze, :kept_stat, :build, :standing, :from_kept, :floors, :kept_hooks, :from_record, :recorded, :parsed,
|
|
201
|
+
:list, :types, :file_descriptions
|
|
202
|
+
|
|
203
|
+
# Why the kept manifest.yml wasn't read: :missing, :stale or :edited (the class
|
|
204
|
+
# comment's cases), nil when it was or there is none.
|
|
205
|
+
def standing = problems[KEPT_FILE]
|
|
206
|
+
|
|
207
|
+
# The keys that didn't parse, key => message, without the kept file's standing.
|
|
208
|
+
def key_problems = problems.except(KEPT_FILE)
|
|
209
|
+
|
|
210
|
+
# Why chi +version+ can't load this bundle, nil when it can (or it
|
|
211
|
+
# names no requirement): the one requires_chi gate.
|
|
212
|
+
def requires_chi_failure(version = Samagotchi::VERSION) = Manifest.requires_chi_failure(requires_chi, version)
|
|
213
|
+
|
|
214
|
+
# A bundle with no trust_level counts as experimental.
|
|
215
|
+
def experimental? = (trust_level || "experimental") == "experimental"
|
|
216
|
+
|
|
217
|
+
def needs? = !needs.empty?
|
|
218
|
+
|
|
219
|
+
def context_providers? = !context_providers.empty?
|
|
220
|
+
|
|
221
|
+
def block_renderers? = !block_renderers.empty?
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|