samagotchi 0.151.0 → 0.153.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 +46 -1
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +84 -0
- data/docs/cli.md +35 -10
- data/docs/configuration.md +19 -14
- data/docs/guardrails.md +11 -5
- data/docs/hooks.md +4 -3
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +3 -0
- data/docs/internals/block-renderers.md +3 -1
- data/docs/internals/bundles.md +48 -14
- data/docs/internals/engine.md +9 -5
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +7 -3
- data/docs/internals/web-frontend.md +20 -17
- data/docs/memory.md +6 -1
- data/docs/plugins.md +80 -29
- data/docs/releasing.md +8 -3
- data/docs/sessions.md +2 -2
- data/docs/testing.md +2 -2
- 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/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +37 -9
- 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/review_boundary.rb +4 -3
- 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/llm_context_stale.rb +2 -2
- 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/message_parts.rb +3 -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/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- 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/public/app.js +61 -357
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/worker.rb +6 -0
- metadata +11 -1
|
@@ -46,6 +46,11 @@ module Samagotchi
|
|
|
46
46
|
RUBY = "{ruby}"
|
|
47
47
|
URL = "{url}"
|
|
48
48
|
MIN_EVERY_SECONDS = 30
|
|
49
|
+
# A bundle whose requires_chi this chi doesn't meet, already logged: said
|
|
50
|
+
# once per process per bundle and version, the way GuardrailWiring says a
|
|
51
|
+
# bundle's kept rules are.
|
|
52
|
+
TOO_NEW_LOGGED = Set.new
|
|
53
|
+
TOO_NEW_LOG_LOCK = Mutex.new
|
|
49
54
|
|
|
50
55
|
module_function
|
|
51
56
|
|
|
@@ -79,15 +84,16 @@ module Samagotchi
|
|
|
79
84
|
|
|
80
85
|
# The source the installed bundles' providers make of +url+: the first
|
|
81
86
|
# bundle (by name) whose provider matches. A bundle whose providers
|
|
82
|
-
# don't parse is skipped
|
|
87
|
+
# don't parse, or whose requires_chi this chi doesn't meet, is skipped
|
|
88
|
+
# (logged).
|
|
83
89
|
# @return [Resolved, nil] nil when none matches
|
|
84
90
|
# @raise [Invalid] the provider makes a name that isn't a source name
|
|
85
91
|
def resolve(url)
|
|
86
92
|
url = url.to_s.strip
|
|
87
93
|
MemoryBundle::Provenance.each_installed do |bundle, record|
|
|
88
|
-
next if record.error? ||
|
|
94
|
+
next if record.error? || too_new?(bundle, record.manifest)
|
|
89
95
|
|
|
90
|
-
providers(bundle, record).each do |provider|
|
|
96
|
+
providers(bundle, record.manifest).each do |provider|
|
|
91
97
|
match = provider.regexp.match(url) or next
|
|
92
98
|
return resolved(bundle, provider, match)
|
|
93
99
|
end
|
|
@@ -97,14 +103,33 @@ module Samagotchi
|
|
|
97
103
|
|
|
98
104
|
# Whether an installed bundle declares a provider (the web's "+ URL").
|
|
99
105
|
def any?
|
|
100
|
-
MemoryBundle::Provenance.each_installed.any?
|
|
106
|
+
MemoryBundle::Provenance.each_installed.any? do |bundle, record|
|
|
107
|
+
!record.error? && !too_new?(bundle, record.manifest) && record.manifest.context_providers?
|
|
108
|
+
end
|
|
101
109
|
end
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
# A bundle this chi is too old for has no usable providers: skip it, and
|
|
112
|
+
# say so once per process per bundle and version.
|
|
113
|
+
# @return [Boolean]
|
|
114
|
+
def too_new?(bundle, manifest)
|
|
115
|
+
return false unless manifest.context_providers?
|
|
116
|
+
|
|
117
|
+
failure = manifest.requires_chi_failure
|
|
118
|
+
return false if failure.nil?
|
|
119
|
+
|
|
120
|
+
if TOO_NEW_LOG_LOCK.synchronize { TOO_NEW_LOGGED.add?([bundle, manifest.version.to_s]) }
|
|
121
|
+
Log.warn(:context, "providers_too_new", bundle: bundle, version: manifest.version.to_s,
|
|
122
|
+
requires_chi: manifest.requires_chi.to_s, msg: failure)
|
|
123
|
+
end
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Its parsed providers; a list that doesn't parse is none (logged).
|
|
128
|
+
def providers(bundle, manifest)
|
|
129
|
+
if (problem = manifest.problems["context_providers"])
|
|
130
|
+
Log.warn(:context, "providers_unreadable", bundle: bundle, msg: problem)
|
|
131
|
+
end
|
|
132
|
+
manifest.context_providers
|
|
108
133
|
end
|
|
109
134
|
|
|
110
135
|
def resolved(bundle, provider, match)
|
|
@@ -124,7 +149,8 @@ module Samagotchi
|
|
|
124
149
|
# installed (they run outside the guardrails gate, as a plugin loads:
|
|
125
150
|
# checked the same way).
|
|
126
151
|
# @return [String]
|
|
127
|
-
# @raise [Invalid] the bundle is gone
|
|
152
|
+
# @raise [Invalid] the bundle is gone, a script changed, or this chi
|
|
153
|
+
# doesn't meet the bundle's requires_chi
|
|
128
154
|
def command_for(source)
|
|
129
155
|
return source.cmd unless source.provider
|
|
130
156
|
|
|
@@ -132,6 +158,12 @@ module Samagotchi
|
|
|
132
158
|
record = provenance.record
|
|
133
159
|
raise Invalid, "bundle #{source.provider} isn't installed" unless record
|
|
134
160
|
|
|
161
|
+
# Its bundle is too new for this chi (installed with --force, or chi
|
|
162
|
+
# was downgraded): its script won't run, as its plugin wouldn't load.
|
|
163
|
+
if (failure = record.manifest.requires_chi_failure)
|
|
164
|
+
raise Invalid, "bundle #{source.provider}: #{failure}"
|
|
165
|
+
end
|
|
166
|
+
|
|
135
167
|
record.scripts.each do |file, sha|
|
|
136
168
|
path = File.join(provenance.scripts_dir, file)
|
|
137
169
|
next if File.file?(path) && MemoryBundle::Provenance.sha_matches?(path, sha)
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -345,7 +345,7 @@ module Samagotchi
|
|
|
345
345
|
@prompt_sections = Plugin::PromptSections.new
|
|
346
346
|
load_plugins if load
|
|
347
347
|
# Registered from what the sources published while they loaded, and
|
|
348
|
-
# with no plugins too: a toolkit's left-out
|
|
348
|
+
# with no plugins too: a toolkit's left-out tools are reachable
|
|
349
349
|
# either way (declared per toolkit, Tools::Registry).
|
|
350
350
|
@tool_index.declare(@tools, failures: @plugin_failures)
|
|
351
351
|
end
|
|
@@ -1037,8 +1037,7 @@ module Samagotchi
|
|
|
1037
1037
|
end
|
|
1038
1038
|
|
|
1039
1039
|
def show_objective_proposals
|
|
1040
|
-
|
|
1041
|
-
proposals.each do |proposal|
|
|
1040
|
+
drain_objective_proposals.each do |proposal|
|
|
1042
1041
|
id = "objectives-#{SecureRandom.hex(4)}"
|
|
1043
1042
|
show_card(source: Objectives::SOURCE, id: id, title: proposal.title, body: proposal.body,
|
|
1044
1043
|
actions: [{ label: "Apply", command: proposal.action_line(id) },
|
|
@@ -1049,6 +1048,22 @@ module Samagotchi
|
|
|
1049
1048
|
end
|
|
1050
1049
|
private :show_objective_proposals
|
|
1051
1050
|
|
|
1051
|
+
# A turn that did not complete (canceled, failed) takes its proposals
|
|
1052
|
+
# off the buffer without showing them: their Apply card would sit after a
|
|
1053
|
+
# turn whose `objectives propose` call is not in the history (a rollback
|
|
1054
|
+
# after Ctrl-C drops it), and leaving them in the buffer would show them
|
|
1055
|
+
# at the end of the next turn.
|
|
1056
|
+
def discard_objective_proposals(turn)
|
|
1057
|
+
dropped = drain_objective_proposals
|
|
1058
|
+
Log.info(:turn, "objectives_proposals_dropped", count: dropped.size, outcome: turn.outcome.to_s) unless dropped.empty?
|
|
1059
|
+
end
|
|
1060
|
+
private :discard_objective_proposals
|
|
1061
|
+
|
|
1062
|
+
def drain_objective_proposals
|
|
1063
|
+
@objective_proposals_mutex.synchronize { @objective_proposals.slice!(0..) }
|
|
1064
|
+
end
|
|
1065
|
+
private :drain_objective_proposals
|
|
1066
|
+
|
|
1052
1067
|
# The strategy the working set is built under now, as LLMContextView
|
|
1053
1068
|
# takes it: the running turn's, else the next turn's (#emit_working_set).
|
|
1054
1069
|
# /working-set's ops act under it.
|
|
@@ -1748,6 +1763,7 @@ module Samagotchi
|
|
|
1748
1763
|
# no Revise on its answer, verdict or not, as a reload does).
|
|
1749
1764
|
def end_turn(turn, outcome, save: false, exhausted: false)
|
|
1750
1765
|
seconds = turn.elapsed
|
|
1766
|
+
turn.outcome = outcome
|
|
1751
1767
|
synchronize_events do
|
|
1752
1768
|
kept, event = yield(seconds)
|
|
1753
1769
|
event[:turn_id] = turn.id
|
|
@@ -2322,9 +2338,13 @@ module Samagotchi
|
|
|
2322
2338
|
# :turn_started event's at and its user message's (and reminders') stamp.
|
|
2323
2339
|
# +verify+, +revises+: a Verify turn's claim, a Revise turn's draft.
|
|
2324
2340
|
Turn = Struct.new(:session, :prompt, :continue, :on_event, :controller, :origin, :started_at, :messages, :ended,
|
|
2325
|
-
:settings, :id, :limit, :resumed, :resume_from, :wall, :verify, :revises) do
|
|
2341
|
+
:settings, :id, :limit, :resumed, :resume_from, :wall, :verify, :revises, :outcome) do
|
|
2326
2342
|
def resumed? = resumed ? true : false
|
|
2327
2343
|
|
|
2344
|
+
# The turn ended as a completed one (not canceled, not failed): the
|
|
2345
|
+
# test the between-turns cards of it (an objectives proposal) use.
|
|
2346
|
+
def completed? = outcome == "completed"
|
|
2347
|
+
|
|
2328
2348
|
# The turn's boundary events carry the origin only when it names a
|
|
2329
2349
|
# sender (not TurnOrigin::NONE), so payloads stay unchanged for callers
|
|
2330
2350
|
# that don't pass one; as its wire Hash (TurnOrigin#to_h), which the
|
|
@@ -2448,7 +2468,10 @@ module Samagotchi
|
|
|
2448
2468
|
ensure
|
|
2449
2469
|
release_turn(probe_cancel_before)
|
|
2450
2470
|
show_context_cards_after_turn
|
|
2451
|
-
|
|
2471
|
+
# Only a completed turn shows its proposal cards: a canceled, failed
|
|
2472
|
+
# or rolled-back turn keeps none, and its proposals are dropped here
|
|
2473
|
+
# so the next turn does not show them either.
|
|
2474
|
+
turn.completed? ? show_objective_proposals : discard_objective_proposals(turn)
|
|
2452
2475
|
end
|
|
2453
2476
|
end
|
|
2454
2477
|
|
|
@@ -3076,10 +3099,14 @@ module Samagotchi
|
|
|
3076
3099
|
private :stage_tools
|
|
3077
3100
|
|
|
3078
3101
|
# Apply the staged tool sets, on the turn thread, before the turn's
|
|
3079
|
-
# system prompt is built (PluginTasks#apply_staged_tools!).
|
|
3102
|
+
# system prompt is built (PluginTasks#apply_staged_tools!). A changed
|
|
3103
|
+
# set may change the chi group the search tool's description counts
|
|
3104
|
+
# (a plugin tool the toolkit leaves out).
|
|
3080
3105
|
# @return [Boolean] whether the tools changed
|
|
3081
3106
|
def apply_staged_tools!
|
|
3082
|
-
@plugin_tasks.apply_staged_tools!
|
|
3107
|
+
changed = @plugin_tasks.apply_staged_tools!
|
|
3108
|
+
@tool_index.refresh! if changed
|
|
3109
|
+
changed
|
|
3083
3110
|
end
|
|
3084
3111
|
public :apply_staged_tools!
|
|
3085
3112
|
|
|
@@ -3208,14 +3235,15 @@ module Samagotchi
|
|
|
3208
3235
|
end.freeze
|
|
3209
3236
|
default_snapshot = ToolkitSuggest::Candidate.new(name: default.name.dup.freeze, description: nil,
|
|
3210
3237
|
rules: ToolkitSuggest.rules(default.resolve(kits)))
|
|
3211
|
-
|
|
3238
|
+
registered = @tools.entries(toolkit: nil).reject { |entry| entry.source == Tools::ToolIndex::SOURCE }
|
|
3239
|
+
core_tools, plugin_tools = registered.partition(&:core?).map { |entries| entries.map { |entry| entry.name.dup.freeze }.freeze }
|
|
3212
3240
|
state = ToolkitSuggest.question(
|
|
3213
3241
|
default: default, kits: kits, project: File.basename(session.project_root.to_s),
|
|
3214
3242
|
prompt: turn.prompt
|
|
3215
3243
|
).last
|
|
3216
3244
|
snapshot = ToolkitSuggest::Snapshot.new(
|
|
3217
3245
|
session_id: session.id.dup.freeze, turn_number: ToolkitSuggest.turn_number(session.messages),
|
|
3218
|
-
default: default_snapshot, candidates: snapshots, core_tools: core_tools,
|
|
3246
|
+
default: default_snapshot, candidates: snapshots, core_tools: core_tools, plugin_tools: plugin_tools,
|
|
3219
3247
|
state: { project: state[:project].dup.freeze, prompt: state[:prompt].dup.freeze }.freeze
|
|
3220
3248
|
)
|
|
3221
3249
|
threshold = Config.get("toolkit.suggest.threshold")
|
|
@@ -87,13 +87,14 @@ module Samagotchi
|
|
|
87
87
|
end
|
|
88
88
|
bundle_dir = File.join(MemoryBundle::Provenance.bundles_dir, bundle_name)
|
|
89
89
|
hooks_dir = File.join(bundle_dir, "hooks")
|
|
90
|
-
|
|
90
|
+
manifest = bundle.manifest
|
|
91
|
+
if manifest.experimental?
|
|
91
92
|
Log.info(:hooks, "experimental_bundle", echo: "[hooks] Bundle '#{bundle_name}' is experimental — its hooks may change or misbehave.", bundle: bundle_name)
|
|
92
93
|
end
|
|
93
94
|
begin
|
|
94
|
-
Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata:
|
|
95
|
+
Hooks::BundleLoader.load(bundle_name: bundle_name, hooks_dir: hooks_dir, metadata: manifest.hooks, registry: registry,
|
|
95
96
|
failures: @hook_failures, settings: settings[bundle_name.to_s] || {},
|
|
96
|
-
|
|
97
|
+
manifest: manifest, hook_failed: @hook_failed)
|
|
97
98
|
rescue Exception => e # rubocop:disable Lint/RescueException -- a hook's SyntaxError or exit must not stop chi
|
|
98
99
|
raise if e.is_a?(SignalException) # Ctrl-C and kill signals are the user's
|
|
99
100
|
|
|
@@ -228,8 +228,9 @@ module Samagotchi
|
|
|
228
228
|
@failures.add("rules (bundle #{bundle_name})", bundle.error, required: true, group: :rules)
|
|
229
229
|
next
|
|
230
230
|
end
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
manifest = bundle.manifest
|
|
232
|
+
if (too_new = manifest.requires_chi_failure)
|
|
233
|
+
rules.concat(too_new_bundle_rules(bundle_name, manifest, too_new))
|
|
233
234
|
next
|
|
234
235
|
end
|
|
235
236
|
dir = MemoryBundle::Provenance.new(name: bundle_name).guardrails_dir
|
|
@@ -323,7 +324,8 @@ module Samagotchi
|
|
|
323
324
|
# one that passes every gate is allowed once without reaching them
|
|
324
325
|
# (docs/guardrails.md, "System 1").
|
|
325
326
|
judgment = begin
|
|
326
|
-
system_one_judge(payload, targets: verdict.targets, ask_user: verdict.fresh_ask? || verdict.second_rule_ask
|
|
327
|
+
system_one_judge(payload, targets: verdict.targets, ask_user: verdict.fresh_ask? || verdict.second_rule_ask?,
|
|
328
|
+
other_rules: verdict.other_ask_rules)
|
|
327
329
|
rescue LLM::RequestCancelled
|
|
328
330
|
return turn_stopped(verdict)
|
|
329
331
|
end
|
|
@@ -367,8 +369,9 @@ module Samagotchi
|
|
|
367
369
|
# host) means no line at all. A judgment that fails is logged with p nil,
|
|
368
370
|
# so the failures are in the log too.
|
|
369
371
|
# With +ask_user+ (a fresh ask, a second rule's) it is judged and logged, never answered.
|
|
372
|
+
# +other_rules+ are the ids of the other rules asking about the same call.
|
|
370
373
|
# @return [Hash, nil] {id:, p:, auto:}, nil when nothing was written
|
|
371
|
-
def system_one_judge(payload, targets: nil, ask_user: false)
|
|
374
|
+
def system_one_judge(payload, targets: nil, ask_user: false, other_rules: nil)
|
|
372
375
|
return nil if Steer.participant_author?(turn_author)
|
|
373
376
|
|
|
374
377
|
client = @system_one.call
|
|
@@ -401,12 +404,16 @@ module Samagotchi
|
|
|
401
404
|
auto = !ask_user && system_one_auto_allow?(client, approval, p, entries, targets: targets)
|
|
402
405
|
decision = system_one_decision(client, auto)
|
|
403
406
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
407
|
+
fields = { session_id: @session_lookup.call&.id, rule: approval.rule, source: approval.source,
|
|
408
|
+
tool: approval.tool, **SystemOne::ApprovalQuestion.call_fields(approval),
|
|
409
|
+
repo_name: approval.repo_name, repo: targets&.repo, p: p,
|
|
410
|
+
threshold: Config.get("guardrails.system_one.threshold"), model: client.model,
|
|
411
|
+
latency_ms: latency_ms, decision: decision, origin: "live" }
|
|
412
|
+
# A call two rules ask about names only one; the others ride along so
|
|
413
|
+
# each rule's record (SystemOne::Record) counts this ask.
|
|
414
|
+
others = Array(other_rules).map(&:to_s).reject(&:empty?) - [approval.rule.to_s]
|
|
415
|
+
fields[:other_rules] = others unless others.empty?
|
|
416
|
+
id = log.judgment(id: run_id, **fields)
|
|
410
417
|
# A judgment the log cannot hold is no judgment: the user is asked.
|
|
411
418
|
system_one_run_facts(run_id, approval, targets, id.nil? && auto ? "asked" : decision)
|
|
412
419
|
{ id: id, p: p, auto: auto && !id.nil? }
|
|
@@ -520,7 +527,8 @@ module Samagotchi
|
|
|
520
527
|
# load the new ones). Never loaded (a chi older than the bundle it
|
|
521
528
|
# starts with): none, and a required load failure, as the hook loader
|
|
522
529
|
# does for a guardrail hook, so the calls aren't quietly unguarded.
|
|
523
|
-
|
|
530
|
+
# @param manifest [MemoryBundle::InstalledManifest]
|
|
531
|
+
def too_new_bundle_rules(bundle_name, manifest, reason)
|
|
524
532
|
kept = @loaded_bundle_rules[bundle_name]
|
|
525
533
|
unless kept
|
|
526
534
|
Log.warn(:guardrails, "bundle_rules_requires_chi", echo: "[samagotchi:guardrails] bundle #{bundle_name}: its rules are not loaded: #{reason}",
|
|
@@ -530,29 +538,30 @@ module Samagotchi
|
|
|
530
538
|
return []
|
|
531
539
|
end
|
|
532
540
|
|
|
533
|
-
key = [bundle_name,
|
|
541
|
+
key = [bundle_name, manifest.version.to_s]
|
|
534
542
|
return kept if @kept_noticed[key]
|
|
535
543
|
|
|
536
544
|
@kept_noticed[key] = true
|
|
537
|
-
Log.warn(:guardrails, "bundle_rules_kept", bundle: bundle_name, version:
|
|
538
|
-
requires_chi:
|
|
539
|
-
@notify.call(kept_notice(bundle_name,
|
|
545
|
+
Log.warn(:guardrails, "bundle_rules_kept", bundle: bundle_name, version: manifest.version.to_s,
|
|
546
|
+
requires_chi: manifest.requires_chi.to_s)
|
|
547
|
+
@notify.call(kept_notice(bundle_name, manifest))
|
|
540
548
|
kept
|
|
541
549
|
end
|
|
542
550
|
|
|
543
|
-
def kept_notice(bundle_name,
|
|
551
|
+
def kept_notice(bundle_name, manifest)
|
|
544
552
|
id = @session_lookup.call&.id || "ID"
|
|
545
|
-
"bundle #{bundle_name} #{
|
|
553
|
+
"bundle #{bundle_name} #{manifest.version} needs chi #{manifest.requires_chi} and this session runs chi #{Samagotchi::VERSION}: " \
|
|
546
554
|
"its new rules are not loaded, the ones loaded before still apply. Restart the session to load them " \
|
|
547
555
|
"(chi sessions stop #{id}, then chi --resume #{id})"
|
|
548
556
|
end
|
|
549
557
|
|
|
550
558
|
# [path, mtime, size] of config.yml and every installed bundle's
|
|
551
|
-
# manifest.json
|
|
559
|
+
# manifest.json, kept manifest.yml (its requires_chi is read from there)
|
|
560
|
+
# and guardrails/ file.
|
|
552
561
|
def rules_stamp
|
|
553
562
|
require_relative "memory_bundle/provenance"
|
|
554
563
|
paths = [Samagotchi::ConfigFile.global_path] +
|
|
555
|
-
Dir[File.join(MemoryBundle::Provenance.bundles_dir, "*", "{manifest.json,guardrails/*}")]
|
|
564
|
+
Dir[File.join(MemoryBundle::Provenance.bundles_dir, "*", "{manifest.json,manifest.yml,guardrails/*}")]
|
|
556
565
|
paths.compact.sort.map do |path|
|
|
557
566
|
stat = File.stat(path)
|
|
558
567
|
[path, stat.mtime.to_r, stat.size]
|
|
@@ -213,8 +213,8 @@ module Samagotchi
|
|
|
213
213
|
end
|
|
214
214
|
end
|
|
215
215
|
|
|
216
|
-
# What a session answer covers, as Approvals.key_for keys
|
|
217
|
-
# tool's later calls on the same paths, else this one exact call.
|
|
216
|
+
# What a session answer covers, as Guardrails::Approvals.key_for keys
|
|
217
|
+
# it: a file tool's later calls on the same paths, else this one exact call.
|
|
218
218
|
def session_subject(targets)
|
|
219
219
|
return "this call" if targets.nil? || targets.command || targets.paths.empty?
|
|
220
220
|
|
|
@@ -22,8 +22,9 @@ module Samagotchi
|
|
|
22
22
|
# resolves (a symlink in the project to ~/.ssh is ~/.ssh);
|
|
23
23
|
# memory_read: the project scope only, named (no fallback to system);
|
|
24
24
|
# anything else, call_tool's real target included: a call_tool of a
|
|
25
|
-
# built-in reaches the gate as
|
|
26
|
-
# so what is left of call_tool is
|
|
25
|
+
# chi tool (a built-in or a plugin's chi/<name>) reaches the gate as
|
|
26
|
+
# that tool (ToolRunner#unwrapped), so what is left of call_tool is
|
|
27
|
+
# an MCP tool, denied in v1.
|
|
27
28
|
# The declared tools stay the session's (no prompt rebuild per author),
|
|
28
29
|
# so a participant turn's model may still try a write: it is denied
|
|
29
30
|
# here, with the header's "This turn may only read" to keep it rare.
|
|
@@ -82,7 +83,7 @@ module Samagotchi
|
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
85
|
-
#
|
|
86
|
+
# An MCP tool behind call_tool names what it acts as.
|
|
86
87
|
def tool_label(tool, targets)
|
|
87
88
|
acts_as = targets&.acts_as
|
|
88
89
|
acts_as && acts_as != tool ? "#{tool} (#{acts_as})" : tool
|
|
@@ -325,6 +325,7 @@ module Samagotchi
|
|
|
325
325
|
if rule.verdict == :deny
|
|
326
326
|
verdict.deny!(rule.reason, rule: rule.id, source: rule.source, decided_by: "rule")
|
|
327
327
|
else
|
|
328
|
+
verdict.note_ask_rule(rule.id)
|
|
328
329
|
verdict.ask!(rule.reason, scopes: rule.scopes, rule: rule.id, source: rule.source, decided_by: "rule")
|
|
329
330
|
verdict.second_rule_asks!(rule.scopes) if asking && asking != rule.id
|
|
330
331
|
asking ||= rule.id
|
|
@@ -63,6 +63,26 @@ module Samagotchi
|
|
|
63
63
|
@note = nil
|
|
64
64
|
@voter = nil
|
|
65
65
|
@advice = nil
|
|
66
|
+
@ask_rules = []
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Every rule id that asked about this call, in the order they voted
|
|
70
|
+
# (Rules#check), including one whose vote did not take over. The ask
|
|
71
|
+
# names a single rule (#rule); the rest are #other_ask_rules.
|
|
72
|
+
attr_reader :ask_rules
|
|
73
|
+
|
|
74
|
+
# A rule that asks, recorded whatever became of its vote.
|
|
75
|
+
def note_ask_rule(rule)
|
|
76
|
+
id = rule.to_s
|
|
77
|
+
@ask_rules << id unless id.empty? || @ask_rules.include?(id)
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The rules that asked besides the one the ask names: nil when only one
|
|
82
|
+
# rule asked, so a judgment line carries nothing extra.
|
|
83
|
+
def other_ask_rules
|
|
84
|
+
others = @ask_rules - [rule.to_s]
|
|
85
|
+
others.empty? ? nil : others
|
|
66
86
|
end
|
|
67
87
|
|
|
68
88
|
# @param rule [String, nil] the rule id, when a rule decided
|
|
@@ -6,8 +6,6 @@ require_relative "../log"
|
|
|
6
6
|
require_relative "../guardrails/load_failures"
|
|
7
7
|
require_relative "../memory_bundle/bundle_hook"
|
|
8
8
|
require_relative "../memory_bundle/provenance"
|
|
9
|
-
require_relative "../memory_bundle/manifest"
|
|
10
|
-
require_relative "../version"
|
|
11
9
|
|
|
12
10
|
module Samagotchi
|
|
13
11
|
# Namespace that wraps hook plugin classes per-bundle. Each bundle's hooks are
|
|
@@ -57,17 +55,17 @@ module Samagotchi
|
|
|
57
55
|
# @param settings [Hash] the bundle's section of config.yml
|
|
58
56
|
# `bundles:` (string keys); a hook class whose initialize takes an
|
|
59
57
|
# argument gets it
|
|
60
|
-
# @param
|
|
61
|
-
# this chi doesn't meet
|
|
62
|
-
# plugin loader does)
|
|
58
|
+
# @param manifest [MemoryBundle::InstalledManifest, nil] the installed
|
|
59
|
+
# bundle's manifest; when this chi doesn't meet its requires_chi no
|
|
60
|
+
# hook loads (each is reported, as the plugin loader does)
|
|
63
61
|
# @param hook_failed [#call, nil] (label, "Class: message", effect:)
|
|
64
62
|
# when a hook file raises at fire time — the Engine's HookRoutes;
|
|
65
63
|
# nil when there is no UI to tell (the test kit)
|
|
66
64
|
# @return [Integer] number of hooks successfully registered
|
|
67
|
-
def load(bundle_name:, hooks_dir:, metadata:, registry:, failures: nil, settings: {},
|
|
65
|
+
def load(bundle_name:, hooks_dir:, metadata:, registry:, failures: nil, settings: {}, manifest: nil, hook_failed: nil)
|
|
68
66
|
return 0 unless metadata.is_a?(Hash)
|
|
69
67
|
|
|
70
|
-
too_old =
|
|
68
|
+
too_old = manifest&.requires_chi_failure
|
|
71
69
|
loaded = 0
|
|
72
70
|
metadata.each do |raw_basename, raw_meta|
|
|
73
71
|
basename = raw_basename.to_s
|
|
@@ -197,8 +197,8 @@ module Samagotchi
|
|
|
197
197
|
return texts ? [[refs.first, call, texts.first.text]] : []
|
|
198
198
|
end
|
|
199
199
|
|
|
200
|
-
# A call_tool of a built-in ran as it: its
|
|
201
|
-
#
|
|
200
|
+
# A call_tool of a chi tool (a built-in or a plugin's) ran as it: its
|
|
201
|
+
# run's lead names the tool (BuiltinCalls.unwrap_saved).
|
|
202
202
|
calls = batch.map(&:last)
|
|
203
203
|
calls = calls.zip(ToolResponse.runs(entry[:content], calls.size)).map do |call, run|
|
|
204
204
|
Tools::BuiltinCalls.unwrap_saved(Tools::BuiltinCalls.wrap_saved(call, run&.text), run&.text)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
|
|
4
|
+
require_relative "file_digest"
|
|
5
5
|
require_relative "bundle_hook"
|
|
6
6
|
require_relative "manifest"
|
|
7
7
|
require_relative "provenance"
|
|
@@ -36,8 +36,10 @@ module Samagotchi
|
|
|
36
36
|
# @param results, warnings, conflicts the Installer's (results and
|
|
37
37
|
# warnings are added to)
|
|
38
38
|
# @param trash [Trash] the Installer's
|
|
39
|
+
# @param chi_version [String] the chi this install is for (the Installer's;
|
|
40
|
+
# a caller that checks its own bundles against another version passes it)
|
|
39
41
|
def initialize(name:, source_dir:, manifest:, provenance:, existing:, results:, warnings:, conflicts:, trash:,
|
|
40
|
-
force: false, strict: true, upgrade: false, dry_run: false, dev: false)
|
|
42
|
+
force: false, strict: true, upgrade: false, dry_run: false, dev: false, chi_version: Samagotchi::VERSION)
|
|
41
43
|
@name = name
|
|
42
44
|
@bundle_dir = source_dir
|
|
43
45
|
@manifest = manifest
|
|
@@ -52,6 +54,7 @@ module Samagotchi
|
|
|
52
54
|
@upgrade = upgrade
|
|
53
55
|
@dry_run = dry_run
|
|
54
56
|
@dev = dev
|
|
57
|
+
@chi_version = chi_version
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
# @return [Installed]
|
|
@@ -175,7 +178,7 @@ module Samagotchi
|
|
|
175
178
|
dest = File.join(@provenance.hooks_dir, basename)
|
|
176
179
|
next unless File.exist?(dest)
|
|
177
180
|
|
|
178
|
-
actual =
|
|
181
|
+
actual = FileDigest.hex(dest)
|
|
179
182
|
if actual != expected
|
|
180
183
|
@warnings << "Checksum mismatch for hook #{basename}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
181
184
|
end
|
|
@@ -229,7 +232,7 @@ module Samagotchi
|
|
|
229
232
|
raise Installer::InstallError, "the manifest names plugin #{plugin[:file]}, which the bundle doesn't have" unless File.file?(src)
|
|
230
233
|
raise Installer::InstallError, "plugin #{plugin[:file]} is a symlink (not installed)" if linked?(src)
|
|
231
234
|
|
|
232
|
-
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi,
|
|
235
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
233
236
|
@warnings << "Plugin #{plugin[:file]} won't load: #{failure}"
|
|
234
237
|
end
|
|
235
238
|
if @dry_run
|
|
@@ -251,7 +254,7 @@ module Samagotchi
|
|
|
251
254
|
{ status: "installed" }
|
|
252
255
|
end
|
|
253
256
|
expected = @manifest.checksum_for_plugin
|
|
254
|
-
actual =
|
|
257
|
+
actual = FileDigest.hex(dest)
|
|
255
258
|
if @strict && !@dev && expected && actual != expected
|
|
256
259
|
@warnings << "Checksum mismatch for plugin #{plugin[:file]}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
257
260
|
end
|
|
@@ -291,7 +294,7 @@ module Samagotchi
|
|
|
291
294
|
else { status: "installed" }
|
|
292
295
|
end
|
|
293
296
|
FileUtils.chmod(0o755, dest) unless @dev
|
|
294
|
-
actual =
|
|
297
|
+
actual = FileDigest.hex(dest)
|
|
295
298
|
expected = declared.to_s.delete_prefix("sha256:")
|
|
296
299
|
if @strict && !@dev && !expected.empty? && actual != expected
|
|
297
300
|
@warnings << "Checksum mismatch for script #{file}: expected #{expected[0..7]}..., got #{actual[0..7]}..."
|
|
@@ -341,7 +344,7 @@ module Samagotchi
|
|
|
341
344
|
def warn_hooks_requires_chi
|
|
342
345
|
return if @manifest.nil? || @bundle_hooks.empty?
|
|
343
346
|
|
|
344
|
-
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi,
|
|
347
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
345
348
|
@warnings << "Bundle #{@name}: its hooks won't load: #{failure}"
|
|
346
349
|
end
|
|
347
350
|
end
|
|
@@ -353,7 +356,7 @@ module Samagotchi
|
|
|
353
356
|
def warn_rules_requires_chi(rule_files)
|
|
354
357
|
return if @manifest.nil? || rule_files.empty?
|
|
355
358
|
|
|
356
|
-
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi,
|
|
359
|
+
if (failure = Manifest.requires_chi_failure(@manifest.requires_chi, @chi_version))
|
|
357
360
|
@warnings << "Bundle #{@name}: its guardrail rules won't load: #{failure}; until chi is updated " \
|
|
358
361
|
"(chi update), guarded tool calls are refused"
|
|
359
362
|
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
|
+
require_relative "file_digest"
|
|
4
5
|
require "fileutils"
|
|
5
6
|
require "tmpdir"
|
|
6
7
|
require_relative "bundle_hook"
|
|
7
8
|
require_relative "manifest"
|
|
8
9
|
require_relative "placeholder"
|
|
9
|
-
require_relative "../context_providers"
|
|
10
10
|
require_relative "../memory_paths"
|
|
11
11
|
require_relative "../model_overlay"
|
|
12
12
|
|
|
@@ -140,7 +140,8 @@ module Samagotchi
|
|
|
140
140
|
|
|
141
141
|
# The installed bundle's hooks (a recorded one missing on disk left
|
|
142
142
|
# out; one with no recorded sha gets its file's), guardrail rules,
|
|
143
|
-
# plugin, scripts
|
|
143
|
+
# plugin, scripts, context providers and block renderers (its
|
|
144
|
+
# InstalledManifest's). Without hooks or rules in a
|
|
144
145
|
# record, the scope dir's hooks/*.rb and guardrails/*.yml (building
|
|
145
146
|
# from a raw bundle dir that is also the memories dir).
|
|
146
147
|
# @return [InstalledParts]
|
|
@@ -174,13 +175,18 @@ module Samagotchi
|
|
|
174
175
|
|
|
175
176
|
rules_dir = record&.guardrails&.any? ? prov.guardrails_dir : File.join(target_dir, "guardrails")
|
|
176
177
|
plugin = record ? prov.plugin_path(record) : nil
|
|
178
|
+
manifest = record&.manifest
|
|
179
|
+
# A key of the record that doesn't parse isn't built as empty: the build refuses, as it always has.
|
|
180
|
+
problem = manifest&.key_problems&.values&.first
|
|
181
|
+
raise BuildError, "the installed #{name}'s record: #{problem}" if problem
|
|
182
|
+
|
|
177
183
|
InstalledParts.new(
|
|
178
184
|
record: record, hooks: hooks, hook_files: hook_files,
|
|
179
185
|
rules: Dir.exist?(rules_dir) ? Dir.glob(File.join(rules_dir, "*.yml")).sort : [],
|
|
180
186
|
plugin: plugin && File.file?(plugin) ? plugin : nil,
|
|
181
187
|
scripts: installed_scripts(prov, record),
|
|
182
|
-
context_providers:
|
|
183
|
-
block_renderers:
|
|
188
|
+
context_providers: manifest ? manifest.context_providers : [],
|
|
189
|
+
block_renderers: manifest ? manifest.block_renderers : []
|
|
184
190
|
)
|
|
185
191
|
end
|
|
186
192
|
|
|
@@ -204,33 +210,45 @@ module Samagotchi
|
|
|
204
210
|
end
|
|
205
211
|
|
|
206
212
|
# manifest.yml for the staged bundle: requires_chi, needs, session_settings and settings (typed) and the
|
|
207
|
-
# file descriptions come from the installed bundle's
|
|
208
|
-
# trust_level from the CLI else
|
|
213
|
+
# file descriptions come from the installed bundle's manifest (InstalledManifest), the
|
|
214
|
+
# trust_level and description from the CLI else that manifest. A key of its kept manifest.yml
|
|
215
|
+
# this chi doesn't know is carried as installed (the build is the bundle again), with a warning.
|
|
209
216
|
def write_manifest(staging, name, version, files_map, parts)
|
|
210
|
-
|
|
217
|
+
manifest = parts.record&.manifest
|
|
211
218
|
trust_level = @trust_level
|
|
212
|
-
trust_level =
|
|
219
|
+
trust_level = manifest.trust_level if (trust_level.nil? || trust_level.empty?) && manifest&.trust_level
|
|
220
|
+
description = @description.empty? && manifest&.description ? manifest.description : @description
|
|
213
221
|
Manifest.write(
|
|
214
222
|
dir: staging,
|
|
215
223
|
name: name,
|
|
216
224
|
version: version,
|
|
217
225
|
scope: @scope,
|
|
218
|
-
description:
|
|
226
|
+
description: description,
|
|
219
227
|
files: files_map,
|
|
220
228
|
hooks: parts.hooks.empty? ? nil : parts.hooks,
|
|
221
229
|
trust_level: trust_level,
|
|
222
|
-
plugin: parts.plugin && { file: File.basename(parts.plugin), sha256:
|
|
223
|
-
requires_chi:
|
|
224
|
-
needs:
|
|
225
|
-
scripts: parts.scripts.transform_values { |src|
|
|
230
|
+
plugin: parts.plugin && { file: File.basename(parts.plugin), sha256: FileDigest.sha256(parts.plugin) },
|
|
231
|
+
requires_chi: manifest&.requires_chi,
|
|
232
|
+
needs: manifest&.needs,
|
|
233
|
+
scripts: parts.scripts.transform_values { |src| FileDigest.sha256(src) },
|
|
226
234
|
context_providers: parts.context_providers,
|
|
227
235
|
block_renderers: parts.block_renderers,
|
|
228
|
-
file_descriptions: file_descriptions(
|
|
229
|
-
session_settings:
|
|
230
|
-
session_setting_types:
|
|
231
|
-
settings:
|
|
232
|
-
setting_types:
|
|
236
|
+
file_descriptions: manifest ? manifest.file_descriptions.slice(*files_map.keys) : {},
|
|
237
|
+
session_settings: manifest&.session_settings || [],
|
|
238
|
+
session_setting_types: manifest&.session_setting_types || {},
|
|
239
|
+
settings: manifest&.settings,
|
|
240
|
+
setting_types: manifest&.setting_types || {}
|
|
233
241
|
)
|
|
242
|
+
carry_unknown_keys(staging, parts.record, manifest) if manifest&.origin == :kept && !manifest.unknown_keys.empty?
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def carry_unknown_keys(staging, record, manifest)
|
|
246
|
+
path = File.join(staging, "manifest.yml")
|
|
247
|
+
kept = YAML.load_file(record.kept_manifest_path) || {}
|
|
248
|
+
File.write(path, YAML.dump(YAML.load_file(path).merge(kept.slice(*manifest.unknown_keys))))
|
|
249
|
+
manifest.unknown_keys.each do |key|
|
|
250
|
+
@warnings << "#{Manifest.unknown_key_line(key)}; the build carries it as installed"
|
|
251
|
+
end
|
|
234
252
|
end
|
|
235
253
|
|
|
236
254
|
# The staged bundle to +out+: a zip or tar archive, or a directory,
|
|
@@ -279,18 +297,6 @@ module Samagotchi
|
|
|
279
297
|
.select { |_, path| File.file?(path) }
|
|
280
298
|
end
|
|
281
299
|
|
|
282
|
-
# The descriptions the installed bundle's record keeps for the files
|
|
283
|
-
# being built (its manifest's files: mappings), so a rebuild writes
|
|
284
|
-
# them back; {} without a record.
|
|
285
|
-
def file_descriptions(record, files_map)
|
|
286
|
-
return {} unless record
|
|
287
|
-
|
|
288
|
-
record.files.each_with_object({}) do |(key, entry), acc|
|
|
289
|
-
text = entry[:description].to_s
|
|
290
|
-
acc[key] = text if files_map.key?(key) && !text.empty?
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
300
|
# Without an allowlist, a memory another installed bundle owns (its
|
|
295
301
|
# record lists it) isn't the user's to share: left out, one line
|
|
296
302
|
# each. The bundle being built (same name) keeps its own.
|