samagotchi 0.129.0 → 0.130.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/docs/architecture.md +1 -1
  4. data/docs/cli.md +29 -11
  5. data/docs/configuration.md +7 -2
  6. data/docs/desktop.md +28 -9
  7. data/docs/guardrails.md +60 -16
  8. data/docs/internals/background-tasks.md +12 -0
  9. data/docs/internals/engine.md +4 -3
  10. data/docs/internals/participants.md +25 -6
  11. data/docs/internals/remote-sessions.md +5 -4
  12. data/docs/sessions.md +5 -3
  13. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  14. data/lib/samagotchi/desktop/macos.rb +99 -19
  15. data/lib/samagotchi/desktop_command.rb +7 -2
  16. data/lib/samagotchi/engine.rb +20 -4
  17. data/lib/samagotchi/engine_peers.rb +3 -1
  18. data/lib/samagotchi/guardrails/chi_dirs.rb +64 -0
  19. data/lib/samagotchi/guardrails/child_boundary.rb +5 -5
  20. data/lib/samagotchi/guardrails/outside.rb +22 -20
  21. data/lib/samagotchi/guardrails/pair_boundary.rb +14 -3
  22. data/lib/samagotchi/guardrails/pair_command.rb +70 -10
  23. data/lib/samagotchi/guardrails/parent_approvals.rb +8 -11
  24. data/lib/samagotchi/guardrails/path_resolve.rb +231 -0
  25. data/lib/samagotchi/guardrails/protected_paths.rb +17 -116
  26. data/lib/samagotchi/guardrails/review_paths.rb +13 -11
  27. data/lib/samagotchi/guardrails/rm_targets.rb +5 -5
  28. data/lib/samagotchi/guardrails/rules.rb +23 -5
  29. data/lib/samagotchi/guardrails/scratch_writes.rb +3 -2
  30. data/lib/samagotchi/guardrails/shell_git_dirs.rb +17 -12
  31. data/lib/samagotchi/guardrails/shell_paths.rb +18 -29
  32. data/lib/samagotchi/guardrails/targets.rb +24 -13
  33. data/lib/samagotchi/remote_command.rb +13 -3
  34. data/lib/samagotchi/remotes.rb +3 -2
  35. data/lib/samagotchi/review_start.rb +3 -2
  36. data/lib/samagotchi/session.rb +49 -4
  37. data/lib/samagotchi/session_inbox.rb +17 -0
  38. data/lib/samagotchi/system_one/self_protect.rb +34 -19
  39. data/lib/samagotchi/terminal_ui.rb +2 -0
  40. data/lib/samagotchi/tool_declarations.rb +1 -1
  41. data/lib/samagotchi/tools/builtins.rb +10 -1
  42. data/lib/samagotchi/tools/execute.rb +5 -2
  43. data/lib/samagotchi/tools/peers.rb +4 -0
  44. data/lib/samagotchi/tools/task_runtime.rb +4 -2
  45. data/lib/samagotchi/tools/task_wait.rb +26 -1
  46. data/lib/samagotchi/turn_state.rb +23 -4
  47. data/lib/samagotchi/update_command.rb +9 -1
  48. data/lib/samagotchi/version.rb +1 -1
  49. data/lib/samagotchi/web/app.rb +69 -25
  50. data/lib/samagotchi/web/participant_review.rb +9 -2
  51. data/lib/samagotchi/web/proxy_gate.rb +14 -6
  52. data/lib/samagotchi/web/server.rb +29 -4
  53. data/lib/samagotchi/web/token.rb +20 -1
  54. data/lib/samagotchi/worker.rb +16 -1
  55. metadata +3 -1
@@ -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
- open_target = public_url ? public_url.link(owner_codes(state_dir).issue(kind: :owner, ttl: LinkCodes::OWNER_TTL)) : url
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
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "openssl"
4
5
  require "securerandom"
5
6
  require_relative "../atomic_file"
6
7
 
@@ -14,11 +15,15 @@ module Samagotchi
14
15
  # can read, $XDG_STATE_HOME/samagotchi/web-token (0600). It is made on
15
16
  # the first LAN start and kept, so a phone's bookmark survives
