samagotchi 0.152.0 → 0.154.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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -1
  3. data/README.md +2 -3
  4. data/bin/chi +23 -24
  5. data/docs/architecture.md +30 -33
  6. data/docs/broadcast.md +5 -5
  7. data/docs/bundle-authoring.md +1 -1
  8. data/docs/bundles.md +93 -12
  9. data/docs/cli.md +83 -99
  10. data/docs/configuration.md +46 -40
  11. data/docs/context.md +3 -3
  12. data/docs/desktop.md +1 -1
  13. data/docs/guardrails.md +15 -15
  14. data/docs/hooks.md +13 -12
  15. data/docs/internals/anthropic.md +3 -3
  16. data/docs/internals/attached-context.md +5 -2
  17. data/docs/internals/background-tasks.md +3 -4
  18. data/docs/internals/block-renderers.md +4 -0
  19. data/docs/internals/bridge.md +3 -3
  20. data/docs/internals/context-telemetry.md +1 -1
  21. data/docs/internals/engine.md +42 -12
  22. data/docs/internals/objectives.md +6 -5
  23. data/docs/internals/participants.md +8 -8
  24. data/docs/internals/prompt-caching.md +8 -4
  25. data/docs/internals/remote-sessions.md +7 -7
  26. data/docs/internals/web-frontend.md +20 -17
  27. data/docs/internals/web-push.md +1 -1
  28. data/docs/memory.md +2 -2
  29. data/docs/plugins.md +52 -43
  30. data/docs/releasing.md +1 -1
  31. data/docs/sessions.md +25 -25
  32. data/docs/sub-agent.md +2 -2
  33. data/docs/system-one.md +1 -1
  34. data/docs/testing.md +18 -8
  35. data/lib/samagotchi/answer_text.rb +34 -0
  36. data/lib/samagotchi/bridge.rb +43 -32
  37. data/lib/samagotchi/bridge_client.rb +10 -9
  38. data/lib/samagotchi/broadcast/recipients.rb +2 -2
  39. data/lib/samagotchi/broadcast/triage.rb +1 -1
  40. data/lib/samagotchi/broadcast_command.rb +5 -5
  41. data/lib/samagotchi/bundle_command/try/command.rb +1 -1
  42. data/lib/samagotchi/bundle_command/try/run.rb +1 -1
  43. data/lib/samagotchi/bundle_command.rb +2 -2
  44. data/lib/samagotchi/bundles/decision-card/manifest.yml +4 -4
  45. data/lib/samagotchi/bundles/decision-card/plugin.rb +1 -9
  46. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  47. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -4
  48. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  49. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  50. data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
  51. data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
  52. data/lib/samagotchi/child_move.rb +3 -3
  53. data/lib/samagotchi/commands/registry.rb +6 -6
  54. data/lib/samagotchi/composer_route.rb +4 -5
  55. data/lib/samagotchi/config.rb +14 -12
  56. data/lib/samagotchi/context_candidate_choice.rb +1 -1
  57. data/lib/samagotchi/context_candidates.rb +4 -3
  58. data/lib/samagotchi/continue_offer.rb +1 -1
  59. data/lib/samagotchi/debug_log.rb +1 -1
  60. data/lib/samagotchi/degrade.rb +75 -0
  61. data/lib/samagotchi/desktop/macos/ChiRunner.swift +2 -2
  62. data/lib/samagotchi/engine/cards.rb +87 -0
  63. data/lib/samagotchi/engine/context_judge.rb +138 -0
  64. data/lib/samagotchi/engine/events.rb +131 -0
  65. data/lib/samagotchi/engine/guardrails.rb +87 -0
  66. data/lib/samagotchi/engine/hooks.rb +119 -0
  67. data/lib/samagotchi/engine/messages.rb +124 -0
  68. data/lib/samagotchi/engine/model_settings.rb +127 -0
  69. data/lib/samagotchi/engine/models.rb +261 -0
  70. data/lib/samagotchi/engine/objectives.rb +116 -0
  71. data/lib/samagotchi/engine/participants.rb +86 -0
  72. data/lib/samagotchi/engine/plugins.rb +208 -0
  73. data/lib/samagotchi/engine/questions.rb +98 -0
  74. data/lib/samagotchi/engine/recap.rb +130 -0
  75. data/lib/samagotchi/engine/session.rb +131 -0
  76. data/lib/samagotchi/engine/side_requests.rb +135 -0
  77. data/lib/samagotchi/engine/steering.rb +134 -0
  78. data/lib/samagotchi/engine/toolkit.rb +269 -0
  79. data/lib/samagotchi/engine/turn_end.rb +180 -0
  80. data/lib/samagotchi/engine/turn_helpers.rb +160 -0
  81. data/lib/samagotchi/engine/verify.rb +153 -0
  82. data/lib/samagotchi/engine.rb +63 -2642
  83. data/lib/samagotchi/grants.rb +575 -0
  84. data/lib/samagotchi/guardrail_wiring.rb +2 -2
  85. data/lib/samagotchi/guardrails/context.rb +2 -3
  86. data/lib/samagotchi/guardrails/parent_approvals.rb +2 -2
  87. data/lib/samagotchi/guardrails/parent_policy.rb +2 -2
  88. data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
  89. data/lib/samagotchi/hook_routes.rb +3 -3
  90. data/lib/samagotchi/idle_recap.rb +4 -5
  91. data/lib/samagotchi/idle_reminders.rb +3 -4
  92. data/lib/samagotchi/iteration_limit.rb +2 -2
  93. data/lib/samagotchi/kernel_loop.rb +10 -11
  94. data/lib/samagotchi/launch_mode.rb +13 -32
  95. data/lib/samagotchi/level_setting_command.rb +3 -2
  96. data/lib/samagotchi/llm_context_command.rb +3 -2
  97. data/lib/samagotchi/llm_context_stale.rb +2 -2
  98. data/lib/samagotchi/log.rb +7 -7
  99. data/lib/samagotchi/log_line.rb +1 -1
  100. data/lib/samagotchi/log_path.rb +1 -1
  101. data/lib/samagotchi/message_parts.rb +3 -2
  102. data/lib/samagotchi/note_command.rb +1 -1
  103. data/lib/samagotchi/note_delivery.rb +18 -8
  104. data/lib/samagotchi/objectives.rb +24 -3
  105. data/lib/samagotchi/objectives_check.rb +8 -27
  106. data/lib/samagotchi/objectives_command.rb +4 -69
  107. data/lib/samagotchi/objectives_edit.rb +87 -0
  108. data/lib/samagotchi/output_formatter.rb +7 -1
  109. data/lib/samagotchi/owner_lock.rb +7 -1
  110. data/lib/samagotchi/parent_answers_command.rb +5 -4
  111. data/lib/samagotchi/participant_exposure.rb +42 -0
  112. data/lib/samagotchi/participant_mode.rb +1 -1
  113. data/lib/samagotchi/participant_name.rb +1 -1
  114. data/lib/samagotchi/participants.rb +17 -17
  115. data/lib/samagotchi/participants_watch.rb +1 -1
  116. data/lib/samagotchi/pending_input_queue.rb +2 -2
  117. data/lib/samagotchi/plugin/api.rb +3 -2
  118. data/lib/samagotchi/plugin/context.rb +21 -7
  119. data/lib/samagotchi/plugin/peer.rb +2 -1
  120. data/lib/samagotchi/plugin/sessions.rb +6 -4
  121. data/lib/samagotchi/plugin_tasks.rb +1 -2
  122. data/lib/samagotchi/private_files.rb +29 -0
  123. data/lib/samagotchi/project_env.rb +2 -2
  124. data/lib/samagotchi/question_desk.rb +8 -7
  125. data/lib/samagotchi/recap_command.rb +2 -1
  126. data/lib/samagotchi/recap_setup.rb +1 -1
  127. data/lib/samagotchi/recap_store.rb +1 -1
  128. data/lib/samagotchi/redact.rb +86 -0
  129. data/lib/samagotchi/reminder_queue.rb +2 -2
  130. data/lib/samagotchi/remote_attach_command.rb +1 -1
  131. data/lib/samagotchi/reply_wait/poll.rb +2 -2
  132. data/lib/samagotchi/review_start.rb +1 -1
  133. data/lib/samagotchi/run_options.rb +6 -6
  134. data/lib/samagotchi/self_report.rb +42 -39
  135. data/lib/samagotchi/send_command/busy.rb +1 -1
  136. data/lib/samagotchi/send_command.rb +4 -1
  137. data/lib/samagotchi/session.rb +11 -2
  138. data/lib/samagotchi/session_access/contract.rb +3 -3
  139. data/lib/samagotchi/session_access/local.rb +9 -9
  140. data/lib/samagotchi/session_access/remote.rb +17 -7
  141. data/lib/samagotchi/session_access.rb +4 -4
  142. data/lib/samagotchi/session_archive_command.rb +2 -2
  143. data/lib/samagotchi/session_chain.rb +8 -6
  144. data/lib/samagotchi/session_commands.rb +45 -48
  145. data/lib/samagotchi/session_continue.rb +1 -1
  146. data/lib/samagotchi/session_delete_command.rb +4 -4
  147. data/lib/samagotchi/session_manager.rb +59 -36
  148. data/lib/samagotchi/session_metrics.rb +2 -2
  149. data/lib/samagotchi/session_model_settings.rb +3 -2
  150. data/lib/samagotchi/session_participants.rb +1 -1
  151. data/lib/samagotchi/session_retention.rb +3 -3
  152. data/lib/samagotchi/sessions_command.rb +4 -4
  153. data/lib/samagotchi/share_command.rb +10 -10
  154. data/lib/samagotchi/steer.rb +1 -1
  155. data/lib/samagotchi/system_prompt.rb +102 -109
  156. data/lib/samagotchi/task_watch.rb +1 -1
  157. data/lib/samagotchi/terminal_ui/attach_launcher.rb +72 -10
  158. data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +12 -9
  159. data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +16 -0
  160. data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +2 -3
  161. data/lib/samagotchi/terminal_ui/attached_loop.rb +17 -8
  162. data/lib/samagotchi/terminal_ui/attached_view.rb +2 -3
  163. data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -5
  164. data/lib/samagotchi/terminal_ui/formatting.rb +7 -7
  165. data/lib/samagotchi/terminal_ui/input_support.rb +1 -7
  166. data/lib/samagotchi/terminal_ui/line_reader.rb +2 -2
  167. data/lib/samagotchi/terminal_ui/live_region.rb +2 -2
  168. data/lib/samagotchi/terminal_ui/log_tail.rb +135 -0
  169. data/lib/samagotchi/terminal_ui/one_shot_sink.rb +2 -2
  170. data/lib/samagotchi/terminal_ui/plain_surface.rb +1 -1
  171. data/lib/samagotchi/terminal_ui/question_prompt.rb +4 -5
  172. data/lib/samagotchi/terminal_ui/reline_seam.rb +3 -3
  173. data/lib/samagotchi/terminal_ui/status_row.rb +7 -8
  174. data/lib/samagotchi/terminal_ui.rb +142 -1387
  175. data/lib/samagotchi/title_command.rb +2 -2
  176. data/lib/samagotchi/tool_runner.rb +14 -16
  177. data/lib/samagotchi/toolkit.rb +8 -5
  178. data/lib/samagotchi/toolkit_binding.rb +30 -5
  179. data/lib/samagotchi/toolkit_command.rb +2 -1
  180. data/lib/samagotchi/toolkit_report_command.rb +9 -5
  181. data/lib/samagotchi/toolkit_suggest.rb +12 -6
  182. data/lib/samagotchi/tools/builtin_calls.rb +14 -10
  183. data/lib/samagotchi/tools/deferred_tools.rb +2 -2
  184. data/lib/samagotchi/tools/delegate.rb +3 -1
  185. data/lib/samagotchi/tools/delegate_cursor.rb +3 -3
  186. data/lib/samagotchi/tools/delegate_wait.rb +1 -1
  187. data/lib/samagotchi/tools/objectives_tool.rb +2 -2
  188. data/lib/samagotchi/tools/registry.rb +44 -20
  189. data/lib/samagotchi/tools/send_note.rb +2 -1
  190. data/lib/samagotchi/tools/tool_index.rb +27 -21
  191. data/lib/samagotchi/turn_endings.rb +2 -2
  192. data/lib/samagotchi/turn_flow.rb +4 -5
  193. data/lib/samagotchi/update_hint.rb +2 -2
  194. data/lib/samagotchi/version.rb +1 -1
  195. data/lib/samagotchi/web/app.rb +39 -50
  196. data/lib/samagotchi/web/participant_access.rb +2 -2
  197. data/lib/samagotchi/web/participant_review.rb +5 -5
  198. data/lib/samagotchi/web/participant_stream_filter.rb +8 -8
  199. data/lib/samagotchi/web/participant_view.rb +2 -82
  200. data/lib/samagotchi/web/participants_admin.rb +1 -1
  201. data/lib/samagotchi/web/public/app.js +61 -357
  202. data/lib/samagotchi/web/public/block_host.js +35 -8
  203. data/lib/samagotchi/web/public/block_renderers.css +9 -5
  204. data/lib/samagotchi/web/public/chains.js +2 -2
  205. data/lib/samagotchi/web/public/claim_link.js +4 -4
  206. data/lib/samagotchi/web/public/composer_mode.js +110 -0
  207. data/lib/samagotchi/web/public/format.js +3 -3
  208. data/lib/samagotchi/web/public/owner_claim.js +81 -0
  209. data/lib/samagotchi/web/public/owner_verify.js +112 -0
  210. data/lib/samagotchi/web/public/session_fields.js +3 -3
  211. data/lib/samagotchi/web/public/strip_grip.js +3 -3
  212. data/lib/samagotchi/web/public/strip_hidden.js +65 -0
  213. data/lib/samagotchi/web/public/version_notices.js +132 -0
  214. data/lib/samagotchi/web/push/store.rb +1 -1
  215. data/lib/samagotchi/web/session_access_routes.rb +5 -4
  216. data/lib/samagotchi/web/session_control.rb +12 -8
  217. data/lib/samagotchi/web/session_create.rb +1 -1
  218. data/lib/samagotchi/web/session_hub.rb +2 -2
  219. data/lib/samagotchi/web/session_show.rb +2 -2
  220. data/lib/samagotchi/web/session_summary.rb +1 -1
  221. data/lib/samagotchi/worker.rb +106 -9
  222. data/lib/samagotchi/worker_link.rb +3 -3
  223. data/lib/samagotchi/working_set_command.rb +3 -2
  224. metadata +36 -6
  225. data/lib/samagotchi/terminal_ui/between_turns.rb +0 -138
  226. data/lib/samagotchi/terminal_ui/repl_input.rb +0 -152
  227. data/lib/samagotchi/web/grants.rb +0 -575
  228. data/lib/samagotchi/web/participant_exposure.rb +0 -44
  229. data/lib/samagotchi/web/private_files.rb +0 -31
