samagotchi 0.129.0 → 0.130.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 +45 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +18 -9
- data/docs/configuration.md +7 -2
- data/docs/guardrails.md +60 -16
- data/docs/internals/background-tasks.md +11 -0
- data/docs/internals/engine.md +4 -3
- data/docs/internals/participants.md +13 -4
- data/docs/internals/remote-sessions.md +5 -4
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +20 -4
- data/lib/samagotchi/engine_peers.rb +3 -1
- data/lib/samagotchi/guardrails/chi_dirs.rb +64 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +5 -5
- data/lib/samagotchi/guardrails/outside.rb +22 -20
- data/lib/samagotchi/guardrails/pair_boundary.rb +14 -3
- data/lib/samagotchi/guardrails/pair_command.rb +70 -10
- data/lib/samagotchi/guardrails/parent_approvals.rb +8 -11
- data/lib/samagotchi/guardrails/path_resolve.rb +231 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +17 -116
- data/lib/samagotchi/guardrails/review_paths.rb +13 -11
- data/lib/samagotchi/guardrails/rm_targets.rb +5 -5
- data/lib/samagotchi/guardrails/rules.rb +23 -5
- data/lib/samagotchi/guardrails/scratch_writes.rb +3 -2
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +17 -12
- data/lib/samagotchi/guardrails/shell_paths.rb +18 -29
- data/lib/samagotchi/guardrails/targets.rb +11 -13
- data/lib/samagotchi/remote_command.rb +13 -3
- data/lib/samagotchi/remotes.rb +3 -2
- data/lib/samagotchi/review_start.rb +3 -2
- data/lib/samagotchi/session_inbox.rb +17 -0
- data/lib/samagotchi/system_one/self_protect.rb +34 -19
- data/lib/samagotchi/terminal_ui.rb +2 -0
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +10 -1
- data/lib/samagotchi/tools/peers.rb +4 -0
- data/lib/samagotchi/tools/task_wait.rb +26 -1
- data/lib/samagotchi/turn_state.rb +23 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +23 -18
- data/lib/samagotchi/web/proxy_gate.rb +8 -6
- data/lib/samagotchi/web/server.rb +29 -4
- data/lib/samagotchi/worker.rb +9 -1
- metadata +3 -1
|
@@ -289,6 +289,23 @@ module Samagotchi
|
|
|
289
289
|
data["verify"].is_a?(Hash)
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
+
# Whether a message waits that a running turn's next boundary merges and
|
|
293
|
+
# that a person (or chi send, or a delegating parent) sent: an unclaimed
|
|
294
|
+
# input file not held for the turn's end (#turn_end_data?), with a
|
|
295
|
+
# prompt, from a sender that may cut (Steer.cuts?). A running task_wait
|
|
296
|
+
# ends on it (Worker#input_waiting?). It only peeks: nothing is claimed;
|
|
297
|
+
# a file the drain or a dequeue claims meanwhile (ENOENT) or one that
|
|
298
|
+
# doesn't parse is skipped.
|
|
299
|
+
def self.steer_waiting?(session_dir)
|
|
300
|
+
find_new_input_files(session_dir).any? do |file|
|
|
301
|
+
data = JSON.parse(File.read(file))
|
|
302
|
+
data.is_a?(Hash) && !turn_end_data?(data) && !data["prompt"].to_s.strip.empty? &&
|
|
303
|
+
Steer.cuts?(Steer.source_for_client(data["client_id"]))
|
|
304
|
+
rescue JSON::ParserError, SystemCallError
|
|
305
|
+
false
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
292
309
|
# How the unclaimed input file carrying +enqueued_id+ waits: :turn_end
|
|
293
310
|
# (a queue or an image message: it runs as a turn of its own, never
|
|
294
311
|
# merged, #waits_for_turn_end?), :merges (a running turn's drain may
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require_relative "../config"
|
|
4
4
|
require_relative "../memory_paths"
|
|
5
5
|
require_relative "../paths"
|
|
6
|
-
require_relative "../guardrails/
|
|
6
|
+
require_relative "../guardrails/chi_dirs"
|
|
7
|
+
require_relative "../guardrails/path_resolve"
|
|
7
8
|
require_relative "../guardrails/parent_approvals"
|
|
8
9
|
require_relative "../guardrails/shell_lex"
|
|
9
10
|
require_relative "../guardrails/shell_paths"
|
|
@@ -17,10 +18,12 @@ module Samagotchi
|
|
|
17
18
|
# "System 1"). The memories dir is deliberately not one of them:
|
|
18
19
|
# reading and appending a memory file stays eligible.
|
|
19
20
|
#
|
|
20
|
-
# A shell command's words are
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
21
|
+
# A file tool's paths and a shell command's words are judged against
|
|
22
|
+
# the same dirs (.chi_dirs: chi's whole state dir among them). The
|
|
23
|
+
# words are resolved as Guardrails::ShellPaths resolves them (cd
|
|
24
|
+
# chains, ~, $HOME and the XDG dirs, symlinks, `..` both ways), under
|
|
25
|
+
# chi's env and the call's env: when the caller hands its Targets; one
|
|
26
|
+
# landing in chi's dirs outside the memories refuses.
|
|
24
27
|
# The memories text is taken out of the command (the exemption) only
|
|
25
28
|
# when its text shows no climb (CLIMB: a `..`, a `$`, backticks, a `~`
|
|
26
29
|
# other than the home one, `cd -`) and every word either resolved or is
|
|
@@ -77,6 +80,7 @@ module Samagotchi
|
|
|
77
80
|
command = fetch(approval, :command).to_s
|
|
78
81
|
words = command.empty? ? nil : words(command, cwd: fetch(approval, :cwd), targets: targets)
|
|
79
82
|
return true if words&.chi
|
|
83
|
+
return true if chi_paths?(approval)
|
|
80
84
|
# The test that makes an ask user_only (ParentApprovals#chi_files?:
|
|
81
85
|
# chi's dirs as the command spells them), minus the memories.
|
|
82
86
|
return true if Guardrails::ParentApprovals.chi_files?(beyond_memories(approval, exempt: exempt?(command, words)))
|
|
@@ -84,14 +88,28 @@ module Samagotchi
|
|
|
84
88
|
command.match?(DEFAULT_STATE_TEXT)
|
|
85
89
|
end
|
|
86
90
|
|
|
91
|
+
# Whether a path of the approval lands in chi's dirs (#chi_dirs: its
|
|
92
|
+
# whole state dir too, as a command's words are judged) outside the
|
|
93
|
+
# memories dir, or in its installed bundles.
|
|
94
|
+
def chi_paths?(approval)
|
|
95
|
+
memories = Guardrails::PathResolve.real(MemoryPaths.system_dir)
|
|
96
|
+
bundles = Guardrails::PathResolve.real(MemoryPaths.bundles_dir)
|
|
97
|
+
dirs = chi_dirs
|
|
98
|
+
Array(fetch(approval, :paths)).map(&:to_s).reject(&:empty?).any? do |path|
|
|
99
|
+
Guardrails::PathResolve.reals(path).any? do |real|
|
|
100
|
+
dirs.any? { |dir| Guardrails::PathResolve.within?(real, dir) } && !memory?(real, memories, bundles)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
87
105
|
# The approval's facts without what is in the memories dir (not its
|
|
88
106
|
# installed bundles): the paths under it, and its spellings in the
|
|
89
107
|
# command when +exempt+ (.exempt?).
|
|
90
108
|
def beyond_memories(approval, exempt:)
|
|
91
|
-
memories = Guardrails::
|
|
92
|
-
bundles = Guardrails::
|
|
109
|
+
memories = Guardrails::PathResolve.real(MemoryPaths.system_dir)
|
|
110
|
+
bundles = Guardrails::PathResolve.real(MemoryPaths.bundles_dir)
|
|
93
111
|
paths = Array(fetch(approval, :paths)).map(&:to_s).reject do |path|
|
|
94
|
-
Guardrails::
|
|
112
|
+
Guardrails::PathResolve.reals(path).all? { |real| memory?(real, memories, bundles) }
|
|
95
113
|
end
|
|
96
114
|
command = fetch(approval, :command).to_s
|
|
97
115
|
command = command.gsub(memories_text) { |text| text.include?("/.bundles") ? text : "" } if exempt
|
|
@@ -107,8 +125,8 @@ module Samagotchi
|
|
|
107
125
|
# The command's words resolved (ShellPaths::Walk) from +cwd+ (else
|
|
108
126
|
# this process's dir) under each of the call's shell envs: Words.
|
|
109
127
|
def words(command, cwd:, targets:)
|
|
110
|
-
memories = Guardrails::
|
|
111
|
-
bundles = Guardrails::
|
|
128
|
+
memories = Guardrails::PathResolve.real(MemoryPaths.system_dir)
|
|
129
|
+
bundles = Guardrails::PathResolve.real(MemoryPaths.bundles_dir)
|
|
112
130
|
dirs = chi_dirs
|
|
113
131
|
tokens = Guardrails::ShellLex.lex(command)
|
|
114
132
|
plain = !Guardrails::ShellLex.unterminated?(command) &&
|
|
@@ -124,8 +142,8 @@ module Samagotchi
|
|
|
124
142
|
walk.step(simple).each do |word, path|
|
|
125
143
|
next plain &&= known && data?(word) unless path
|
|
126
144
|
|
|
127
|
-
chi ||= Guardrails::
|
|
128
|
-
dirs.any? { |dir| Guardrails::
|
|
145
|
+
chi ||= Guardrails::PathResolve.reals(path).any? do |real|
|
|
146
|
+
dirs.any? { |dir| Guardrails::PathResolve.under?(real, dir) } && !memory?(real, memories, bundles)
|
|
129
147
|
end
|
|
130
148
|
end
|
|
131
149
|
end
|
|
@@ -148,15 +166,12 @@ module Samagotchi
|
|
|
148
166
|
end
|
|
149
167
|
|
|
150
168
|
# chi's own dirs (ParentApprovals.chi_dirs) and its whole state dir
|
|
151
|
-
# (DEFAULT_STATE_TEXT's), resolved.
|
|
152
|
-
def chi_dirs
|
|
153
|
-
[*Guardrails::ParentApprovals.chi_dirs, Paths.state_dir]
|
|
154
|
-
.map { |dir| Guardrails::ProtectedPaths.real(dir.to_s.chomp("/")) }.reject(&:empty?).uniq
|
|
155
|
-
end
|
|
169
|
+
# (DEFAULT_STATE_TEXT's), resolved (Guardrails::ChiDirs.self_protect).
|
|
170
|
+
def chi_dirs = Guardrails::ChiDirs.self_protect
|
|
156
171
|
|
|
157
172
|
# Whether +real+ is in the memories dir and not in its installed bundles.
|
|
158
173
|
def memory?(real, memories, bundles)
|
|
159
|
-
Guardrails::
|
|
174
|
+
Guardrails::PathResolve.within?(real, memories) && !Guardrails::PathResolve.within?(real, bundles)
|
|
160
175
|
end
|
|
161
176
|
|
|
162
177
|
# The memories dir as a command spells it: the default
|
|
@@ -164,7 +179,7 @@ module Samagotchi
|
|
|
164
179
|
# rest of that shell word.
|
|
165
180
|
def memories_text
|
|
166
181
|
dir = ConfigFile.config_dir
|
|
167
|
-
prefixes = [".config/samagotchi", dir, Guardrails::
|
|
182
|
+
prefixes = [".config/samagotchi", dir, Guardrails::PathResolve.real(dir)]
|
|
168
183
|
.map { |prefix| Regexp.escape(prefix.to_s.chomp("/")) }.uniq
|
|
169
184
|
word = WORD_END.sub("[", "[^")
|
|
170
185
|
Regexp.new("(?:#{prefixes.join("|")})/memories(?=/|#{WORD_END}|\\z)(?:/#{word}*)?")
|
|
@@ -716,6 +716,8 @@ module Samagotchi
|
|
|
716
716
|
max_iterations: max_iterations,
|
|
717
717
|
cancel_controller: cancellation_controller,
|
|
718
718
|
pending_input: method(:drain_steering),
|
|
719
|
+
# A steering line waits (#steer_line): a running task_wait ends for it.
|
|
720
|
+
input_waiting: -> { !@pending_input_queue.empty? },
|
|
719
721
|
continue: continue,
|
|
720
722
|
images: images,
|
|
721
723
|
resumed: resumed,
|
|
@@ -221,7 +221,7 @@ module Samagotchi
|
|
|
221
221
|
},
|
|
222
222
|
{
|
|
223
223
|
name: "task_wait",
|
|
224
|
-
description: "Wait for a background task to finish. Polls every 0.5s until completion, a log pattern matches, or timeout. Timed-out waits include a bounded output tail.",
|
|
224
|
+
description: "Wait for a background task to finish. Polls every 0.5s until completion, a log pattern matches, a message from the user arrives, or timeout. Timed-out waits include a bounded output tail.",
|
|
225
225
|
parameters: {
|
|
226
226
|
type: "object",
|
|
227
227
|
properties: {
|
|
@@ -86,7 +86,8 @@ module Samagotchi
|
|
|
86
86
|
end,
|
|
87
87
|
TaskWait::NAME => lambda do |call, kctx|
|
|
88
88
|
TaskWait.call(call[:content], timeout: call[:timeout], tail_lines: call[:tail_lines],
|
|
89
|
-
done_pattern: call[:done_pattern], cancelled: cancelled_proc(kctx)
|
|
89
|
+
done_pattern: call[:done_pattern], cancelled: cancelled_proc(kctx),
|
|
90
|
+
message_waiting: input_waiting_proc(kctx))
|
|
90
91
|
end,
|
|
91
92
|
Execute::NAME => lambda do |call, kctx|
|
|
92
93
|
Execute.call(call[:content], cwd: call[:cwd], cancelled: cancelled_proc(kctx), stop_reason: stop_reason_proc(kctx),
|
|
@@ -188,6 +189,14 @@ module Samagotchi
|
|
|
188
189
|
-> { !!peers&.cancelled? }
|
|
189
190
|
end
|
|
190
191
|
|
|
192
|
+
# A person's message waits for the turn's next boundary
|
|
193
|
+
# (Engine#input_waiting?, through its PeerView): a task_wait returns on
|
|
194
|
+
# it. A kernel whose peers can't tell: never.
|
|
195
|
+
def input_waiting_proc(kctx)
|
|
196
|
+
peers = kctx.peers
|
|
197
|
+
-> { peers.respond_to?(:input_waiting?) && !!peers.input_waiting? }
|
|
198
|
+
end
|
|
199
|
+
|
|
191
200
|
# The running turn's stop reason (:signal, :manual, …), nil without one.
|
|
192
201
|
def stop_reason_proc(kctx)
|
|
193
202
|
peers = kctx.peers
|
|
@@ -23,6 +23,10 @@ module Samagotchi
|
|
|
23
23
|
def cancelled?
|
|
24
24
|
cancelled.respond_to?(:call) ? !!cancelled.call : !!cancelled
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
# A person's message waits for the turn's next boundary: never here
|
|
28
|
+
# (Engine::PeerView asks the running turn).
|
|
29
|
+
def input_waiting? = false
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -14,13 +14,23 @@ module Samagotchi
|
|
|
14
14
|
# A handed-over execute keeps stderr in a log of its own: at most this
|
|
15
15
|
# many of its last lines follow the output tail.
|
|
16
16
|
STDERR_TAIL_LINES_MAX = 10
|
|
17
|
+
# The note of a wait a waiting message ended (wait_result:
|
|
18
|
+
# message_waiting); %<id>s is the task's id. "The user" whoever sent
|
|
19
|
+
# it: the merged message itself names its sender.
|
|
20
|
+
MESSAGE_WAITING_NOTE = [
|
|
21
|
+
"note: a message from the user is waiting; the task is STILL RUNNING.",
|
|
22
|
+
" Read the message first. To keep waiting after it: task_wait %<id>s. If it is no longer needed: task_stop %<id>s."
|
|
23
|
+
].join("\n").freeze
|
|
17
24
|
|
|
18
25
|
def self.name = NAME
|
|
19
26
|
|
|
20
27
|
# @param cancelled [#call] true once the user stopped the turn: the
|
|
21
28
|
# wait returns then, and the task keeps running
|
|
29
|
+
# @param message_waiting [#call] true once a person's message waits for
|
|
30
|
+
# the turn's next boundary (Engine#input_waiting?): the wait returns
|
|
31
|
+
# then, so the model reads it, and the task keeps running
|
|
22
32
|
def self.call(task_id, timeout: TIMEOUT_DEFAULT, tail_lines: TAIL_LINES_DEFAULT, done_pattern: nil,
|
|
23
|
-
cancelled: -> { false })
|
|
33
|
+
cancelled: -> { false }, message_waiting: -> { false })
|
|
24
34
|
timeout = TIMEOUT_DEFAULT if blank?(timeout)
|
|
25
35
|
tail_lines = TAIL_LINES_DEFAULT if blank?(tail_lines)
|
|
26
36
|
timeout = timeout.to_i
|
|
@@ -48,6 +58,7 @@ module Samagotchi
|
|
|
48
58
|
end
|
|
49
59
|
|
|
50
60
|
return cancelled_response(task_id, tail_lines, started) if cancelled.call
|
|
61
|
+
return message_waiting_response(task_id, tail_lines, started) if message_waiting.call
|
|
51
62
|
|
|
52
63
|
break if monotonic_time > deadline
|
|
53
64
|
|
|
@@ -80,6 +91,20 @@ module Samagotchi
|
|
|
80
91
|
end
|
|
81
92
|
private_class_method :cancelled_response
|
|
82
93
|
|
|
94
|
+
# A message waits: the wait ends so the model reads it at the next
|
|
95
|
+
# boundary. A task that finished meanwhile gives its finished result,
|
|
96
|
+
# as after a Stop.
|
|
97
|
+
def self.message_waiting_response(task_id, tail_lines, started)
|
|
98
|
+
record, error = TaskRuntime.get_record(task_id)
|
|
99
|
+
return error if error
|
|
100
|
+
return finished_response(record, tail_lines) unless record.running?
|
|
101
|
+
|
|
102
|
+
note = [format(MESSAGE_WAITING_NOTE, id: task_id), "waited: #{(monotonic_time - started).round}s"]
|
|
103
|
+
format_response(record, wait_result: "message_waiting", output_tail: output_tail(record, tail_lines),
|
|
104
|
+
tail_lines: tail_lines, extra: note)
|
|
105
|
+
end
|
|
106
|
+
private_class_method :message_waiting_response
|
|
107
|
+
|
|
83
108
|
# A finished task's text, as task_wait gives it: its status lines and
|
|
84
109
|
# always the log's last lines (a test run's summary, an error), then a
|
|
85
110
|
# stderr_tail when the record has a non-empty one. A task the user
|
|
@@ -5,7 +5,8 @@ require "monitor"
|
|
|
5
5
|
module Samagotchi
|
|
6
6
|
# A session's turn as other threads see it: whether one runs, its cancel
|
|
7
7
|
# controller and event sink, the plugin steers waiting for its next
|
|
8
|
-
# boundary,
|
|
8
|
+
# boundary, whether a person's message waits for it (its caller's
|
|
9
|
+
# input_waiting: probe), and the activity clock the idle jobs read.
|
|
9
10
|
#
|
|
10
11
|
# The turn thread writes it (Engine#begin_turn, #release_turn); the idle
|
|
11
12
|
# scheduler, the bridge's request threads, plugin threads and the UIs
|
|
@@ -24,6 +25,8 @@ module Samagotchi
|
|
|
24
25
|
@sink = nil
|
|
25
26
|
@thread = nil
|
|
26
27
|
@ended = false
|
|
28
|
+
@input_waiting = nil
|
|
29
|
+
@input_waiting_warned = false
|
|
27
30
|
@steers = []
|
|
28
31
|
@carried_steers = []
|
|
29
32
|
end
|
|
@@ -67,20 +70,24 @@ module Samagotchi
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
# A turn begins, on the calling thread (the turn's own): running, with
|
|
70
|
-
# its controller and
|
|
71
|
-
|
|
73
|
+
# its controller, sink and input_waiting probe, in one step.
|
|
74
|
+
# @param input_waiting [#call, nil] true while a person's message waits
|
|
75
|
+
# for the turn's next boundary (Engine#input_waiting?); nil: never
|
|
76
|
+
def begin!(controller:, sink:, input_waiting: nil)
|
|
72
77
|
@lock.synchronize do
|
|
73
78
|
@running = true
|
|
74
79
|
@controller = controller
|
|
75
80
|
@sink = sink
|
|
76
81
|
@thread = Thread.current
|
|
77
82
|
@ended = false
|
|
83
|
+
@input_waiting = input_waiting
|
|
84
|
+
@input_waiting_warned = false
|
|
78
85
|
@steers.concat(@carried_steers)
|
|
79
86
|
@carried_steers = []
|
|
80
87
|
end
|
|
81
88
|
end
|
|
82
89
|
|
|
83
|
-
# The turn is over: not running, no controller, sink or steers.
|
|
90
|
+
# The turn is over: not running, no controller, sink, probe or steers.
|
|
84
91
|
# @return [Array<Hash>] the steers it never took ({text:, source:})
|
|
85
92
|
def finish!
|
|
86
93
|
@lock.synchronize do
|
|
@@ -89,6 +96,7 @@ module Samagotchi
|
|
|
89
96
|
@sink = nil
|
|
90
97
|
@thread = nil
|
|
91
98
|
@ended = false
|
|
99
|
+
@input_waiting = nil
|
|
92
100
|
@steers.tap { @steers = [] }
|
|
93
101
|
end
|
|
94
102
|
end
|
|
@@ -118,6 +126,17 @@ module Samagotchi
|
|
|
118
126
|
end
|
|
119
127
|
end
|
|
120
128
|
|
|
129
|
+
# @return [#call, nil] the running turn's input_waiting probe (called
|
|
130
|
+
# outside the lock), nil outside a turn or for a turn given none
|
|
131
|
+
def input_waiting
|
|
132
|
+
@lock.synchronize { @running ? @input_waiting : nil }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A probe failed: true the first time in a turn (warn then), false after.
|
|
136
|
+
def input_waiting_failed!
|
|
137
|
+
@lock.synchronize { !@input_waiting_warned.tap { @input_waiting_warned = true } }
|
|
138
|
+
end
|
|
139
|
+
|
|
121
140
|
# Cancel the running turn, if any (outside the lock).
|
|
122
141
|
# @return [Boolean] whether a cancellation was triggered
|
|
123
142
|
def cancel!(reason)
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -459,12 +459,14 @@ module Samagotchi
|
|
|
459
459
|
# The request's Principal, as #route resolved it.
|
|
460
460
|
def principal(req) = req.env.fetch(PRINCIPAL_ENV)
|
|
461
461
|
|
|
462
|
-
# LAN mode
|
|
463
|
-
#
|
|
464
|
-
#
|
|
465
|
-
#
|
|
466
|
-
#
|
|
467
|
-
#
|
|
462
|
+
# LAN mode: every route needs the access token, from a phone and from
|
|
463
|
+
# this machine's loopback too (a pair participant's shell runs here),
|
|
464
|
+
# as the chi_token cookie or an Authorization: Bearer header; only
|
|
465
|
+
# GATE_FREE_PATHS and chi's token-less /api/info probe
|
|
466
|
+
# (ProxyGate#probe_info?) are answered without it. The page with
|
|
467
|
+
# ?token= (or a link's #code=, POST /api/token) trades it for the
|
|
468
|
+
# cookie and a 303 to itself without it, so the token doesn't stay in
|
|
469
|
+
# the browser's history.
|
|
468
470
|
# @return [Array(Principal, nil), Array(nil, Array)] the request's
|
|
469
471
|
# principal (#principal_for), or the response that refuses it
|
|
470
472
|
def token_gate(req)
|
|
@@ -489,9 +491,9 @@ module Samagotchi
|
|
|
489
491
|
# the access token; a participant for a live grant's token (a chp_
|
|
490
492
|
# one, looked up only when no owner token was given), wherever the
|
|
491
493
|
# request comes from, this machine's loopback too; with neither, the
|
|
492
|
-
# owner
|
|
493
|
-
#
|
|
494
|
-
# nil anywhere: it never falls back to the owner.
|
|
494
|
+
# owner only on a chi web that asks no token (loopback only: no LAN
|
|
495
|
+
# mode, no web.public_url), else nil. An ended or unknown grant's
|
|
496
|
+
# token is nil anywhere: it never falls back to the owner.
|
|
495
497
|
# @return [Principal, nil]
|
|
496
498
|
# With web.public_url (ProxyGate) every request needs a token, and a
|
|
497
499
|
# proxied one is #proxied_principal's.
|
|
@@ -595,11 +597,10 @@ module Samagotchi
|
|
|
595
597
|
"#{token_cookie_name(req)}=; #{token_cookie_attributes(req).sub(/Max-Age=\d+/, "Max-Age=0")}"
|
|
596
598
|
end
|
|
597
599
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
end
|
|
600
|
+
# Whether the gate asks this request for a token: on a chi web on the
|
|
601
|
+
# LAN or behind a proxy, every request does, this machine's loopback
|
|
602
|
+
# too. Only a loopback-only chi web takes loopback as the owner.
|
|
603
|
+
def token_required?(_req) = !(@public_url || @lan).nil?
|
|
603
604
|
|
|
604
605
|
# The page opened with ?token= (a gated request, or a grant's token
|
|
605
606
|
# anywhere): its token is traded for the cookie, whatever else the
|
|
@@ -903,7 +904,11 @@ module Samagotchi
|
|
|
903
904
|
|
|
904
905
|
def unauthorized_detail(req, reason)
|
|
905
906
|
return "chi web behind the proxy: #{reason || "needs its access token"}" if proxied?(req)
|
|
906
|
-
|
|
907
|
+
|
|
908
|
+
unless @public_url
|
|
909
|
+
here = LOOPBACK_PEERS.include?(req.get_header("REMOTE_ADDR").to_s)
|
|
910
|
+
return "chi web on the LAN needs its access token#{", on this machine too" if here}"
|
|
911
|
+
end
|
|
907
912
|
|
|
908
913
|
detail = "chi web with web.public_url needs its access token, on this machine too"
|
|
909
914
|
direct_public?(req) ? "#{detail}; open its public address #{@public_url.origin}/ instead" : detail
|
|
@@ -941,7 +946,7 @@ module Samagotchi
|
|
|
941
946
|
# the proxy too.
|
|
942
947
|
def self.public_unauthorized_page(page)
|
|
943
948
|
page.sub('<form method="get" action="/">', '<form method="post" action="/api/token">')
|
|
944
|
-
.sub("Open the link chi web printed in its terminal (LAN, or
|
|
949
|
+
.sub("Open the link chi web printed in its terminal (Local on this machine, LAN, or\nProxied behind an auth proxy), or scan its QR code.",
|
|
945
950
|
"Open the Proxied link chi web printed in its terminal (it works once; chi web again prints a new " \
|
|
946
951
|
"one), or paste the token from its web-token file.")
|
|
947
952
|
end
|
|
@@ -961,8 +966,8 @@ module Samagotchi
|
|
|
961
966
|
</style></head>
|
|
962
967
|
<body>
|
|
963
968
|
<h1>chi web</h1>
|
|
964
|
-
<p>This page needs chi web's access token. Open the link chi web printed in its terminal (LAN, or
|
|
965
|
-
auth proxy), or scan its QR code.</p>
|
|
969
|
+
<p>This page needs chi web's access token. Open the link chi web printed in its terminal (Local on this machine, LAN, or
|
|
970
|
+
Proxied behind an auth proxy), or scan its QR code.</p>
|
|
966
971
|
<p id="trade" hidden></p>
|
|
967
972
|
<form method="get" action="/">
|
|
968
973
|
<label for="token">Or paste the token:</label>
|
|
@@ -124,13 +124,15 @@ module Samagotchi
|
|
|
124
124
|
participant_principal(req, grant_tokens, identity: identity, proxied: true)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
# chi's own probe of a chi web
|
|
128
|
-
#
|
|
127
|
+
# chi's own probe of a chi web that asks this machine for its token
|
|
128
|
+
# (behind a proxy, or on the LAN: InfoProbe from a second chi web, chi
|
|
129
|
+
# share, chi update, chi self) sends no token: GET /api/info from this
|
|
129
130
|
# machine, not through the proxy, with no token at all, gets the
|
|
130
|
-
# fields it checks (nothing secret
|
|
131
|
-
# to whatever listens on
|
|
131
|
+
# fields it checks (nothing secret: its start folder is a path on this
|
|
132
|
+
# machine), so no probe ever hands the token to whatever listens on
|
|
133
|
+
# the port. Any token or cookie makes it an ordinary request.
|
|
132
134
|
def probe_info?(req)
|
|
133
|
-
return false unless @public_url && req.get? && req.path_info == "/api/info"
|
|
135
|
+
return false unless (@public_url || @lan) && req.get? && req.path_info == "/api/info"
|
|
134
136
|
return false unless App::LOOPBACK_PEERS.include?(req.get_header("REMOTE_ADDR").to_s) && !proxied?(req)
|
|
135
137
|
|
|
136
138
|
req.get_header("HTTP_AUTHORIZATION").nil? && req.get_header("HTTP_COOKIE").nil?
|
|
@@ -138,7 +140,7 @@ module Samagotchi
|
|
|
138
140
|
|
|
139
141
|
def probe_info
|
|
140
142
|
json_response(200, { app: "chi-web", version: Samagotchi::VERSION, pid: Process.pid, features: App::INFO_FEATURES,
|
|
141
|
-
lan: @lan && @lan[:ip] })
|
|
143
|
+
lan: @lan && @lan[:ip], start_dir: start_dir })
|
|
142
144
|
end
|
|
143
145
|
|
|
144
146
|
def proxy_refuse(req, reason)
|
|
@@ -118,11 +118,15 @@ module Samagotchi
|
|
|
118
118
|
puts "chi web already runs on port #{port} (pid #{found["pid"]}): #{url}"
|
|
119
119
|
other_start = start_given && other_start_line(found["start_dir"], start_dir)
|
|
120
120
|
puts other_start if other_start
|
|
121
|
-
puts running_lan_lines(found["lan"], port) if found["lan"]
|
|
122
121
|
running_public = public_url || Running.live_public_url(probe: ->(at) { probe(host, at) })
|
|
122
|
+
# On the LAN this machine's page needs the token too: the link
|
|
123
|
+
# printed and opened here carries an owner's one-time code.
|
|
124
|
+
here = found["lan"] && !running_public && Token.read(Token.path) ? local_owner_link(url) : nil
|
|
125
|
+
puts local_lines(here) if here
|
|
126
|
+
puts running_lan_lines(found["lan"], port) if found["lan"]
|
|
123
127
|
puts running_public_lines(running_public) if running_public
|
|
124
128
|
puts lan_off_line(setting) if new_token && !found["lan"]
|
|
125
|
-
open_url(url) if open_browser
|
|
129
|
+
open_url(here ? local_owner_link(url) : url) if open_browser
|
|
126
130
|
0
|
|
127
131
|
when :other
|
|
128
132
|
warn in_use_message(port)
|
|
@@ -388,6 +392,10 @@ module Samagotchi
|
|
|
388
392
|
on_bound&.call
|
|
389
393
|
record_running(running_path, port, public_url)
|
|
390
394
|
puts "Chi Web on #{url} (public: #{File.expand_path("public", __dir__)})"
|
|
395
|
+
# On the LAN (no web.public_url) this machine's page needs the
|
|
396
|
+
# token too: a one-time owner's code in the link instead.
|
|
397
|
+
local = lan && !public_url
|
|
398
|
+
puts local_lines(local_owner_link(url, codes: owner_codes(state_dir))) if local
|
|
391
399
|
puts lan_lines(ip: lan.ip, port: port, token: Token.read(token_path), others: lan.others, public: lan.public) if lan
|
|
392
400
|
if public_url
|
|
393
401
|
puts public_lines(public_url, token: Token.read(token_path), codes: owner_codes(state_dir), notes: public_notes)
|
|
@@ -395,8 +403,12 @@ module Samagotchi
|
|
|
395
403
|
puts "Press Ctrl-C to stop."
|
|
396
404
|
$stdout.flush # a log file isn't line-buffered
|
|
397
405
|
if open_browser
|
|
398
|
-
# With web.public_url the page opens through the proxy
|
|
399
|
-
|
|
406
|
+
# With web.public_url the page opens through the proxy; on the
|
|
407
|
+
# LAN with a one-time owner's code (#code=, its own).
|
|
408
|
+
open_target = if public_url then public_url.link(owner_codes(state_dir).issue(kind: :owner, ttl: LinkCodes::OWNER_TTL))
|
|
409
|
+
elsif local then local_owner_link(url, codes: owner_codes(state_dir))
|
|
410
|
+
else url
|
|
411
|
+
end
|
|
400
412
|
Thread.new do
|
|
401
413
|
sleep 0.8
|
|
402
414
|
open_url(open_target)
|
|
@@ -518,6 +530,19 @@ module Samagotchi
|
|
|
518
530
|
lines
|
|
519
531
|
end
|
|
520
532
|
|
|
533
|
+
# This machine's page on the LAN (web.host lan, no web.public_url),
|
|
534
|
+
# whose loopback needs the token too: +url+ with an owner's one-time
|
|
535
|
+
# code in its fragment (token_fragment.js trades it for the cookie on
|
|
536
|
+
# 127.0.0.1), never the token. One code per link.
|
|
537
|
+
def self.local_owner_link(url, codes: owner_codes)
|
|
538
|
+
"#{url}#code=#{codes.issue(kind: :owner, ttl: LinkCodes::OWNER_TTL)}"
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def self.local_lines(link)
|
|
542
|
+
["Local: #{link} ← opens chi web as you once, within #{LinkCodes::OWNER_TTL / 60} minutes " \
|
|
543
|
+
"(chi web again prints a new one; the LAN link works here too)"]
|
|
544
|
+
end
|
|
545
|
+
|
|
521
546
|
# The owner's one-time link codes, next to the grants.
|
|
522
547
|
def self.owner_codes(state_dir = nil) = LinkCodes.for_grants(Grants.path(state_dir: state_dir))
|
|
523
548
|
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1245,7 +1245,8 @@ module Samagotchi
|
|
|
1245
1245
|
# status, its rings and every save from here on say so.
|
|
1246
1246
|
@session.parent_id = Session.parent_override(@session_id, state_dir: @state_dir) || @session.parent_id
|
|
1247
1247
|
save_session
|
|
1248
|
-
result = @engine.run_turn(@session, prompt, pending_input: pending_input_drain,
|
|
1248
|
+
result = @engine.run_turn(@session, prompt, pending_input: pending_input_drain, input_waiting: method(:input_waiting?),
|
|
1249
|
+
**turn_args)
|
|
1249
1250
|
rescue StandardError => e
|
|
1250
1251
|
error = e
|
|
1251
1252
|
# Still running: no turn of the Engine's ended (it never began).
|
|
@@ -1468,6 +1469,13 @@ module Samagotchi
|
|
|
1468
1469
|
end
|
|
1469
1470
|
end
|
|
1470
1471
|
|
|
1472
|
+
# Whether a person's message waits that the next boundary merges
|
|
1473
|
+
# (SessionInbox.steer_waiting?): a running task_wait ends on it. Never
|
|
1474
|
+
# in a participant's turn, whose drain merges none.
|
|
1475
|
+
def input_waiting?
|
|
1476
|
+
!@participant_turn && SessionInbox.steer_waiting?(@session_dir)
|
|
1477
|
+
end
|
|
1478
|
+
|
|
1471
1479
|
# A participant's author fields (Engine#author_fields), {} for the owner.
|
|
1472
1480
|
def turn_author_fields(origin)
|
|
1473
1481
|
origin.participant? ? @engine.author_fields(origin.author, session: @session) : {}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.130.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -342,6 +342,7 @@ files:
|
|
|
342
342
|
- lib/samagotchi/guardrails.rb
|
|
343
343
|
- lib/samagotchi/guardrails/approval.rb
|
|
344
344
|
- lib/samagotchi/guardrails/approvals.rb
|
|
345
|
+
- lib/samagotchi/guardrails/chi_dirs.rb
|
|
345
346
|
- lib/samagotchi/guardrails/child_boundary.rb
|
|
346
347
|
- lib/samagotchi/guardrails/context.rb
|
|
347
348
|
- lib/samagotchi/guardrails/data_readers.rb
|
|
@@ -354,6 +355,7 @@ files:
|
|
|
354
355
|
- lib/samagotchi/guardrails/parent_approvals.rb
|
|
355
356
|
- lib/samagotchi/guardrails/parent_levels.rb
|
|
356
357
|
- lib/samagotchi/guardrails/parent_policy.rb
|
|
358
|
+
- lib/samagotchi/guardrails/path_resolve.rb
|
|
357
359
|
- lib/samagotchi/guardrails/protected_paths.rb
|
|
358
360
|
- lib/samagotchi/guardrails/read_only_shell.rb
|
|
359
361
|
- lib/samagotchi/guardrails/review_boundary.rb
|