samagotchi 0.154.1 → 0.156.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/bin/chi +1 -1
- data/docs/architecture.md +12 -7
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +5 -5
- data/docs/internals/attached-context.md +9 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +642 -831
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/llm-context-forget.md +8 -3
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +13 -6
- data/docs/internals/remote-sessions.md +3 -1
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/system-one.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +1 -1
- data/lib/samagotchi/broadcast_command.rb +3 -3
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +7 -51
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/cli/parent_wait.rb +1 -1
- data/lib/samagotchi/config.rb +17 -112
- data/lib/samagotchi/context_command.rb +1 -2
- data/lib/samagotchi/context_window.rb +3 -3
- data/lib/samagotchi/desktop/macos.rb +1 -1
- data/lib/samagotchi/engine/context_judge.rb +21 -119
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/models.rb +5 -2
- data/lib/samagotchi/engine/objectives.rb +30 -80
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +26 -13
- data/lib/samagotchi/engine/toolkit.rb +35 -191
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +166 -98
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +28 -11
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/host_config.rb +72 -1
- data/lib/samagotchi/host_model.rb +2 -6
- data/lib/samagotchi/host_registry.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +215 -313
- data/lib/samagotchi/llm/anthropic_wire.rb +2 -2
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +87 -49
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_apply.rb +54 -1
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/llm_context_notice.rb +1 -1
- data/lib/samagotchi/llm_context_strategy.rb +7 -9
- data/lib/samagotchi/llm_context_view.rb +9 -0
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/model_price.rb +4 -10
- data/lib/samagotchi/model_profile.rb +6 -6
- data/lib/samagotchi/model_settings.rb +37 -2
- data/lib/samagotchi/note_command.rb +1 -2
- data/lib/samagotchi/objectives_audit.rb +4 -3
- data/lib/samagotchi/participants.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +1 -1
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/plugin/sessions.rb +2 -5
- data/lib/samagotchi/plugin/tool_spec.rb +5 -3
- data/lib/samagotchi/project_env.rb +1 -1
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/reasoning_history.rb +3 -3
- data/lib/samagotchi/reminder_carry.rb +1 -1
- data/lib/samagotchi/reprefill_meter.rb +45 -0
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/sampling_settings.rb +3 -3
- data/lib/samagotchi/self_report.rb +7 -7
- data/lib/samagotchi/session.rb +50 -7
- data/lib/samagotchi/session_access/local.rb +7 -5
- data/lib/samagotchi/session_access/remote.rb +17 -3
- data/lib/samagotchi/session_archive_command.rb +2 -2
- data/lib/samagotchi/session_chain.rb +7 -6
- data/lib/samagotchi/session_continue.rb +6 -5
- data/lib/samagotchi/session_delete_command.rb +1 -1
- data/lib/samagotchi/session_manager/delivery.rb +254 -0
- data/lib/samagotchi/session_manager/emptiness.rb +138 -0
- data/lib/samagotchi/session_manager/lifecycle.rb +274 -0
- data/lib/samagotchi/session_manager/listing.rb +189 -0
- data/lib/samagotchi/session_manager/removal.rb +190 -0
- data/lib/samagotchi/session_manager/spawn.rb +242 -0
- data/lib/samagotchi/session_manager/worker_command.rb +157 -0
- data/lib/samagotchi/session_manager.rb +26 -1540
- data/lib/samagotchi/session_metrics.rb +1 -1
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/share_command.rb +1 -1
- data/lib/samagotchi/starting_marker.rb +65 -0
- data/lib/samagotchi/steer_cut_after.rb +3 -3
- data/lib/samagotchi/system_one/recorder.rb +49 -0
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/terminal_ui/exit_lines.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +2 -6
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/testing/sessions.rb +1 -1
- data/lib/samagotchi/thinking.rb +3 -3
- data/lib/samagotchi/token_usage.rb +1 -1
- data/lib/samagotchi/tool_activity.rb +15 -7
- data/lib/samagotchi/tool_declarations.rb +6 -6
- data/lib/samagotchi/tool_output_cut.rb +17 -0
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +96 -102
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/toolkit_command.rb +1 -1
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/delegate.rb +10 -5
- data/lib/samagotchi/tools/delegate_result.rb +4 -4
- data/lib/samagotchi/tools/index_hint.rb +14 -0
- data/lib/samagotchi/tools/registry.rb +64 -44
- data/lib/samagotchi/tools/send_note.rb +4 -6
- data/lib/samagotchi/tools/tool_index.rb +150 -102
- data/lib/samagotchi/tools/tool_surface.rb +187 -0
- data/lib/samagotchi/turn_context.rb +68 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +172 -0
- data/lib/samagotchi/turn_stages/objectives.rb +150 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +257 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +168 -0
- data/lib/samagotchi/turn_stages/warmup.rb +82 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +3 -3
- data/lib/samagotchi/web/app.rb +15 -8
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +3 -3
- data/lib/samagotchi/web/session_create.rb +3 -3
- data/lib/samagotchi/web/session_hub.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +3 -3
- data/lib/samagotchi/web/session_summary.rb +3 -4
- data/lib/samagotchi/worker.rb +55 -27
- data/lib/samagotchi/worker_boot.rb +184 -0
- data/lib/samagotchi/worker_signals.rb +1 -1
- data/lib/samagotchi/worker_task_wakes.rb +2 -2
- metadata +29 -1
|
@@ -24,8 +24,17 @@ module Samagotchi
|
|
|
24
24
|
# a tool call is judged in, the approval question, and the Gate the kernel
|
|
25
25
|
# asks before every tool call.
|
|
26
26
|
#
|
|
27
|
-
# What it needs from the Engine comes through lookups, read at call time
|
|
27
|
+
# What it needs from the Engine comes through lookups, read at call time;
|
|
28
|
+
# what it needs of a turn (its author, participant mode, git facts,
|
|
29
|
+
# origin) comes with the turn's TurnContext, which the Gate is given per
|
|
30
|
+
# call. It keeps no per-turn state.
|
|
28
31
|
class GuardrailWiring
|
|
32
|
+
# What #begin_turn decides for a turn, once (Engine#begin_turn puts it
|
|
33
|
+
# in the TurnContext): its Guardrails::GitInfo (asked afresh), the
|
|
34
|
+
# participant's Participants::Entry and their turn's mode (nil both for
|
|
35
|
+
# the owner's turns).
|
|
36
|
+
TurnStart = Data.define(:git, :participant, :mode)
|
|
37
|
+
|
|
29
38
|
# @return [Guardrails::LoadFailures] what failed to load (hooks, rules)
|
|
30
39
|
attr_reader :failures
|
|
31
40
|
|
|
@@ -33,11 +42,10 @@ module Samagotchi
|
|
|
33
42
|
attr_reader :approvals
|
|
34
43
|
|
|
35
44
|
# @param scratch [Boolean] a `chi scratch` session (writes into the memories are denied)
|
|
36
|
-
# @param
|
|
37
|
-
#
|
|
45
|
+
# @param tools [#call] → Tools::Registry: an approval's plugin label.
|
|
46
|
+
# A lookup: the Engine builds its wiring (and so the load failures its
|
|
47
|
+
# hooks are loaded into, ExtensionLoad) before its tools and hooks.
|
|
38
48
|
# @param session [#call] → Session, nil
|
|
39
|
-
# @param model_key [#call] → String, the effective model's overlay key
|
|
40
|
-
# @param model_name [#call] → String, nil: the effective model's bare name
|
|
41
49
|
# @param cancelled [#call] → Boolean, whether the running turn is cancelled
|
|
42
50
|
# @param ask [#call] (fields) → the answer; the Engine's question flow
|
|
43
51
|
# @param notify [#call] (message) → shows a guardrails line in every UI
|
|
@@ -50,15 +58,11 @@ module Samagotchi
|
|
|
50
58
|
# @param participant [#call] (author) → the participant's
|
|
51
59
|
# Participants::Entry (participants.json: their name, their grant's
|
|
52
60
|
# mode), nil for none
|
|
53
|
-
def initialize(scratch:,
|
|
54
|
-
|
|
55
|
-
participant: ->(_author) {})
|
|
61
|
+
def initialize(scratch:, tools:, session:, cancelled:, ask:, notify: ->(_message) {}, system_one: -> {},
|
|
62
|
+
system_one_runs: -> {}, cancel_controller: -> {}, participant: ->(_author) {})
|
|
56
63
|
@scratch = scratch
|
|
57
|
-
@hooks_lookup = hooks
|
|
58
64
|
@tools_lookup = tools
|
|
59
65
|
@session_lookup = session
|
|
60
|
-
@model_key_lookup = model_key
|
|
61
|
-
@model_name_lookup = model_name
|
|
62
66
|
@cancelled_lookup = cancelled
|
|
63
67
|
@ask = ask
|
|
64
68
|
@notify = notify
|
|
@@ -66,15 +70,12 @@ module Samagotchi
|
|
|
66
70
|
@system_one_runs = system_one_runs
|
|
67
71
|
@cancel_controller = cancel_controller
|
|
68
72
|
@participant = participant
|
|
69
|
-
@turn_mode = nil
|
|
70
73
|
# Bundle name → the rules last loaded from it whole, kept while the
|
|
71
74
|
# installed version needs a newer chi; [name, version] said so once.
|
|
72
75
|
@loaded_bundle_rules = {}
|
|
73
76
|
@kept_noticed = {}
|
|
74
77
|
@failures = Guardrails::LoadFailures.new
|
|
75
78
|
@rules_mutex = Mutex.new
|
|
76
|
-
@git = Guardrails::GitInfo.new
|
|
77
|
-
@origin = TurnOrigin::NONE
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Who can answer an approval: :worker or :non_interactive (the
|
|
@@ -104,53 +105,44 @@ module Samagotchi
|
|
|
104
105
|
@protected = nil
|
|
105
106
|
end
|
|
106
107
|
|
|
107
|
-
# A turn starts
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
author =
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
# A turn starts (Engine#begin_turn, before it is published): the git
|
|
109
|
+
# facts read afresh, and a participant's turn's mode decided once for
|
|
110
|
+
# the whole turn from its origin (Engine#run_turn's TurnOrigin; nil or a
|
|
111
|
+
# Hash is read as one, TurnOrigin.of). Nothing is kept here.
|
|
112
|
+
# @param session [Session, nil] the turn's
|
|
113
|
+
# @return [TurnStart]
|
|
114
|
+
def begin_turn(origin, session:)
|
|
115
|
+
author = TurnOrigin.of(origin).participant_author
|
|
116
|
+
entry = author && @participant.call(author)
|
|
117
|
+
TurnStart.new(git: Guardrails::GitInfo.new, participant: entry,
|
|
118
|
+
mode: author && participant_turn_mode(session, entry))
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
# A participant's turn's mode: in a Verify fresh child the one saved
|
|
120
122
|
# on it at spawn (VerifyFreshOrigin#turn_mode: :review unless saved
|
|
121
|
-
# :pair), never looked up again; elsewhere ParticipantMode.for_turn
|
|
123
|
+
# :pair), never looked up again; elsewhere ParticipantMode.for_turn
|
|
124
|
+
# (:pair only in a working session for a participant invited to act).
|
|
122
125
|
def participant_turn_mode(session, entry)
|
|
123
126
|
fresh = session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
124
127
|
fresh ? fresh.turn_mode : ParticipantMode.for_turn(session, entry)
|
|
125
128
|
end
|
|
126
129
|
private :participant_turn_mode
|
|
127
130
|
|
|
128
|
-
# The
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# @
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# Who wrote the running turn when it isn't the session's owner (its
|
|
136
|
-
# origin's participant author); nil for the owner's turns and every
|
|
137
|
-
# other origin.
|
|
138
|
-
# @return [String, nil]
|
|
139
|
-
def turn_author = @origin.participant_author
|
|
140
|
-
|
|
141
|
-
# The gate every tool call asks first.
|
|
131
|
+
# The gate every tool call asks first, given the call's TurnContext
|
|
132
|
+
# (Guardrails::Gate#evaluate's context:): its judged context (#context),
|
|
133
|
+
# its checks (#checks) and its approval (#request_approval) are that
|
|
134
|
+
# turn's.
|
|
135
|
+
# @param hooks [Hooks::Registry, nil] the voters (the Engine's)
|
|
136
|
+
# @param tools [Tools::Registry, nil] the session's tools (the Engine's)
|
|
142
137
|
# @return [Guardrails::Gate]
|
|
143
|
-
def gate
|
|
138
|
+
def gate(hooks:, tools:)
|
|
144
139
|
Guardrails::Gate.new(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
approver: ->(verdict) { request_approval(verdict) },
|
|
140
|
+
hooks: hooks,
|
|
141
|
+
tools: tools,
|
|
142
|
+
context_for: ->(turn) { context(turn) },
|
|
143
|
+
approver: ->(verdict, turn) { request_approval(verdict, turn) },
|
|
150
144
|
approvals_lookup: -> { @approvals },
|
|
151
|
-
|
|
152
|
-
cancelled_lookup: -> { @cancelled_lookup.call },
|
|
153
|
-
tools_lookup: -> { @tools_lookup.call }
|
|
145
|
+
checks_for: ->(turn) { checks(turn) }
|
|
154
146
|
)
|
|
155
147
|
end
|
|
156
148
|
|
|
@@ -161,39 +153,42 @@ module Samagotchi
|
|
|
161
153
|
# ParticipantMode: a review turn's (ReviewBoundary) or a pair turn's
|
|
162
154
|
# (PairBoundary). Either denies what a participant's turn may not do,
|
|
163
155
|
# never asks about it; both are no-ops for the owner's turns.
|
|
164
|
-
|
|
156
|
+
# @param turn [TurnContext] the call's turn
|
|
157
|
+
def checks(turn)
|
|
165
158
|
rules = self.rules
|
|
166
|
-
[@failures, participant_boundary, rules.hook_asks, protected_paths,
|
|
167
|
-
(@scratch_writes ||= Guardrails::ScratchWrites.new if @scratch), child_boundary, rules].compact
|
|
159
|
+
[@failures, participant_boundary(turn), rules.hook_asks, protected_paths,
|
|
160
|
+
(@scratch_writes ||= Guardrails::ScratchWrites.new if @scratch), child_boundary(turn), rules].compact
|
|
168
161
|
end
|
|
169
162
|
|
|
170
|
-
# The boundary for a participant's turn in this session (#checks)
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
# The boundary for a participant's turn in this session (#checks), by
|
|
164
|
+
# the turn's mode.
|
|
165
|
+
def participant_boundary(turn)
|
|
166
|
+
turn.participant_mode == ParticipantMode::PAIR ? pair_boundary(turn) : review_boundary(turn)
|
|
173
167
|
end
|
|
174
168
|
|
|
175
169
|
# A participant's review turn's boundary (Guardrails::ReviewBoundary): a
|
|
176
|
-
# no-op unless
|
|
177
|
-
def review_boundary
|
|
178
|
-
|
|
170
|
+
# no-op unless +turn+'s author is a participant's.
|
|
171
|
+
def review_boundary(turn)
|
|
172
|
+
Guardrails::ReviewBoundary.new(author: -> { turn.author }, root: -> { boundary_root(turn.git) })
|
|
179
173
|
end
|
|
180
174
|
|
|
181
175
|
# A participant's pair turn's boundary (Guardrails::PairBoundary): a
|
|
182
|
-
# no-op unless
|
|
183
|
-
def pair_boundary
|
|
184
|
-
|
|
176
|
+
# no-op unless +turn+'s author is a participant's.
|
|
177
|
+
def pair_boundary(turn)
|
|
178
|
+
Guardrails::PairBoundary.new(author: -> { turn.author }, root: -> { boundary_root(turn.git) })
|
|
185
179
|
end
|
|
186
180
|
|
|
187
|
-
# The delegate child's boundary (Guardrails::ChildBoundary)
|
|
188
|
-
# any other session.
|
|
189
|
-
def child_boundary
|
|
181
|
+
# The delegate child's boundary (Guardrails::ChildBoundary), on +turn+'s
|
|
182
|
+
# git facts; nil for any other session.
|
|
183
|
+
def child_boundary(turn)
|
|
190
184
|
return nil unless @session_lookup.call&.delegate?
|
|
191
185
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
186
|
+
git = turn.git
|
|
187
|
+
Guardrails::ChildBoundary.new(
|
|
188
|
+
root: -> { boundary_root(git) },
|
|
189
|
+
worktrees: -> { (root = boundary_root(git)) ? git.worktrees(root) : [] },
|
|
190
|
+
common_dir: -> { (root = boundary_root(git)) && git.common_dir(root) },
|
|
191
|
+
branch: -> { (root = boundary_root(git)) && git.branch(root) }
|
|
197
192
|
)
|
|
198
193
|
end
|
|
199
194
|
|
|
@@ -285,11 +280,13 @@ module Samagotchi
|
|
|
285
280
|
end
|
|
286
281
|
end
|
|
287
282
|
|
|
288
|
-
# The context the gate sees for a tool call now
|
|
283
|
+
# The context the gate sees for a tool call of +turn+ now: the process
|
|
284
|
+
# cwd, the session, the interface, the turn's origin and git facts.
|
|
285
|
+
# @param turn [TurnContext]
|
|
289
286
|
# @return [Guardrails::Context]
|
|
290
|
-
def context
|
|
287
|
+
def context(turn)
|
|
291
288
|
Guardrails::Context.new(cwd: Dir.pwd, session_id: @session_lookup.call&.id, interface: interface,
|
|
292
|
-
origin:
|
|
289
|
+
origin: turn.origin.wire, git: turn.git)
|
|
293
290
|
end
|
|
294
291
|
|
|
295
292
|
# Ask the user to approve a call the gate voted `ask` on, through the
|
|
@@ -297,8 +294,10 @@ module Samagotchi
|
|
|
297
294
|
# verdict: allow with the picked scope, or deny with a note for the
|
|
298
295
|
# model. A --non-interactive run has no one to ask and denies at once.
|
|
299
296
|
# @param verdict [Guardrails::Verdict]
|
|
297
|
+
# @param turn [TurnContext, nil] the call's turn (its author, the
|
|
298
|
+
# participant's name); nil: the owner's
|
|
300
299
|
# @return [Guardrails::Verdict]
|
|
301
|
-
def request_approval(verdict)
|
|
300
|
+
def request_approval(verdict, turn = nil)
|
|
302
301
|
if interface == :non_interactive
|
|
303
302
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it (non-interactive run).")
|
|
304
303
|
end
|
|
@@ -313,10 +312,10 @@ module Samagotchi
|
|
|
313
312
|
# verdict.call is the call that will run (a hook may have replaced it).
|
|
314
313
|
# A participant's turn's ask names them and offers once only
|
|
315
314
|
# (Approval.payload's author:), whichever session it is.
|
|
316
|
-
author =
|
|
315
|
+
author = turn&.author
|
|
317
316
|
payload = Guardrails::Approval.payload(verdict, label: label, preview: approval_preview(verdict.call),
|
|
318
317
|
only: (SCRATCH_SCOPES if @scratch), author: author,
|
|
319
|
-
author_name: (
|
|
318
|
+
author_name: (turn.participant&.name if author))
|
|
320
319
|
# A call on chi's own config, hooks or guardrails is the user's to
|
|
321
320
|
# allow, never a parent agent's (Guardrails::ParentPolicy).
|
|
322
321
|
payload[:user_only] = Guardrails::ParentApprovals.protected_facts?(payload[:approval])
|
|
@@ -324,8 +323,8 @@ module Samagotchi
|
|
|
324
323
|
# one that passes every gate is allowed once without reaching them
|
|
325
324
|
# (docs/guardrails.md, "System 1").
|
|
326
325
|
judgment = begin
|
|
327
|
-
system_one_judge(payload,
|
|
328
|
-
|
|
326
|
+
system_one_judge(payload, author: author, targets: verdict.targets,
|
|
327
|
+
ask_user: verdict.fresh_ask? || verdict.second_rule_ask?, other_rules: verdict.other_ask_rules)
|
|
329
328
|
rescue LLM::RequestCancelled
|
|
330
329
|
return turn_stopped(verdict)
|
|
331
330
|
end
|
|
@@ -370,9 +369,10 @@ module Samagotchi
|
|
|
370
369
|
# so the failures are in the log too.
|
|
371
370
|
# With +ask_user+ (a fresh ask, a second rule's) it is judged and logged, never answered.
|
|
372
371
|
# +other_rules+ are the ids of the other rules asking about the same call.
|
|
372
|
+
# +author+ is the turn's (TurnContext#author).
|
|
373
373
|
# @return [Hash, nil] {id:, p:, auto:}, nil when nothing was written
|
|
374
|
-
def system_one_judge(payload, targets: nil, ask_user: false, other_rules: nil)
|
|
375
|
-
return nil if Steer.participant_author?(
|
|
374
|
+
def system_one_judge(payload, author: nil, targets: nil, ask_user: false, other_rules: nil)
|
|
375
|
+
return nil if Steer.participant_author?(author)
|
|
376
376
|
|
|
377
377
|
client = @system_one.call
|
|
378
378
|
log = system_one_log
|
|
@@ -513,13 +513,14 @@ module Samagotchi
|
|
|
513
513
|
end
|
|
514
514
|
|
|
515
515
|
# The work tree top of the session's working_directory (the folder
|
|
516
|
-
# itself outside a repo, or when it's gone)
|
|
517
|
-
# the waker's cwd for a worker whose
|
|
518
|
-
|
|
516
|
+
# itself outside a repo, or when it's gone), asked of +git+ (the turn's
|
|
517
|
+
# GitInfo); never Dir.pwd, which is the waker's cwd for a worker whose
|
|
518
|
+
# folder is gone.
|
|
519
|
+
def boundary_root(git)
|
|
519
520
|
dir = @session_lookup.call&.working_directory.to_s
|
|
520
521
|
return nil if dir.empty?
|
|
521
522
|
|
|
522
|
-
|
|
523
|
+
git.root(dir) || File.expand_path(dir)
|
|
523
524
|
end
|
|
524
525
|
|
|
525
526
|
# The rules of a bundle that needs a newer chi: the ones last loaded
|
|
@@ -16,65 +16,65 @@ module Samagotchi
|
|
|
16
16
|
# deny. A hook may replace event[:call]; the verdict carries the final
|
|
17
17
|
# call.
|
|
18
18
|
class Gate
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# @param
|
|
26
|
-
#
|
|
27
|
-
#
|
|
19
|
+
# What a call is judged in comes with it: #evaluate's +context:+, the
|
|
20
|
+
# call's turn (TurnContext: its controller, model key and name, and
|
|
21
|
+
# what +context_for+ and +checks_for+ make of it).
|
|
22
|
+
#
|
|
23
|
+
# @param hooks [Hooks::Registry, nil] the before_tool_call voters (the
|
|
24
|
+
# Engine's registry, built before its gate; nil: no hooks)
|
|
25
|
+
# @param context_for [#call] (turn) → the Context for a call of that
|
|
26
|
+
# turn (the Engine's: session, interface, origin, the turn's git cache)
|
|
27
|
+
# @param approver [#call, nil] (verdict, turn) settles an ask
|
|
28
|
+
# (GuardrailWiring#request_approval); without one an ask is denied
|
|
28
29
|
# @param approvals_lookup [#call] returns the Approvals store (or nil)
|
|
29
|
-
# @param
|
|
30
|
-
# final call after the hooks: protected paths, then the rules
|
|
31
|
-
# @param
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@hooks_lookup = hooks_lookup
|
|
39
|
-
@context_lookup = context_lookup
|
|
40
|
-
@model_key_lookup = model_key_lookup
|
|
41
|
-
@model_name_lookup = model_name_lookup
|
|
30
|
+
# @param checks_for [#call] (turn) → core checks (#check(verdict)) run
|
|
31
|
+
# on the final call after the hooks: protected paths, then the rules
|
|
32
|
+
# @param tools [Tools::Registry, nil] the session's tools (the Engine's,
|
|
33
|
+
# built before its gate): a plugin tool's targets: say what its call
|
|
34
|
+
# acts on
|
|
35
|
+
def initialize(hooks: nil, tools: nil, context_for: ->(_turn) { Context.new }, approver: nil, approvals_lookup: -> {},
|
|
36
|
+
checks_for: ->(_turn) { [] })
|
|
37
|
+
@hooks = hooks
|
|
38
|
+
@context_for = context_for
|
|
42
39
|
@approver = approver
|
|
43
40
|
@approvals_lookup = approvals_lookup
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@tools_lookup = tools_lookup
|
|
41
|
+
@checks_for = checks_for
|
|
42
|
+
@tools = tools
|
|
47
43
|
end
|
|
48
44
|
|
|
49
45
|
# @param call [Hash] the parsed tool call
|
|
46
|
+
# @param context [TurnContext] the call's turn: a cancelled one's
|
|
47
|
+
# controller denies the call without a vote (a hook's stop_turn: the
|
|
48
|
+
# rest of a batch); its model key and name go to the targets
|
|
49
|
+
# (memory overlays, rules' models:)
|
|
50
50
|
# @param iteration [Integer]
|
|
51
51
|
# @param params [String] the call's one-line preview
|
|
52
52
|
# @param verdict [Verdict, nil] the one the voters vote on (the test
|
|
53
53
|
# kit's recording one, maybe with a vote already in); default a new
|
|
54
54
|
# one for +call+
|
|
55
55
|
# @return [Verdict]
|
|
56
|
-
def evaluate(call, iteration:, params:, verdict: nil)
|
|
57
|
-
|
|
56
|
+
def evaluate(call, context:, iteration:, params:, verdict: nil)
|
|
57
|
+
judged = @context_for.call(context)
|
|
58
58
|
verdict ||= Verdict.new(call: call)
|
|
59
59
|
verdict.call = call
|
|
60
|
-
if
|
|
60
|
+
if context.cancelled?
|
|
61
61
|
verdict.deny!("the turn was stopped", decided_by: "core")
|
|
62
|
-
verdict.context =
|
|
63
|
-
verdict.targets = targets_for(call, context)
|
|
62
|
+
verdict.context = judged
|
|
63
|
+
verdict.targets = targets_for(call, judged, context)
|
|
64
64
|
return verdict
|
|
65
65
|
end
|
|
66
66
|
before = { type: :before_tool_call, iteration: iteration, call: call.dup, params: params,
|
|
67
67
|
blocked: verdict.deny?, block_reason: (verdict.reason if verdict.deny?), guardrail: verdict,
|
|
68
|
-
context:
|
|
68
|
+
context: judged.to_h, targets: targets_for(call, judged, context).to_h }
|
|
69
69
|
fire_each(:before_tool_call, before) do |event|
|
|
70
70
|
verdict.legacy_deny!(event[:block_reason]) if event[:blocked]
|
|
71
71
|
event[:blocked] = verdict.deny?
|
|
72
72
|
event[:block_reason] = verdict.reason if verdict.deny?
|
|
73
73
|
end
|
|
74
74
|
verdict.call = before[:call] || call
|
|
75
|
-
verdict.context =
|
|
76
|
-
verdict.targets = targets_for(verdict.call, context)
|
|
77
|
-
Array(@
|
|
75
|
+
verdict.context = judged
|
|
76
|
+
verdict.targets = targets_for(verdict.call, judged, context)
|
|
77
|
+
Array(@checks_for.call(context)).each { |check| check.check(verdict) }
|
|
78
78
|
verdict
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -83,7 +83,8 @@ module Samagotchi
|
|
|
83
83
|
# @return [Verdict]
|
|
84
84
|
# A stored approval that covers the call allows it without asking; an
|
|
85
85
|
# answer that allows it beyond this once is stored.
|
|
86
|
-
|
|
86
|
+
# @param context [TurnContext] the call's turn (the approver's)
|
|
87
|
+
def settle_ask(verdict, context:)
|
|
87
88
|
approvals = @approvals_lookup.call
|
|
88
89
|
if !verdict.fresh_ask? && (entry = stored(approvals, verdict))
|
|
89
90
|
verdict.settle!(:allow, decided_by: "approval")
|
|
@@ -92,7 +93,7 @@ module Samagotchi
|
|
|
92
93
|
end
|
|
93
94
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it.") unless @approver
|
|
94
95
|
|
|
95
|
-
@approver.call(verdict)
|
|
96
|
+
@approver.call(verdict, context)
|
|
96
97
|
return verdict.settle!(:deny, decided_by: "no one", note: "No one to approve it.") if verdict.ask?
|
|
97
98
|
|
|
98
99
|
remember(approvals, verdict)
|
|
@@ -118,14 +119,14 @@ module Samagotchi
|
|
|
118
119
|
Log.warn(:guardrails, "approval_store_failed", echo: "[samagotchi:guardrails] could not store the approval: #{e.class}: #{e.message}", error: e.class.name)
|
|
119
120
|
end
|
|
120
121
|
|
|
121
|
-
def targets_for(call,
|
|
122
|
-
Targets.for(call,
|
|
123
|
-
|
|
122
|
+
def targets_for(call, judged, turn)
|
|
123
|
+
Targets.for(call, judged, model_key: turn.model_key, model_name: turn.model_name, registry: @tools,
|
|
124
|
+
surface: turn.surface)
|
|
124
125
|
end
|
|
125
126
|
|
|
126
127
|
# Registry#fire_each rescues a raising hook itself.
|
|
127
128
|
def fire_each(name, event, &)
|
|
128
|
-
@
|
|
129
|
+
@hooks&.fire_each(name, event, &)
|
|
129
130
|
end
|
|
130
131
|
end
|
|
131
132
|
end
|
|
@@ -146,7 +146,8 @@ module Samagotchi
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
# @param author [#call] → String, nil: the running turn's author
|
|
149
|
-
# (
|
|
149
|
+
# (the call's TurnContext#author, GuardrailWiring#review_boundary /
|
|
150
|
+
# #pair_boundary); the check is off unless it is a
|
|
150
151
|
# participant's (Steer.participant_author?)
|
|
151
152
|
# @param denied_dirs [#call] → Array<String>: chi's own dirs
|
|
152
153
|
# @param home [#call] → String, nil
|
|
@@ -45,7 +45,8 @@ module Samagotchi
|
|
|
45
45
|
MEMORY_SCOPE = "a participant's review turn reads project memories only: call memory_read with scope: project"
|
|
46
46
|
|
|
47
47
|
# @param author [#call] → String, nil: the running turn's author
|
|
48
|
-
# (
|
|
48
|
+
# (the call's TurnContext#author, GuardrailWiring#review_boundary /
|
|
49
|
+
# #pair_boundary); the check is off unless it is a
|
|
49
50
|
# participant's (Steer.participant_author?)
|
|
50
51
|
# @param root [#call] → String, nil: the session's project root (the
|
|
51
52
|
# work tree top of its folder); nil denies every read
|
|
@@ -113,7 +113,11 @@ module Samagotchi
|
|
|
113
113
|
# @param model_name [String, nil] the bare model name, for a rule's models:
|
|
114
114
|
# @param registry [Tools::Registry, nil] the session's tools: a plugin
|
|
115
115
|
# tool's entry says what it acts on (its targets:)
|
|
116
|
-
|
|
116
|
+
# @param surface [Tools::ToolSurface, nil] the turn's: the entry is
|
|
117
|
+
# looked up in it (.entry_of) and call_tool's targets name what it
|
|
118
|
+
# reaches (nil: the registry's entry; call_tool's then raises
|
|
119
|
+
# Tools::MissingSurface)
|
|
120
|
+
def self.for(call, context, model_key: nil, model_name: nil, registry: nil, surface: nil)
|
|
117
121
|
tool = call[:name].to_s
|
|
118
122
|
base = context.cwd
|
|
119
123
|
command = nil
|
|
@@ -136,12 +140,12 @@ module Samagotchi
|
|
|
136
140
|
when "memory_write"
|
|
137
141
|
paths << memory_path(call, model_key)
|
|
138
142
|
else
|
|
139
|
-
given = plugin_targets(call, registry)
|
|
143
|
+
given = plugin_targets(call, registry, surface)
|
|
140
144
|
command = given[:command]
|
|
141
145
|
given_cwd = given[:cwd].to_s.strip
|
|
142
146
|
cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
|
|
143
147
|
paths.concat(given[:paths].flat_map { |path| absolutes(path, cwd) })
|
|
144
|
-
entry = registry
|
|
148
|
+
entry = entry_of(tool, registry, surface)
|
|
145
149
|
args = given[:args] || call[:args] if entry && !entry.core? && call[:args].is_a?(Hash)
|
|
146
150
|
acts_as = given[:acts_as]
|
|
147
151
|
label = given[:label]
|
|
@@ -162,20 +166,25 @@ module Samagotchi
|
|
|
162
166
|
# logged). args: counts when it is a Hash; acts_as: is dropped (and
|
|
163
167
|
# logged) when it names a core tool or another bundle's tool, so a
|
|
164
168
|
# call can't pass for one and match (or reuse the approvals of) its
|
|
165
|
-
# rules. label: counts when it is one short line (.label_of).
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
# rules. label: counts when it is one short line (.label_of). A
|
|
170
|
+
# missing surface (Tools::MissingSurface) is raised, never taken as
|
|
171
|
+
# nothing: call_tool's acts_as would go missing, and with it the
|
|
172
|
+
# mcp_* rules on the call (fail-open).
|
|
173
|
+
def self.plugin_targets(call, registry, surface = nil)
|
|
174
|
+
entry = entry_of(call[:name], registry, surface)
|
|
168
175
|
none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
|
|
169
176
|
return none unless entry && !entry.core? && entry.targets
|
|
170
177
|
|
|
171
|
-
given = entry.
|
|
178
|
+
given = entry.targets_of(call, surface)
|
|
172
179
|
return none unless given.is_a?(Hash)
|
|
173
180
|
|
|
174
181
|
given = given.transform_keys(&:to_sym)
|
|
175
182
|
command = given[:command].to_s
|
|
176
183
|
{ command: command.empty? ? nil : command, paths: Array(given[:paths]).map(&:to_s), cwd: given[:cwd],
|
|
177
|
-
acts_as: acts_as_of(given[:acts_as], entry, registry), args: given[:args].is_a?(Hash) ? given[:args] : nil,
|
|
184
|
+
acts_as: acts_as_of(given[:acts_as], entry, registry, surface), args: given[:args].is_a?(Hash) ? given[:args] : nil,
|
|
178
185
|
label: label_of(given[:label], entry) }
|
|
186
|
+
rescue Tools::MissingSurface
|
|
187
|
+
raise
|
|
179
188
|
rescue StandardError => e
|
|
180
189
|
Log.warn(:plugins, "plugin_targets_failed", tool: call[:name].to_s, error: e.class.name,
|
|
181
190
|
msg: "#{call[:name]} targets failed: #{e.message}")
|
|
@@ -190,24 +199,32 @@ module Samagotchi
|
|
|
190
199
|
# name, ToolIndex#claimed), so the registry is the wrong owner test,
|
|
191
200
|
# and a bundle tool sharing an MCP tool's name would otherwise take
|
|
192
201
|
# call_tool calls of that tool out of its mcp_* rules.
|
|
193
|
-
def self.acts_as_of(name, entry, registry)
|
|
202
|
+
def self.acts_as_of(name, entry, registry, surface = nil)
|
|
194
203
|
name = name.to_s.strip
|
|
195
204
|
return nil if name.empty? || name == entry.name
|
|
196
205
|
|
|
197
|
-
other = registry
|
|
206
|
+
other = entry_of(name, registry, surface)
|
|
198
207
|
whose = if CORE_TOOLS.include?(name) || other&.core? then "a core tool"
|
|
199
208
|
elsif other && other.source != entry.source && entry.source != Tools::ToolIndex::SOURCE
|
|
200
209
|
"bundle #{other.source}'s tool"
|
|
201
210
|
end
|
|
202
211
|
return name unless whose
|
|
203
212
|
|
|
204
|
-
if first_drop?(registry, entry.name, name)
|
|
213
|
+
if first_drop?(registry || surface, entry.name, name)
|
|
205
214
|
Log.warn(:plugins, "plugin_acts_as_dropped", tool: entry.name, acts_as: name,
|
|
206
215
|
msg: "#{entry.name} targets: acts_as #{name} dropped: it is #{whose}")
|
|
207
216
|
end
|
|
208
217
|
nil
|
|
209
218
|
end
|
|
210
219
|
|
|
220
|
+
# The tool +name+ names: the turn's entry (+surface+'s registered),
|
|
221
|
+
# else +registry+'s (a gate without a turn's surface); nil for none.
|
|
222
|
+
# @return [Tools::Registry::Entry, nil]
|
|
223
|
+
def self.entry_of(name, registry, surface)
|
|
224
|
+
name = name.to_s
|
|
225
|
+
surface ? surface.registered[name] : registry&.[](name)
|
|
226
|
+
end
|
|
227
|
+
|
|
211
228
|
# The drops logged, per registry (a session's): each tool's acts_as
|
|
212
229
|
# once, not at every call (targets run several times a call).
|
|
213
230
|
DROPS_LOGGED = ObjectSpace::WeakKeyMap.new
|
|
@@ -166,7 +166,7 @@ module Samagotchi
|
|
|
166
166
|
# The rule set as a worker would load it now, the rule's id looked up in
|
|
167
167
|
# it. Every other answer than ASKS names what stands in the way.
|
|
168
168
|
def rule_status(id)
|
|
169
|
-
wiring = GuardrailWiring.new(scratch: false,
|
|
169
|
+
wiring = GuardrailWiring.new(scratch: false, tools: -> {}, session: -> {},
|
|
170
170
|
cancelled: -> { false }, ask: ->(_fields) {})
|
|
171
171
|
rules = wiring.rules
|
|
172
172
|
failed = wiring.failures.required
|
|
@@ -8,7 +8,7 @@ require_relative "llm/dialect"
|
|
|
8
8
|
require_relative "reasoning_history"
|
|
9
9
|
|
|
10
10
|
module Samagotchi
|
|
11
|
-
# One config.yml hosts: entry, parsed and checked (
|
|
11
|
+
# One config.yml hosts: entry, parsed and checked (HostConfig.all
|
|
12
12
|
# holds them by lowercased name; HostRegistry builds its HostEntry from
|
|
13
13
|
# one).
|
|
14
14
|
#
|
|
@@ -28,6 +28,77 @@ module Samagotchi
|
|
|
28
28
|
super(**members.to_h { |m| [m, nil] }, models: {}, **fields)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# config.yml's hosts: by lowercased name; an entry that is disabled or
|
|
32
|
+
# invalid (warned) is left out. No entry left: "default", from
|
|
33
|
+
# server.host/port.
|
|
34
|
+
# @return [Hash{String => HostConfig}]
|
|
35
|
+
def self.all(env: ENV, path: ConfigFile.global_path(env: env))
|
|
36
|
+
data = ConfigFile.read_yaml(env: env, path: path)
|
|
37
|
+
raw_hosts = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
|
|
38
|
+
|
|
39
|
+
normalized = {}
|
|
40
|
+
if raw_hosts.is_a?(Hash)
|
|
41
|
+
raw_hosts.each do |raw_name, raw_cfg|
|
|
42
|
+
host = parse(raw_name, raw_cfg)
|
|
43
|
+
normalized[host.name] = host if host
|
|
44
|
+
end
|
|
45
|
+
warn_duplicate_models(normalized)
|
|
46
|
+
end
|
|
47
|
+
normalized["default"] = default_host(data, env) if normalized.empty?
|
|
48
|
+
normalized
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
Log.error(:config, "hosts_config_failed", error: e.class.name, message: e.message)
|
|
51
|
+
{}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The names (lowercased) of the hosts entries with enabled: false, which
|
|
55
|
+
# .all leaves out, from config.yml's hosts:.
|
|
56
|
+
# @return [Array<String>]
|
|
57
|
+
def self.disabled_names(env: ENV, path: ConfigFile.global_path(env: env))
|
|
58
|
+
data = ConfigFile.read_yaml(env: env, path: path)
|
|
59
|
+
raw = data[ConfigFile::HOSTS_KEY] if data.is_a?(Hash)
|
|
60
|
+
return [] unless raw.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
raw.filter_map { |name, cfg| name.to_s.strip.downcase if cfg.is_a?(Hash) && disabled?(cfg) }
|
|
63
|
+
rescue StandardError # degrade-ok: Config↔Log: Log resolves its file and level through Config
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# +raw+ parsed against config.yml's hosts (or +hosts+) and aliases.
|
|
68
|
+
# @return [ModelRef]
|
|
69
|
+
def self.model_ref(raw, env: ENV, path: ConfigFile.global_path(env: env), hosts: nil)
|
|
70
|
+
ModelRef.parse(raw, hosts: hosts || all(env: env, path: path), aliases: ConfigFile.model_aliases(env: env, path: path))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The host chi uses when hosts: names none: server.host/port.
|
|
74
|
+
def self.default_host(data, env)
|
|
75
|
+
opts = { file_data: data, env: env, cli_overrides: Config.cli_overrides }
|
|
76
|
+
host = Config.resolve("server.host", **opts).to_s.strip
|
|
77
|
+
host = "localhost" if host.empty?
|
|
78
|
+
port = Config.resolve("server.port", **opts).to_i
|
|
79
|
+
port = 8080 if port <= 0 || port > 65_535
|
|
80
|
+
new(name: "default", host: host, port: port)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :default_host
|
|
83
|
+
|
|
84
|
+
# Two hosts declaring one id under hosts.<name>.models: a bare id goes
|
|
85
|
+
# where HostRegistry#host_for_model sends it (the default host when it
|
|
86
|
+
# declares the id, else the first in hosts: order); say so once.
|
|
87
|
+
def self.warn_duplicate_models(hosts)
|
|
88
|
+
declared = Hash.new { |h, k| h[k] = [] }
|
|
89
|
+
hosts.each { |name, cfg| cfg.models.each { |down, model| declared[down] << [name, model.id] } }
|
|
90
|
+
declared.each_value do |owners|
|
|
91
|
+
next if owners.size < 2
|
|
92
|
+
|
|
93
|
+
names = owners.map(&:first)
|
|
94
|
+
id = owners.first.last
|
|
95
|
+
goes = names.include?("default") ? "default" : "#{names.first} unless the default host lists it"
|
|
96
|
+
ConfigFile.warn_once "Warning: #{names.map { |n| "hosts.#{n}.models" }.join(" and ")} #{names.size == 2 ? "both" : "all"} " \
|
|
97
|
+
"declare #{id}; a bare #{id} goes to #{goes}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :warn_duplicate_models
|
|
101
|
+
|
|
31
102
|
# A hosts: entry as written in config.yml,
|
|
32
103
|
# or nil when it is disabled (enabled: false) or invalid. An invalid
|
|
33
104
|
# one warns once, naming the entry, the way each reader of a setting
|