@@ -34,7 +34,7 @@ module Samagotchi
34
34
 
35
35
  # What the session keeps of a turn that ended normally (nil: nothing to
36
36
  # replace). Runs under the event lock: it also adds the turn's note to
37
- # result.conversation, which the REPL keeps as-is.
37
+ # result.conversation, which the session keeps as-is.
38
38
  # A turn that ran out of iterations ends at its tool results, so a
39
39
  # continue resumes from them rather than after a made-up reply (and a
40
40
  # step-limit Continue from its run's start: ModelResult#run_start).
@@ -43,7 +43,7 @@ module Samagotchi
43
43
  # Nothing visible (no text) is a turn the model should know ended
44
44
  # that way: the note, and nothing made up as its answer. The note's
45
45
  # marker is for the UIs (the notice, and the empty steps after a
46
- # reload); it goes on the result's conversation too (the REPL keeps it).
46
+ # reload); it goes on the result's conversation too (the session keeps it).
47
47
  # A retry's nudge at the tail goes: this note says it all. An earlier
48
48
  # turn's note stays: it says why that turn ended.
49
49
  if result.empty_answer?
@@ -6,8 +6,8 @@ require_relative "context_note"
6
6
  require_relative "llm/errors"
7
7
 
8
8
  module Samagotchi
