samagotchi 0.154.1 → 0.155.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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +1 -1
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/anthropic.md +4 -4
  9. data/docs/internals/attached-context.md +8 -6
  10. data/docs/internals/block-renderers.md +8 -1
  11. data/docs/internals/engine.md +220 -99
  12. data/docs/internals/inline-widgets.md +136 -0
  13. data/docs/internals/objectives.md +8 -6
  14. data/docs/internals/participants.md +5 -5
  15. data/docs/internals/prompt-caching.md +9 -5
  16. data/docs/internals/web-frontend.md +1 -1
  17. data/docs/internals/working-set.md +2 -1
  18. data/docs/plugins.md +2 -0
  19. data/docs/releasing.md +1 -1
  20. data/docs/sessions.md +2 -2
  21. data/docs/testing.md +1 -1
  22. data/lib/samagotchi/block_renderers.rb +5 -0
  23. data/lib/samagotchi/bundle_command/check.rb +1 -1
  24. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  25. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  26. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  27. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -125
  29. data/lib/samagotchi/engine/guardrails.rb +4 -2
  30. data/lib/samagotchi/engine/messages.rb +9 -6
  31. data/lib/samagotchi/engine/objectives.rb +22 -90
  32. data/lib/samagotchi/engine/participants.rb +15 -10
  33. data/lib/samagotchi/engine/plugins.rb +3 -1
  34. data/lib/samagotchi/engine/questions.rb +1 -1
  35. data/lib/samagotchi/engine/side_requests.rb +5 -2
  36. data/lib/samagotchi/engine/toolkit.rb +33 -190
  37. data/lib/samagotchi/engine/turn_end.rb +1 -1
  38. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  39. data/lib/samagotchi/engine/verify.rb +7 -143
  40. data/lib/samagotchi/engine.rb +162 -99
  41. data/lib/samagotchi/engine_peers.rb +1 -1
  42. data/lib/samagotchi/event_fanout.rb +18 -12
  43. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  44. data/lib/samagotchi/guardrails/gate.rb +41 -40
  45. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  46. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  47. data/lib/samagotchi/guardrails/targets.rb +6 -4
  48. data/lib/samagotchi/guardrails_command.rb +1 -1
  49. data/lib/samagotchi/inline_widgets.rb +325 -0
  50. data/lib/samagotchi/kernel_loop.rb +190 -213
  51. data/lib/samagotchi/llm/backend.rb +3 -1
  52. data/lib/samagotchi/llm/chat_loop.rb +63 -41
  53. data/lib/samagotchi/llm/native_backend.rb +3 -2
  54. data/lib/samagotchi/llm_context_forget.rb +5 -3
  55. data/lib/samagotchi/model_binding.rb +3 -3
  56. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  57. data/lib/samagotchi/question_desk.rb +1 -1
  58. data/lib/samagotchi/revise_anchor.rb +1 -1
  59. data/lib/samagotchi/system_prompt.rb +37 -30
  60. data/lib/samagotchi/testing/boundary.rb +7 -0
  61. data/lib/samagotchi/testing/bundle.rb +7 -5
  62. data/lib/samagotchi/testing/install.rb +3 -2
  63. data/lib/samagotchi/tool_activity.rb +8 -6
  64. data/lib/samagotchi/tool_declarations.rb +4 -4
  65. data/lib/samagotchi/tool_response.rb +4 -3
  66. data/lib/samagotchi/tool_runner.rb +91 -100
  67. data/lib/samagotchi/toolkit_binding.rb +40 -14
  68. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  69. data/lib/samagotchi/tools/registry.rb +43 -40
  70. data/lib/samagotchi/tools/tool_index.rb +135 -88
  71. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  72. data/lib/samagotchi/turn_context.rb +67 -0
  73. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  74. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  75. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  76. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  77. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  78. data/lib/samagotchi/turn_state.rb +46 -4
  79. data/lib/samagotchi/version.rb +1 -1
  80. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  81. data/lib/samagotchi/web/message_display.rb +1 -1
  82. data/lib/samagotchi/web/public/app.js +5 -1
  83. data/lib/samagotchi/web/public/block_host.js +74 -199
  84. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  85. data/lib/samagotchi/web/public/block_source.js +7 -4
  86. data/lib/samagotchi/web/public/block_watch.js +256 -0
  87. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  88. data/lib/samagotchi/web/public/index.html +28 -9
  89. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  90. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  91. data/lib/samagotchi/web/public/participant.js +1 -1
  92. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  93. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  94. data/lib/samagotchi/web/public/text_roots.js +1 -1
  95. data/lib/samagotchi/web/public/verify_target.js +1 -1
  96. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  97. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  98. data/lib/samagotchi/web/public/widgets.css +70 -0
  99. metadata +17 -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 hooks [#call] → Hooks::Registry
37
- # @param tools [#call] → Tools::Registry
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:, hooks:, tools:, session:, model_key:, cancelled:, ask:, model_name: -> {},
54
- notify: ->(_message) {}, system_one: -> {}, system_one_runs: -> {}, cancel_controller: -> {},
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: its origin (Engine#run_turn's TurnOrigin; nil or a
108
- # Hash is read as one, TurnOrigin.of), the git facts read afresh, and
109
- # a participant's turn's mode decided once for the whole turn
110
- # (#turn_mode).
111
- def begin_turn(origin)
112
- @origin = TurnOrigin.of(origin)
113
- @git = Guardrails::GitInfo.new
114
- author = turn_author
115
- @turn_entry = author && @participant.call(author)
116
- @turn_mode = author && participant_turn_mode(@session_lookup.call, @turn_entry)
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 running participant turn's mode (ParticipantMode.for_turn: :pair
129
- # only in a working session for a participant invited to act), decided
130
- # at its start; nil for the owner's turns. The boundary in #checks, the
131
- # Engine's stamp and mid-turn stop, and ToolRunner read it.
132
- # @return [Symbol, nil]
133
- attr_reader :turn_mode
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
- -> { @hooks_lookup.call },
146
- context_lookup: -> { context },
147
- model_key_lookup: -> { @model_key_lookup.call },
148
- model_name_lookup: -> { @model_name_lookup.call },
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
- checks_lookup: -> { checks },
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
- def checks
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
- def participant_boundary
172
- @turn_mode == ParticipantMode::PAIR ? pair_boundary : review_boundary
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 the running turn's author is a participant's.
177
- def review_boundary
178
- @review_boundary ||= Guardrails::ReviewBoundary.new(author: -> { turn_author }, root: -> { boundary_root })
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 the running turn's author is a participant's.
183
- def pair_boundary
184
- @pair_boundary ||= Guardrails::PairBoundary.new(author: -> { turn_author }, root: -> { boundary_root })
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); nil for
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
- @child_boundary ||= Guardrails::ChildBoundary.new(
193
- root: -> { boundary_root },
194
- worktrees: -> { (root = boundary_root) ? @git.worktrees(root) : [] },
195
- common_dir: -> { (root = boundary_root) && @git.common_dir(root) },
196
- branch: -> { (root = boundary_root) && @git.branch(root) }
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: @origin.wire, git: @git)
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 = turn_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: (@turn_entry&.name if author))
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, targets: verdict.targets, ask_user: verdict.fresh_ask? || verdict.second_rule_ask?,
328
- other_rules: verdict.other_ask_rules)
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?(turn_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); never Dir.pwd, which is
517
- # the waker's cwd for a worker whose folder is gone.
518
- def boundary_root
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
- @git.root(dir) || File.expand_path(dir)
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
- # @param hooks_lookup [#call] returns the Hooks::Registry (or nil);
20
- # read per call, since the Engine sets the kernel's hooks after
21
- # the kernel is built.
22
- # @param context_lookup [#call] returns the Context for this call
23
- # (the Engine's: session, interface, origin, per-turn git cache)
24
- # @param model_key_lookup [#call] the model key (memory overlays, rules' models:)
25
- # @param model_name_lookup [#call] the bare model name (rules' models:)
26
- # @param approver [#call, nil] settles an ask (Engine#request_approval);
27
- # without one an ask is denied
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 checks_lookup [#call] core checks (#check(verdict)) run on the
30
- # final call after the hooks: protected paths, then the rules
31
- # @param cancelled_lookup [#call] true once the turn was cancelled (a
32
- # hook's stop_turn): the rest of a batch is denied without a vote
33
- # @param tools_lookup [#call] returns the Tools::Registry (or nil): a
34
- # plugin tool's targets: say what its call acts on
35
- def initialize(hooks_lookup, context_lookup: -> { Context.new }, model_key_lookup: -> {}, model_name_lookup: -> {},
36
- approver: nil, approvals_lookup: -> {}, checks_lookup: -> { [] }, cancelled_lookup: -> { false },
37
- tools_lookup: -> {})
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
- @checks_lookup = checks_lookup
45
- @cancelled_lookup = cancelled_lookup
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
- context = @context_lookup.call
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 @cancelled_lookup.call
60
+ if context.cancelled?
61
61
  verdict.deny!("the turn was stopped", decided_by: "core")
62
- verdict.context = 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: context.to_h, targets: targets_for(call, context).to_h }
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 = context
76
- verdict.targets = targets_for(verdict.call, context)
77
- Array(@checks_lookup.call).each { |check| check.check(verdict) }
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
- def settle_ask(verdict)
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, context)
122
- Targets.for(call, context, model_key: @model_key_lookup.call, model_name: @model_name_lookup.call,
123
- registry: @tools_lookup.call)
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
- @hooks_lookup.call&.fire_each(name, event, &)
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
- # (GuardrailWiring#turn_author); the check is off unless it is a
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
- # (GuardrailWiring#turn_author); the check is off unless it is a
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,9 @@ 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
- def self.for(call, context, model_key: nil, model_name: nil, registry: nil)
116
+ # @param surface [Tools::ToolSurface, nil] the turn's: call_tool's
117
+ # targets name what the turn reaches (nil: the Registry now)
118
+ def self.for(call, context, model_key: nil, model_name: nil, registry: nil, surface: nil)
117
119
  tool = call[:name].to_s
