samagotchi 0.128.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 +126 -1
- data/docs/architecture.md +13 -6
- data/docs/cli.md +20 -10
- data/docs/configuration.md +7 -2
- data/docs/guardrails.md +202 -33
- data/docs/internals/background-tasks.md +11 -0
- data/docs/internals/engine.md +4 -3
- data/docs/internals/participants.md +29 -6
- data/docs/internals/remote-sessions.md +5 -4
- data/docs/internals/web-push.md +4 -1
- data/docs/sessions.md +2 -2
- data/lib/samagotchi/bridge_client.rb +8 -2
- 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/guardrail_wiring.rb +6 -5
- data/lib/samagotchi/guardrails/chi_dirs.rb +64 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +26 -16
- data/lib/samagotchi/guardrails/outside.rb +68 -17
- data/lib/samagotchi/guardrails/pair_boundary.rb +89 -26
- data/lib/samagotchi/guardrails/pair_command.rb +327 -72
- data/lib/samagotchi/guardrails/parent_approvals.rb +8 -10
- data/lib/samagotchi/guardrails/path_resolve.rb +231 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +18 -24
- data/lib/samagotchi/guardrails/read_only_shell.rb +191 -8
- data/lib/samagotchi/guardrails/review_paths.rb +30 -14
- data/lib/samagotchi/guardrails/rm_targets.rb +7 -6
- data/lib/samagotchi/guardrails/rules.rb +23 -5
- data/lib/samagotchi/guardrails/scratch_writes.rb +4 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +75 -38
- data/lib/samagotchi/guardrails/shell_lex.rb +1 -1
- data/lib/samagotchi/guardrails/shell_paths.rb +85 -48
- data/lib/samagotchi/guardrails/targets.rb +131 -29
- 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/session_manager.rb +9 -15
- data/lib/samagotchi/system_one/self_protect.rb +127 -20
- data/lib/samagotchi/terminal_ui/exit_lines.rb +2 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- 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 +32 -25
- data/lib/samagotchi/web/proxy_gate.rb +8 -6
- data/lib/samagotchi/web/public/app.js +8 -1
- data/lib/samagotchi/web/public/participant.js +2 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +12 -5
- data/lib/samagotchi/web/server.rb +29 -4
- data/lib/samagotchi/worker.rb +91 -33
- data/lib/samagotchi/worker_idle_exit.rb +17 -0
- data/lib/samagotchi/worker_link.rb +13 -5
- metadata +3 -1
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -168,6 +168,10 @@ module Samagotchi
|
|
|
168
168
|
@loop_started = false
|
|
169
169
|
@left_loop = false
|
|
170
170
|
@signal_mutex = Mutex.new
|
|
171
|
+
# The exit being taken or taken (:exit, :delete, :restart), claimed
|
|
172
|
+
# under its own lock before #exit_request's slow stop-all (#claim_exit).
|
|
173
|
+
@exit_claim = nil
|
|
174
|
+
@exit_claim_mutex = Mutex.new
|
|
171
175
|
@grace_expired = false
|
|
172
176
|
# The running turn has ended (its end event) and its after_turn hooks
|
|
173
177
|
# run: a signal's grace waits for them (#signalled).
|
|
@@ -1241,7 +1245,8 @@ module Samagotchi
|
|
|
1241
1245
|
# status, its rings and every save from here on say so.
|
|
1242
1246
|
@session.parent_id = Session.parent_override(@session_id, state_dir: @state_dir) || @session.parent_id
|
|
1243
1247
|
save_session
|
|
1244
|
-
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)
|
|
1245
1250
|
rescue StandardError => e
|
|
1246
1251
|
error = e
|
|
1247
1252
|
# Still running: no turn of the Engine's ended (it never began).
|
|
@@ -1464,6 +1469,13 @@ module Samagotchi
|
|
|
1464
1469
|
end
|
|
1465
1470
|
end
|
|
1466
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
|
+
|
|
1467
1479
|
# A participant's author fields (Engine#author_fields), {} for the owner.
|
|
1468
1480
|
def turn_author_fields(origin)
|
|
1469
1481
|
origin.participant? ? @engine.author_fields(origin.author, session: @session) : {}
|
|
@@ -1583,41 +1595,89 @@ module Samagotchi
|
|
|
1583
1595
|
def exit_request(client_id, delete: false, restart: false, stop: false, ask: true)
|
|
1584
1596
|
# The Bridge serves before the idle-exit policy exists.
|
|
1585
1597
|
return WorkerIdleExit::Hold.new(reason: :starting) unless @idle_exit
|
|
1586
|
-
# One exit is already taken: the first one decides how the worker
|
|
1587
|
-
# leaves (a later stop-all can't turn a restart, whose reminders go to
|
|
1588
|
-
# the next worker, into a plain exit). Held at the leave, it is
|
|
1589
|
-
# forgotten and the next request counts again.
|
|
1590
|
-
return nil if @exit_requested
|
|
1591
1598
|
|
|
1592
|
-
if restart
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
recheck = @idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT)
|
|
1610
|
-
return recheck if recheck
|
|
1611
|
-
end
|
|
1599
|
+
kind = if restart then :restart
|
|
1600
|
+
elsif delete then :delete
|
|
1601
|
+
else :exit
|
|
1602
|
+
end
|
|
1603
|
+
# One exit at a time, and the first one taken decides how the worker
|
|
1604
|
+
# leaves (a later stop-all can't turn a restart, whose reminders go to
|
|
1605
|
+
# the next worker, into a plain exit): claimed before the slow
|
|
1606
|
+
# stop-all, so a request of another kind meanwhile is refused (409),
|
|
1607
|
+
# not told it went through. Held at the leave, it is forgotten and the
|
|
1608
|
+
# next request counts again.
|
|
1609
|
+
refused = claim_exit(kind)
|
|
1610
|
+
return refused.value if refused
|
|
1611
|
+
|
|
1612
|
+
hold = exit_hold(client_id, kind: kind, stop: stop, ask: ask)
|
|
1613
|
+
if hold
|
|
1614
|
+
forget_exit
|
|
1615
|
+
return hold
|
|
1612
1616
|
end
|
|
1613
1617
|
|
|
1614
1618
|
@stopped_holds = nil unless stop
|
|
1615
|
-
@exit_requested = true
|
|
1616
1619
|
@exit_requested_by = client_id
|
|
1617
|
-
@exit_deletes =
|
|
1618
|
-
@exit_restart = restart
|
|
1620
|
+
@exit_deletes = kind == :delete
|
|
1621
|
+
@exit_restart = kind == :restart
|
|
1622
|
+
@exit_requested = true
|
|
1619
1623
|
@waker.wake
|
|
1620
1624
|
nil
|
|
1625
|
+
rescue StandardError
|
|
1626
|
+
# A look or a stop that raised lets the claim go (the Bridge answers 500).
|
|
1627
|
+
forget_exit unless @exit_requested
|
|
1628
|
+
raise
|
|
1629
|
+
end
|
|
1630
|
+
|
|
1631
|
+
# One answer for an exit request that doesn't get the claim, boxed so a
|
|
1632
|
+
# nil (accepted) is told apart from "claimed".
|
|
1633
|
+
ExitRefused = Data.define(:value)
|
|
1634
|
+
|
|
1635
|
+
# @return [ExitRefused, nil] nil when this request now holds the claim;
|
|
1636
|
+
# else its answer: nil (accepted: an exit of the same kind is already
|
|
1637
|
+
# taken) or WorkerIdleExit.leaving (one of another kind is, or is still
|
|
1638
|
+
# being taken)
|
|
1639
|
+
def claim_exit(kind)
|
|
1640
|
+
@exit_claim_mutex.synchronize do
|
|
1641
|
+
next ExitRefused.new(value: nil) if @exit_claim == kind && @exit_requested
|
|
1642
|
+
next ExitRefused.new(value: WorkerIdleExit.leaving(@exit_claim)) if @exit_claim
|
|
1643
|
+
|
|
1644
|
+
@exit_claim = kind
|
|
1645
|
+
nil
|
|
1646
|
+
end
|
|
1647
|
+
end
|
|
1648
|
+
|
|
1649
|
+
# The taken exit is let go: held at the leave, or its hand-off failed.
|
|
1650
|
+
def forget_exit
|
|
1651
|
+
@exit_claim_mutex.synchronize do
|
|
1652
|
+
@exit_requested = nil
|
|
1653
|
+
@exit_restart = false
|
|
1654
|
+
@exit_claim = nil
|
|
1655
|
+
end
|
|
1656
|
+
end
|
|
1657
|
+
|
|
1658
|
+
# What keeps the worker up for this request, nil when nothing does; a
|
|
1659
|
+
# stop request stops what the user can stop first (#exit_request).
|
|
1660
|
+
# @return [WorkerIdleExit::Hold, nil]
|
|
1661
|
+
def exit_hold(client_id, kind:, stop:, ask:)
|
|
1662
|
+
if kind == :restart
|
|
1663
|
+
hold = @idle_exit.hold_for_restart
|
|
1664
|
+
return hold && WorkerIdleExit::Hold.new(reason: hold)
|
|
1665
|
+
end
|
|
1666
|
+
|
|
1667
|
+
# /exit names the tasks it waits for (D3), from the one look that found them.
|
|
1668
|
+
# The ask covers the delegate children too, but only for a plain /exit: an
|
|
1669
|
+
# exit that deletes the session, or one a UI won't ask about, ignores them.
|
|
1670
|
+
hold = @idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT, ask: ask && kind != :delete)
|
|
1671
|
+
return nil unless hold
|
|
1672
|
+
# A hold the user can't stop here: no ask, today's refusal.
|
|
1673
|
+
return hold unless hold.ask?
|
|
1674
|
+
# The ask's 409: the list, for the terminal to ask with.
|
|
1675
|
+
return hold unless stop
|
|
1676
|
+
|
|
1677
|
+
@stopped_holds = @hold_stop.stop_all
|
|
1678
|
+
# An unstoppable hold may have arrived while stopping (a child stopped
|
|
1679
|
+
# here is no longer counted).
|
|
1680
|
+
@idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT)
|
|
1621
1681
|
end
|
|
1622
1682
|
|
|
1623
1683
|
# #leave_idle for an exit a client asked for: check again with the event
|
|
@@ -1636,8 +1696,7 @@ module Samagotchi
|
|
|
1636
1696
|
@idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
|
|
1637
1697
|
end
|
|
1638
1698
|
if hold
|
|
1639
|
-
|
|
1640
|
-
@exit_restart = false
|
|
1699
|
+
forget_exit
|
|
1641
1700
|
Log.info(:worker, "exit_held", reason: hold)
|
|
1642
1701
|
next false
|
|
1643
1702
|
end
|
|
@@ -1666,8 +1725,7 @@ module Samagotchi
|
|
|
1666
1725
|
end
|
|
1667
1726
|
true
|
|
1668
1727
|
rescue StandardError => e
|
|
1669
|
-
|
|
1670
|
-
@exit_restart = false
|
|
1728
|
+
forget_exit
|
|
1671
1729
|
Log.warn(:worker, "reminders_carry_failed", error: e.class.name, msg: e.message)
|
|
1672
1730
|
false
|
|
1673
1731
|
end
|
|
@@ -39,6 +39,23 @@ module Samagotchi
|
|
|
39
39
|
def ask? = !holds.empty?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# The 409 reason for an exit request when another client's exit, of
|
|
43
|
+
# another kind, is already taken (Worker#exit_request): the first one
|
|
44
|
+
# decides how the worker leaves, so a restart asked after a plain /exit
|
|
45
|
+
# is refused rather than told "restarting".
|
|
46
|
+
LEAVING = { exit: :leaving_exit, delete: :leaving_delete, restart: :leaving_restart }.freeze
|
|
47
|
+
# What each of those says, for every client's refusal line
|
|
48
|
+
# (TerminalUI::ExitLines, SessionManager::RestartRefused).
|
|
49
|
+
LEAVING_WORDS = {
|
|
50
|
+
leaving_exit: "the worker is already leaving as a plain exit",
|
|
51
|
+
leaving_delete: "the worker is already leaving as an exit that deletes the session",
|
|
52
|
+
leaving_restart: "the worker is already leaving as a restart"
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
# @param kind [Symbol] the exit already taken (:exit, :delete or :restart)
|
|
56
|
+
# @return [Hold] the refusal for a request of another kind
|
|
57
|
+
def self.leaving(kind) = Hold.new(reason: LEAVING.fetch(kind))
|
|
58
|
+
|
|
42
59
|
# One kind of hold the exit ask offers to stop: +kind+ (:reminders,
|
|
43
60
|
# :background_tasks or :delegates) and its +items+ (#HoldItem). The 409's
|
|
44
61
|
# +holds+ is this in JSON: `{kind:, items: [{id:, label:, detail:}]}`.
|
|
@@ -121,7 +121,7 @@ module Samagotchi
|
|
|
121
121
|
# @param owner [OwnerLock::Owner, nil, :read] the lock's holder as the
|
|
122
122
|
# caller already read it (nil: free); :read reads it here
|
|
123
123
|
# @return [Live, Starting, Leaving, None, Stale, Skew]
|
|
124
|
-
def resolve(session_dir, unlink:, owner: :read
|
|
124
|
+
def resolve(session_dir, unlink:, owner: :read)
|
|
125
125
|
owner = OwnerLock.owner(session_dir) if owner == :read
|
|
126
126
|
stamp = file_stamp(session_dir)
|
|
127
127
|
sidecar = WorkerSidecar.read(session_dir)
|
|
@@ -136,7 +136,7 @@ module Samagotchi
|
|
|
136
136
|
|
|
137
137
|
# @return [BridgeClient, nil] a client for the session's Live worker
|
|
138
138
|
def client(session_id, session_dir:, unlink: true, host: BridgeClient::HOST, read_timeout: BridgeClient::READ_TIMEOUT)
|
|
139
|
-
link = resolve(session_dir, unlink: unlink
|
|
139
|
+
link = resolve(session_dir, unlink: unlink)
|
|
140
140
|
link.live? ? link.client(session_id, host: host, read_timeout: read_timeout) : nil
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -152,11 +152,11 @@ module Samagotchi
|
|
|
152
152
|
# @return [Live, Starting, Leaving, None, Stale, Skew] the outcome it
|
|
153
153
|
# stopped at, else the last one seen
|
|
154
154
|
def await(_session_id, session_dir:, timeout:, unlink: true, interval: BridgeClient::SPAWN_POLL_INTERVAL,
|
|
155
|
-
until: nil, give_up: nil
|
|
155
|
+
until: nil, give_up: nil)
|
|
156
156
|
wanted = binding.local_variable_get(:until) # a keyword: no plain read
|
|
157
157
|
last = nil
|
|
158
158
|
found = BridgeClient.poll(timeout, interval: interval) do
|
|
159
|
-
last = resolve(session_dir, unlink: unlink
|
|
159
|
+
last = resolve(session_dir, unlink: unlink)
|
|
160
160
|
last if (last.live? && (wanted.nil? || wanted.call(last))) || give_up&.call(last)
|
|
161
161
|
end
|
|
162
162
|
found || last
|
|
@@ -189,7 +189,15 @@ module Samagotchi
|
|
|
189
189
|
def unowned(session_dir, sidecar, owner, stamp, unlink)
|
|
190
190
|
return None.new(owner: owner) unless sidecar
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
if unlink
|
|
193
|
+
# A worker that took the lock and wrote its sidecar since the lock
|
|
194
|
+
# was read (before the stamp): its fresh file is the one stamped, so
|
|
195
|
+
# read the lock again just before the unlink, and take that worker.
|
|
196
|
+
fresh = OwnerLock.owner(session_dir)
|
|
197
|
+
return resolve(session_dir, unlink: false, owner: fresh) if fresh&.worker?
|
|
198
|
+
|
|
199
|
+
unlink_unchanged(session_dir, stamp)
|
|
200
|
+
end
|
|
193
201
|
Stale.new(sidecar: sidecar, reason: :lock_free, owner: owner)
|
|
194
202
|
end
|
|
195
203
|
private_class_method :unowned
|
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
|