9
- # The state around turns that the REPL and a session worker share, kept out
10
- # of either loop: the pre-turn checkpoint (what !rollback and a failed turn
9
+ # The state around turns a session worker (and a one-shot) keeps out of
10
+ # its loop: the pre-turn checkpoint (what !rollback and a failed turn
11
11
  # restore), the continue offer after a turn that ran out of iterations, and
12
12
  # the checkpoint a continue turn starts from.
13
13
  #
@@ -189,9 +189,8 @@ module Samagotchi
189
189
  end
190
190
 
191
191
  # Restore the pre-turn checkpoint (!rollback after a Ctrl-C). A pending
192
- # offer goes too: the turn it offered to continue is gone. (The REPL
193
- # reads every line as an answer while an offer is pending; a worker
194
- # also takes !rollback then.) The cards' places follow the messages
192
+ # offer goes too: the turn it offered to continue is gone. (A worker
193
+ # takes !rollback while an offer is pending.) The cards' places follow the messages
195
194
  # (Bridge::CardStore).
196
195
  # @return [Boolean] false when there is none
197
196
  def rollback!
@@ -7,8 +7,8 @@ require_relative "session"
7
7
  require_relative "version"
8
8
 
9
9
  module Samagotchi
10
- # One line at the first interactive start of a new chi version (REPL,
11
- # attached, chi web) when something chi update would update is behind:
10
+ # One line at the first interactive start of a new chi version (a
11
+ # terminal, chi web) when something chi update would update is behind:
12
12
  # "chi 0.3.1: 2 bundles and the desktop helper can be updated: chi update".