118
120
  base = context.cwd
119
121
  command = nil
@@ -136,7 +138,7 @@ module Samagotchi
136
138
  when "memory_write"
137
139
  paths << memory_path(call, model_key)
138
140
  else
139
- given = plugin_targets(call, registry)
141
+ given = plugin_targets(call, registry, surface)
140
142
  command = given[:command]
141
143
  given_cwd = given[:cwd].to_s.strip
142
144
  cwd = File.expand_path(given_cwd, base) unless given_cwd.empty?
@@ -163,12 +165,12 @@ module Samagotchi
163
165
  # logged) when it names a core tool or another bundle's tool, so a
164
166
  # call can't pass for one and match (or reuse the approvals of) its
165
167
  # rules. label: counts when it is one short line (.label_of).
166
- def self.plugin_targets(call, registry)
168
+ def self.plugin_targets(call, registry, surface = nil)
167
169
  entry = registry && registry[call[:name].to_s]
168
170
  none = { command: nil, paths: [], cwd: nil, acts_as: nil, args: nil, label: nil }
169
171
  return none unless entry && !entry.core? && entry.targets
170
172
 
171
- given = entry.targets.call(call)
173
+ given = entry.targets_of(call, surface)
172
174
  return none unless given.is_a?(Hash)
173
175
 
174
176
  given = given.transform_keys(&:to_sym)
@@ -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, hooks: -> {}, tools: -> {}, session: -> {}, model_key: -> {},
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