16
17
  # restarts; `chi web --new-token` replaces it. A loopback-only chi web
17
- # never reads it.
18
+ # never reads it. The owner's cookie for this machine's 127.0.0.1
19
+ # holds #loopback_cookie, never the token: a server on any other local
20
+ # port is sent that cookie too.
18
21
  module Token
19
22
  FILE = "web-token"
20
23
  # 32 random bytes: 43 URL-safe characters.
21
24
  BYTES = 32
25
+ LOOPBACK_PREFIX = "chl_"
26
+ LOOPBACK_LABEL = "chi web loopback cookie v1"
22
27
 
23
28
  module_function
24
29
 
@@ -49,6 +54,20 @@ module Samagotchi
49
54
  SecureRandom.urlsafe_base64(BYTES)
50
55
  end
51
56
 
57
+ # The owner's 127.0.0.1 cookie on the LAN: an HMAC of a label keyed
58
+ # by +token+, so a leaked one gives nothing about the token, and
59
+ # rotating the token ends it. The gate takes it only as that cookie,
60
+ # from an unproxied loopback peer (App#loopback_cookie_peer?).
61
+ # @return [String, nil] "chl_" and 43 URL-safe characters; nil
62
+ # without a token
63
+ def loopback_cookie(token)
64
+ return nil if token.nil? || token.empty?
65
+
66
+ # URL-safe base64 without padding (pack: base64 isn't a default gem).
67
+ digest = OpenSSL::HMAC.digest("SHA256", token, LOOPBACK_LABEL)
68
+ LOOPBACK_PREFIX + [digest].pack("m0").tr("+/", "-_").delete("=")
69
+ end
70
+
52
71
  # Written whole under a temporary name, then renamed over +path+: a
53
72
  # reader never sees half a token. The file is 0600 from the start,
54
73
  # its folder 0700 when this makes it.
@@ -251,6 +251,13 @@ module Samagotchi
251
251
  ChildMove.clear_starting(@session_id, state_dir: @state_dir)
252
252
  ChildMove.adopt(@session_id, state_dir: @state_dir)
253
253
  @session = Session.load(@session_id, state_dir: @state_dir)
254
+ # A newer chi saved it: this worker keeps the fields it doesn't know
255
+ # but runs the session without them (docs/sessions.md).
256
+ if @session.newer_writer?
257
+ unknown = @session.extra_fields.keys.join(",")
258
+ Log.warn(:worker, "newer_writer", sid: @session_id, version: VERSION, written_by: @session.written_by,
259
+ unknown_fields: (unknown unless unknown.empty?))
260
+ end
254
261
  drop_dead_question
255
262
  # The running turn, on disk at its iteration boundaries.
256
263
  @checkpoint = TurnCheckpoint.new(session: @session, state_dir: @state_dir)
@@ -1245,7 +1252,8 @@ module Samagotchi
1245
1252
  # status, its rings and every save from here on say so.
1246
1253
  @session.parent_id = Session.parent_override(@session_id, state_dir: @state_dir) || @session.parent_id
1247
1254
  save_session
1248
- result = @engine.run_turn(@session, prompt, pending_input: pending_input_drain, **turn_args)
1255
+ result = @engine.run_turn(@session, prompt, pending_input: pending_input_drain, input_waiting: method(:input_waiting?),
1256
+ **turn_args)
1249
1257
  rescue StandardError => e
1250
1258
  error = e
1251
1259
  # Still running: no turn of the Engine's ended (it never began).
@@ -1468,6 +1476,13 @@ module Samagotchi
1468
1476
  end
1469
1477
  end
1470
1478
 
1479
+ # Whether a person's message waits that the next boundary merges
1480
+ # (SessionInbox.steer_waiting?): a running task_wait ends on it. Never
1481
+ # in a participant's turn, whose drain merges none.
1482
+ def input_waiting?
1483
+ !@participant_turn && SessionInbox.steer_waiting?(@session_dir)
1484
+ end
1485
+
1471
1486
  # A participant's author fields (Engine#author_fields), {} for the owner.
1472
1487
  def turn_author_fields(origin)
1473
1488
  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.129.0
4
+ version: 0.130.1
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