13
13
  # Noted once per version in $XDG_STATE_HOME/samagotchi/update_noted,
14
14
  # whether or not anything was behind. No network; nothing is changed (the
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.152.0"
4
+ VERSION = "0.154.0"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -12,6 +12,7 @@ require "rack/request"
12
12
 
13
13
  require_relative "../analytics_file"
14
14
  require_relative "../client_id"
15
+ require_relative "../degrade"
15
16
  require_relative "../delivery"
16
17
  require_relative "../answer_tail"
17
18
  require_relative "../answer_refs"
@@ -969,11 +970,7 @@ module Samagotchi
969
970
 
970
971
  root = dir && ProjectScope.root_for(dir)
971
972
  # Lazy retention sweep (once per 24h)
972
- begin
973
- @manager.retention_sweep_if_due(state_dir: @state_dir)
974
- rescue StandardError
975
- nil
976
- end
973
+ Degrade.call(:web, "retention_sweep_failed") { @manager.retention_sweep_if_due(state_dir: @state_dir) }
977
974
  limit = sanitize_limit(req.params["limit"])
978
975
  offset = sanitize_offset(req.params["offset"])
979
976
  all = @hub.snapshot(project_root: root, sort: sanitize_sort(req.params["sort"]), order: sanitize_order(req.params["order"]))
@@ -1056,8 +1053,8 @@ module Samagotchi
1056
1053
  # A line the page sent, once the worker took it, into the shared prompt
1057
1054
  # history; "history": false (an image-only send's placeholder text)
1058
1055
  # keeps it out. A failed write never fails the send. There's no scratch
1059
- # check: a live scratch session is the REPL's (OwnedByTUI, 409 before
1060
- # any delivery), and one whose REPL died is left as an edge case.
1056
+ # check: a scratch session is refused before any delivery
1057
+ # (SessionManager::ScratchSession, an OwnedByTUI: 409).
1061
1058
  def record_history(body, line)
1062
1059
  return if body["history"] == false
1063
1060
 
@@ -1067,11 +1064,7 @@ module Samagotchi
1067
1064
  end
1068
1065
 
1069
1066
  def handle_models(_req)
1070
- default = begin
1071
- ModelProfile.required_model_name
1072
- rescue StandardError
1073
- nil
1074
- end
1067
+ default = Degrade.call(:web, "default_model_unresolved") { ModelProfile.required_model_name }
1075
1068
  models = []
1076
1069
  warnings = []
1077
1070
  begin
@@ -1156,35 +1149,35 @@ module Samagotchi
1156
1149
  # and each model's LLM context (LLMContextStrategy::Explained#summary).
1157
1150
  # A config that can't be read leaves the list as it is.
1158
1151
  def add_sampling(models, registry)
1159
- return if registry.nil? || models.empty?
1160
-
1161
- settings = ConfigFile.model_settings
1162
- models.each do |model|
1163
- target = registry.resolve(model[:name])
1164
- names = registry.lookup_names(model[:name], target: target)
1165
- summary = SamplingSettings.summary(target, names: names, models: settings)
1166
- model[:sampling] = summary if summary
1167
- add_llm_context(model, target, names, settings)
1168
- add_thinking(model, target, names, settings)
1152
+ Degrade.call(:web, "model_sampling_unreadable") do
1153
+ return if registry.nil? || models.empty?
1154
+
1155
+ settings = ConfigFile.model_settings
1156
+ models.each do |model|
1157
+ target = registry.resolve(model[:name])
1158
+ names = registry.lookup_names(model[:name], target: target)
1159
+ summary = SamplingSettings.summary(target, names: names, models: settings)
1160
+ model[:sampling] = summary if summary
1161
+ add_llm_context(model, target, names, settings)
1162
+ add_thinking(model, target, names, settings)
1163
+ end
1169
1164
  end
1170
- rescue StandardError
1171
- nil
1172
1165
  end
1173
1166
 
1174
1167
  # The LLM context +model+ starts under (the start page's ⚙ panel);
1175
1168
  # one that can't be read leaves this model without, not the others.
1176
1169
  def add_llm_context(model, target, names, settings)
1177
- model[:llm_context] = LLMContextStrategy.explain(target, names: names, models: settings).summary(nil)
1178
- rescue StandardError
1179
- nil
1170
+ Degrade.call(:web, "model_llm_context_unreadable") do
1171
+ model[:llm_context] = LLMContextStrategy.explain(target, names: names, models: settings).summary(nil)
1172
+ end
1180
1173
  end
1181
1174
 
1182
1175
  # The thinking level +model+ starts at, with where it came from (the
1183
1176
  # start page's ⚙ panel; SettingExplained#summary, no session).
1184
1177
  def add_thinking(model, target, names, settings)
1185
- model[:thinking] = Thinking.explain(target, names: names, models: settings).summary
1186
- rescue StandardError
1187
- nil
1178
+ Degrade.call(:web, "model_thinking_unreadable") do
1179
+ model[:thinking] = Thinking.explain(target, names: names, models: settings).summary
1180
+ end
1188
1181
  end
1189
1182
 
1190
1183
  # The served model a session's last generation reported (analytics.json,
@@ -1193,25 +1186,25 @@ module Samagotchi
1193
1186
  # Engine#served_model does, with the host whose served: expects it.
1194
1187
  # {} when none applies.
1195
1188
  def saved_served_model(session, persisted)
1196
- served = persisted["served_model"]
1197
- model = session.model_name.to_s
1198
- return {} unless served.is_a?(String) && !served.empty? && !model.empty?
1189
+ Degrade.call(:web, "served_model_unreadable", fallback: {}) do
1190
+ served = persisted["served_model"]
1191
+ model = session.model_name.to_s
1192
+ return {} unless served.is_a?(String) && !served.empty? && !model.empty?
1199
1193
 
1200
- registry = host_registry
1201
- asked = registry.bare_name(model)
1202
- return {} unless persisted["served_model_for"] == asked
1194
+ registry = host_registry
1195
+ asked = registry.bare_name(model)
1196
+ return {} unless persisted["served_model_for"] == asked
1203
1197
 
1204
- { served_model: served, served_model_for: asked, served_expected_by: saved_served_expected_by(registry, model, served) }
1205
- rescue StandardError
1206
- {}
1198
+ { served_model: served, served_model_for: asked, served_expected_by: saved_served_expected_by(registry, model, served) }
1199
+ end
1207
1200
  end
1208
1201
 
1209
1202
  # See ServedModel.expected_by; nil when the model doesn't resolve (the
1210
1203
  # mismatch still shows).
1211
1204
  def saved_served_expected_by(registry, model, served)
1212
- ServedModel.expected_by(registry.resolve(model), served)
1213
- rescue StandardError
1214
- nil
1205
+ Degrade.call(:web, "served_expected_by_unresolved") do
1206
+ ServedModel.expected_by(registry.resolve(model), served)
1207
+ end
1215
1208
  end
1216
1209
 
1217
1210
  # Built again when config.yml's hosts: changed (compared as read: the
@@ -1343,9 +1336,9 @@ module Samagotchi
1343
1336
 
1344
1337
  # The process holding the session (an OwnerLock::Owner), or nil. A worker always runs a Bridge; a TUI (plain `chi`) doesn't share.
1345
1338
  def session_owner(id)
1346
- @manager.session_owner(id, state_dir: @state_dir)
1347
- rescue StandardError
1348
- nil
1339
+ Degrade.call(:web, "session_owner_unreadable") do
1340
+ @manager.session_owner(id, state_dir: @state_dir)
1341
+ end
1349
1342
  end
1350
1343
 
1351
1344
  # ── The working set (web.working_set) ───────────────────────────────
@@ -1871,11 +1864,7 @@ module Samagotchi
1871
1864
  # Try ISO8601, then float timestamp
1872
1865
  Time.iso8601(val.to_s)
1873
1866
  rescue ArgumentError
1874
- begin
1875
- Time.at(Float(val.to_s))
1876
- rescue StandardError
1877
- nil
1878
- end
1867
+ Degrade.call(:web, "since_unparsable") { Time.at(Float(val.to_s)) }
1879
1868
  end
1880
1869
 
1881
1870
  def default_state_dir
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require_relative "grants"
4
+ require_relative "../grants"
5
5
  require_relative "principal"
6
6
  require_relative "participant_view"
7
7
  require_relative "../archive_store"
@@ -19,7 +19,7 @@ require_relative "../log"
19
19
 
20
20
  module Samagotchi
21
21
  module Web
22
- # What a participant (a Web::Grants token) may do in chi web, mixed into
22
+ # What a participant (a Grants token) may do in chi web, mixed into
23
23
  # App: the gate's lookup of their grant, the refusals that are theirs
24
24
  # (token_revoked, token_expired with its ended page, token_bound for a
25
25
  # link bound to another account, another session's
@@ -3,7 +3,7 @@
3
3
  require_relative "participant_view"
4
4
  require_relative "token"
5
5
  require_relative "participant_stream_filter"
6
- require_relative "participant_exposure"
6
+ require_relative "../participant_exposure"
7
7
  require_relative "../session_metrics"
8
8
  require_relative "../review_start"
9
9
  require_relative "../guardrails/review_paths"
@@ -44,7 +44,7 @@ module Samagotchi
44
44
  # cookie value.
45
45
  def participant_viewer(req, session)
46
46
  author = principal(req).owner? ? nil : principal(req).grant.author
47
- ParticipantView::Viewer.new(author: author, cwd: session.working_directory, secrets: owner_secrets)
47
+ Redact::Viewer.new(author: author, cwd: session.working_directory, secrets: owner_secrets)
48
48
  end
49
49
 
50
50
  def owner_secrets
@@ -119,7 +119,7 @@ module Samagotchi
119
119
 
120
120
  def review_subject(path, full, viewer)
121
121
  raw = File.open(full, File::RDONLY | File::NOFOLLOW | File::BINARY) { |file| file.read(SUBJECT_BYTES + 1) }.to_s
122
- text = ParticipantView::Redact.text(raw.byteslice(0, SUBJECT_BYTES).force_encoding(Encoding::UTF_8).scrub, viewer)
122
+ text = Redact.text(raw.byteslice(0, SUBJECT_BYTES).force_encoding(Encoding::UTF_8).scrub, viewer)
123
123
  entry = { path: path, text: text, truncated: raw.bytesize > SUBJECT_BYTES }
124
124
  markdown = MARKDOWN_EXT.include?(File.extname(path).downcase)
125
125
  entry[:html] = @markdown_renderer.render(text) if markdown && @markdown_renderer.available?
@@ -226,7 +226,7 @@ module Samagotchi
226
226
  return nil if ref.to_s.empty?
227
227
 
228
228
  name = host_registry.bare_name(ref.to_s)
229
- name.to_s.empty? ? nil : ParticipantView::Redact.text(name.to_s, viewer)
229
+ name.to_s.empty? ? nil : Redact.text(name.to_s, viewer)
230
230
  rescue StandardError
231
231
  nil
232
232
  end
@@ -235,7 +235,7 @@ module Samagotchi
235
235
  # enqueued id, for dequeue, and the text), and how many others wait.
236
236
  def review_queue(queued, viewer)
237
237
  mine, others = queued.partition { |entry| entry.is_a?(Hash) && viewer.own?(entry["author"]) }
238
- { mine: mine.map { |entry| ParticipantView::Redact.deep({ "enqueued_id" => entry["enqueued_id"].to_s, "prompt" => entry["prompt"].to_s }, viewer) },
238
+ { mine: mine.map { |entry| Redact.deep({ "enqueued_id" => entry["enqueued_id"].to_s, "prompt" => entry["prompt"].to_s }, viewer) },
239
239
  others: others.size }
240
240
  end
241
241
 
@@ -134,14 +134,14 @@ module Samagotchi
134
134
  def hard_cut(id, shown)
135
135
  text = @held.sub(/\S*\z/, "")
136
136
  # A key word or a key's BEGIN line before that run goes with it.
137
- open = ParticipantView::Redact::OPEN_VALUE.match(text[-80..] || text)
137
+ open = Redact::OPEN_VALUE.match(text[-80..] || text)
138
138
  text = text[0, text.size - open[0].size] if open
139
139
  @dropped = +""
140
140
  @dropped_key = false
141
141
  dropping = @held[text.size..]
142
142
  @held = +""
143
143
  @held << undropped(dropping)
144
- frame(id, shown.merge("text" => "#{lines_of(text)}#{ParticipantView::Redact::MARK}"))
144
+ frame(id, shown.merge("text" => "#{lines_of(text)}#{Redact::MARK}"))
145
145
  end
146
146
 
147
147
  # The last newline (past MAX_HELD, the last space) the held text may
@@ -162,11 +162,11 @@ module Samagotchi
162
162
  # a secret's value would go on.
163
163
  def open_value?(at)
164
164
  from = [at - 80, 0].max
165
- ParticipantView::Redact::OPEN_VALUE.match?(@held[from..at])
165
+ Redact::OPEN_VALUE.match?(@held[from..at])
166
166
  end
167
167
 
168
- SPAN_PATTERNS = (ParticipantView::Redact::PATTERNS - [ParticipantView::Redact::PRIVATE_KEY] +
169
- ParticipantView::Redact::VALUES).freeze
168
+ SPAN_PATTERNS = (Redact::PATTERNS - [Redact::PRIVATE_KEY] +
169
+ Redact::VALUES).freeze
170
170
 
171
171
  # [from, to) of every match Redact would redact in +text+ (but a
172
172
  # private key's block: lines_of withholds it by its lines).
@@ -220,15 +220,15 @@ module Samagotchi
220
220
  if @in_key
221
221
  @in_key = opens_key?(line) if PEM_END.match?(line)
222
222
  elsif opens_key?(line)
223
- out << ParticipantView::Redact.text(run, @viewer)
223
+ out << Redact.text(run, @viewer)
224
224
  run = +""
225
225
  @in_key = true
226
- out << "#{ParticipantView::Redact::MARK}#{"\n" if line.end_with?("\n")}"
226
+ out << "#{Redact::MARK}#{"\n" if line.end_with?("\n")}"
227
227
  else
228
228
  run << line
229
229
  end
230
230
  end
231
- out << ParticipantView::Redact.text(run, @viewer)
231
+ out << Redact.text(run, @viewer)
232
232
  end
233
233
 
234
234
  # Whether +line+ leaves a private key block open: a BEGIN line with
@@ -6,6 +6,7 @@ require_relative "../turn_origin"
6
6
  require_relative "../participant_mode"
7
7
  require_relative "../revise_anchor"
8
8
  require_relative "../verify_anchor"
9
+ require_relative "../redact"
9
10
 
10
11
  module Samagotchi
11
12
  module Web
@@ -23,23 +24,10 @@ module Samagotchi
23
24
  # is an allowlist that builds a new hash from named fields, so a field
24
25
  # added to an event later never reaches a participant until it is
25
26
  # named here, and an event type not named is dropped. Every string
26
- # that goes out passes Redact (secret shapes, the owner's token, the
27
+ # that goes out passes Redact (lib/samagotchi/redact.rb: secret shapes, the owner's token, the
27
28
  # home folder), defence in depth only: the allowlist is the control.
28
29
  # Pure: no I/O; keys of what comes in may be strings (JSON) or symbols.
29
30
  module ParticipantView
30
- # Who is looking: +author+ the participant's ("p:<id>"; nil for the
31
- # owner through the participant view: nothing is theirs), +cwd+ the
32
- # session's folder (tool paths are shown relative to it, only inside
33
- # it), +home+ the folder shown as ~, +secrets+ literal strings never
34
- # shown (the owner's token).
35
- Viewer = Data.define(:author, :cwd, :home, :secrets) do
36
- def initialize(author: nil, cwd: nil, home: Dir.home, secrets: [])
37
- super(author: author, cwd: cwd, home: home, secrets: Array(secrets).map(&:to_s).reject(&:empty?))
38
- end
39
-
40
- def own?(author) = !self.author.nil? && author.to_s == self.author
41
- end
42
-
43
31
  # A turn's participant_mode values the view passes on (ParticipantMode).
44
32
  PARTICIPANT_MODES = [ParticipantMode::REVIEW.to_s, ParticipantMode::PAIR.to_s].freeze
45
33
 
@@ -431,74 +419,6 @@ module Samagotchi
431
419
 
432
420
  hash.key?(key) ? hash[key] : hash[key.to_s]
433
421
  end
434
-
435
- # Secret shapes and host paths out of what a participant gets (the
436
- # session's folder as relative paths, home as ~). Defence in depth: the
437
- # allowlist above is the control; this catches a key the model quoted
438
- # in a reply, or one in a prompt.
439
- module Redact
440
- MARK = "[redacted]"
441
- PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|\z)/m
442
- PATTERNS = [
443
- PRIVATE_KEY,
444
- /\bsk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/,
445
- /\bgh[pousr]_[A-Za-z0-9]{20,}/,
446
- /\bgithub_pat_[A-Za-z0-9_]{20,}/,
447
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
448
- /\bxox[abprs]-[A-Za-z0-9-]{10,}/,
449
- /\bchp_[A-Za-z0-9_-]{20,}/,
450
- # A one-time link code (LinkCodes::CODE_RE): a bearer credential
451
- # until its grant expires (an owner's, 10 minutes).
452
- /\bchc_[A-Za-z0-9_-]{43}/,
453
- /\bAIza[0-9A-Za-z_-]{30,}/,
454
- /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/,
455
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
456
- %r{https://hooks\.slack\.com/services/[A-Za-z0-9/_-]+}
457
- ].freeze
458
- # The value after an Authorization (or a Bridge's secret,
459
- # BridgeSecret::HEADER) header or a token=/key= pair.
460
- VALUES = [
461
- /(\b(?:Authorization|X-Chi-Bridge):\s*(?:Bearer\s+|Basic\s+|Token\s+)?)[^\s"'`]+/i,
462
- /((?<![A-Za-z0-9])[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*[=:]\s*["']?)[^\s"'&`,;}]+/i,
463
- # A URL's password: scheme://user:PASSWORD@host.
464
- %r{(\b[a-z][a-z0-9+.-]*://[^/\s:@]+:)[^@\s/]+(?=@)}i
465
- ].freeze
466
- # A text that ends where one of VALUES' values (or a private key's
467
- # BEGIN line) would go on: a stream cut there would send the value
468
- # on its own, unredacted (ParticipantStreamFilter never cuts there).
469
- OPEN_VALUE = /(?:\b(?:Authorization|X-Chi-Bridge):\s*(?:(?:Bearer|Basic|Token)\s*)?|[A-Za-z0-9_-]*?(?:token|api[_-]?key|access[_-]?key|secret|password|passwd)["']?\s*(?:[=:]\s*["']?)?|-----BEGIN [A-Z0-9 ]*)\z/i
470
-
471
- module_function
472
-
473
- # +value+ with every string in it redacted (hash values, array items).
474
- def deep(value, viewer)
475
- case value
476
- when String then text(value, viewer)
477
- when Hash then value.transform_values { |item| deep(item, viewer) }
478
- when Array then value.map { |item| deep(item, viewer) }
479
- else value
480
- end
481
- end
482
-
483
- def text(value, viewer)
484
- out = value.dup
485
- viewer.secrets.each { |secret| out = out.gsub(secret, MARK) }
486
- PATTERNS.each { |pattern| out = out.gsub(pattern, MARK) }
487
- VALUES.each { |pattern| out = out.gsub(pattern) { "#{Regexp.last_match(1)}#{MARK}" } }
488
- out = within(out, viewer.cwd.to_s)
489
- home = viewer.home.to_s
490
- out = out.gsub(%r{#{Regexp.escape(home)}(?=/|\b|\z)}, "~") if home.length > 1
491
- out
492
- end
493
-
494
- # +text+ with +dir+ (the session's folder) shown relative: "dir/x"
495
- # as "x", "dir" alone as ".".
496
- def within(text, dir)
497
- return text if dir.length < 2
498
-
499
- text.gsub(%r{#{Regexp.escape(dir)}(/|(?=\b|\z))}) { Regexp.last_match(1) == "/" ? "" : "." }
500
- end
501
- end
502
422
  end
503
423
  end
504
424
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "grants"
3
+ require_relative "../grants"
4
4
  require_relative "qr"
5
5
  require_relative "../review_start"
6
6
  require_relative "../review_apply"