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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -1
  3. data/docs/architecture.md +13 -6
  4. data/docs/cli.md +20 -10
  5. data/docs/configuration.md +7 -2
  6. data/docs/guardrails.md +202 -33
  7. data/docs/internals/background-tasks.md +11 -0
  8. data/docs/internals/engine.md +4 -3
  9. data/docs/internals/participants.md +29 -6
  10. data/docs/internals/remote-sessions.md +5 -4
  11. data/docs/internals/web-push.md +4 -1
  12. data/docs/sessions.md +2 -2
  13. data/lib/samagotchi/bridge_client.rb +8 -2
  14. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  15. data/lib/samagotchi/engine.rb +20 -4
  16. data/lib/samagotchi/engine_peers.rb +3 -1
  17. data/lib/samagotchi/guardrail_wiring.rb +6 -5
  18. data/lib/samagotchi/guardrails/chi_dirs.rb +64 -0
  19. data/lib/samagotchi/guardrails/child_boundary.rb +26 -16
  20. data/lib/samagotchi/guardrails/outside.rb +68 -17
  21. data/lib/samagotchi/guardrails/pair_boundary.rb +89 -26
  22. data/lib/samagotchi/guardrails/pair_command.rb +327 -72
  23. data/lib/samagotchi/guardrails/parent_approvals.rb +8 -10
  24. data/lib/samagotchi/guardrails/path_resolve.rb +231 -0
  25. data/lib/samagotchi/guardrails/protected_paths.rb +18 -24
  26. data/lib/samagotchi/guardrails/read_only_shell.rb +191 -8
  27. data/lib/samagotchi/guardrails/review_paths.rb +30 -14
  28. data/lib/samagotchi/guardrails/rm_targets.rb +7 -6
  29. data/lib/samagotchi/guardrails/rules.rb +23 -5
  30. data/lib/samagotchi/guardrails/scratch_writes.rb +4 -1
  31. data/lib/samagotchi/guardrails/shell_git_dirs.rb +75 -38
  32. data/lib/samagotchi/guardrails/shell_lex.rb +1 -1
  33. data/lib/samagotchi/guardrails/shell_paths.rb +85 -48
  34. data/lib/samagotchi/guardrails/targets.rb +131 -29
  35. data/lib/samagotchi/remote_command.rb +13 -3
  36. data/lib/samagotchi/remotes.rb +3 -2
  37. data/lib/samagotchi/review_start.rb +3 -2
  38. data/lib/samagotchi/session_inbox.rb +17 -0
  39. data/lib/samagotchi/session_manager.rb +9 -15
  40. data/lib/samagotchi/system_one/self_protect.rb +127 -20
  41. data/lib/samagotchi/terminal_ui/exit_lines.rb +2 -1
  42. data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
  43. data/lib/samagotchi/terminal_ui.rb +2 -0
  44. data/lib/samagotchi/tool_declarations.rb +1 -1
  45. data/lib/samagotchi/tools/builtins.rb +10 -1
  46. data/lib/samagotchi/tools/peers.rb +4 -0
  47. data/lib/samagotchi/tools/task_wait.rb +26 -1
  48. data/lib/samagotchi/turn_state.rb +23 -4
  49. data/lib/samagotchi/version.rb +1 -1
  50. data/lib/samagotchi/web/app.rb +32 -25
  51. data/lib/samagotchi/web/proxy_gate.rb +8 -6
  52. data/lib/samagotchi/web/public/app.js +8 -1
  53. data/lib/samagotchi/web/public/participant.js +2 -0
  54. data/lib/samagotchi/web/public/verdict_fresh.js +12 -5
  55. data/lib/samagotchi/web/server.rb +29 -4
  56. data/lib/samagotchi/worker.rb +91 -33
  57. data/lib/samagotchi/worker_idle_exit.rb +17 -0
  58. data/lib/samagotchi/worker_link.rb +13 -5
  59. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b175cb8468dd1b6dccbb74c3c372b0796aa18126b47034d07d11d6ead1e1e22
4
- data.tar.gz: e6b6ea98bf78a7a43962a70ab6daf3cd35519c7db1cefdae44ad8021d4274b75
3
+ metadata.gz: 5830c4028607020136e2f0493d0d365f5d10889fa8ff2a493b979de7b7e164b9
4
+ data.tar.gz: 28f0fda8ba4b824af19381b12274262c0858f879565b58ab4a6a12e18b82f64e
5
5
  SHA512:
6
- metadata.gz: adcfe4e020e6829703e1b9e1d2836d53bb3ddc336da1001c598853352eef75a3ca3df25b7e4d07d292bdaf8b413be228e57487a8b570365e2fba4cecd98490f0
7
- data.tar.gz: d4b57b741cd271740cb0826e0ff50845a5213c1de30b28c9477ba1b126e5448511f5b73861a3380ae2a93e4102e41df9d4093b14a193a61dcbdd7ed6894a11cd
6
+ metadata.gz: 38f04438ffc54f5569abbbfb5fdfdd7470ca805926f3593969924a5a248b29ff266f2eec6972aff25d29395982b255b6518989f174a1579b227a711ae1aeec68
7
+ data.tar.gz: 3dc45f2617ca67e11f8cf563c83ea1c979142d798d900b000b5f1ae0c12ffb559b692cdec8f8f614da09eeb8cd176a1e7e5af59d47958ddffc838b3faa8f8d9d
data/CHANGELOG.md CHANGED
@@ -7,6 +7,129 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.130.0] - 2026-10-29
11
+
12
+ ### Changed
13
+
14
+ - A message you send while the model is blocked in `task_wait` ends the wait within half a second instead of
15
+ waiting for the task: the task keeps running, the result says `wait_result: message_waiting`, and the model reads
16
+ your message at once and decides whether to wait again. A queued message, a plugin's steer and a delegate child's
17
+ report don't end it. A worker started before the upgrade keeps the old behaviour until it is restarted.
18
+
19
+ - `chi web --web-host lan` prints a `Local:` link for this machine with an owner's one-time code (10 minutes, once),
20
+ and `--open` opens it; a second `chi web` prints a new one. `chi remote add` without a token refuses a chi web that
21
+ asks for one (through an SSH tunnel to its loopback) instead of saving a remote that would 401.
22
+
23
+ ### Security
24
+
25
+ - chi web on the LAN (`web.host: lan`) now asks this machine's 127.0.0.1 for its access token too: a token-less
26
+ loopback request was the owner, so a participant's pair turn could `curl` it (by a decimal IP or a DNS alias past
27
+ the pair boundary's text check) to mint grants, queue owner turns and read every session. Only chi's token-less
28
+ `GET /api/info` probe (its version, pid, features, LAN address and start folder) is answered without it. A chi web
29
+ on 127.0.0.1 only is unchanged. After the upgrade an open 127.0.0.1 tab (and a push link opened in one) lands on
30
+ the 401 page: open the `Local:` link chi web prints (its one-time code sets the cookie; `chi web --open` opens
31
+ it), the LAN link, or paste the token. A browser left with an ended participant cookie gets the ended page there
32
+ instead of becoming the owner again.
33
+ - A participant's pair turn can no longer open a URL or a file in the owner's apps (`open`, `xdg-open`, a browser,
34
+ `osascript`'s `open location`) or listen on a port (`nc -l`, `socat …LISTEN`, `python -m http.server`, `php -S`, a
35
+ static server): a page opened at a port it listens on would catch the owner's chi web cookie.
36
+ - System 1 no longer auto-allows a `write`/`edit` anywhere in chi's state dir (`state/sessions/x.json`), as it already
37
+ refused a command naming one: a file tool's paths are judged by the same dirs as a command's words.
38
+ - On a disk that folds case (macOS) a write to `<state dir>/CONTEXT/…` before the attached context dir exists is denied
39
+ as one to it (`chi-context`), and a command naming it touches chi: the protected-path checks compared the typed case.
40
+ - `git -C $XDG_CONFIG_HOME …` (an XDG dir or `$TMPDIR`) counts as the folder it names for `git: outside_repo` and the
41
+ delegate child's boundary, as a `cd` there already did; it was an unknown folder that never asked.
42
+ - A guardrail rule's `path:` glob also matches the path as it resolves: `write hl/pre-commit` with `hl` a symlink to
43
+ `.git/hooks` is denied by `git-hooks-write` (a relative glob is read from the repo root as it resolves, `/tmp` being
44
+ `/private/tmp` on macOS). Rules only ask or deny, so this only adds votes.
45
+ - A participant's pair turn can no longer glob or `$var` its way into a secret through an XDG dir: `ls
46
+ $XDG_CONFIG_HOME/samago*` is denied like `ls ~/.config/samago*`. After `$TMPDIR` only what lands in a denied folder
47
+ counts, so `ls $TMPDIR/*` stays allowed when chi's state dir is in tmp (isolated test runs).
48
+ - A `hooks.hooks_dir` configured outside chi's config dir is one of chi's dirs for participants too: neither a review
49
+ nor a pair turn reads it (the owner's own checks already counted it).
50
+ - `outside_repo` and `rm: outside_tmp` compare a path with an existing root in any case on a disk that folds case,
51
+ as the protected-path checks do; a session root that doesn't exist yet keeps the case it was typed in.
52
+
53
+ ## [0.129.0] - 2026-10-28
54
+
55
+ ### Changed
56
+
57
+ - A delegate child's read-only command that writes only into a tmp dir (`sqlite3 -readonly -safe ../main/x.db "…" >
58
+ /tmp/rows`, `cp ../main/x.db /tmp/`, `… | tee /tmp/f`) no longer asks as a change outside its folder
59
+ (`child-boundary`); a tmp path that links back into the repository still asks, as does a target spelt with `$` or
60
+ `~`, or one after a `cd`/`pushd` the shell may not have taken (anything but one existing dir joined by `&&`).
61
+
62
+ ### Fixed
63
+
64
+ - The owner page's Verify fresh button no longer stays disabled ("restart this session to use Verify") after the
65
+ session's worker comes back with the `verify` feature: the cards follow the session's update without a reload.
66
+ - The web's "Couldn't start a fresh check." under a verdict card no longer vanishes at the next redraw (the participant
67
+ page re-creates its cards on every update): the page keeps it until the next attempt.
68
+ - A `write` or `edit` of a dangling symlink (or a path through one) or of a file with another hard link no longer
69
+ counts as in a tmp dir or in the session's repo: a write there lands outside, so the delegate child's
70
+ `child-boundary` and `path: outside_repo` rules ask for it.
71
+ - A `task_create` call's `env:` now counts in the guardrails' path checks: `env: {TMPDIR: <main checkout>}` with
72
+ `echo x > $TMPDIR/a.txt` (or a `HOME`, `GIT_DIR`, `GIT_WORK_TREE` there) asks as the delegate child's
73
+ `child-boundary`, and `rm -rf`, chi-dir and `git: outside_repo` checks read it too. An `env:` setting `BASH_ENV`,
74
+ `ENV`, `CDPATH`, `IFS`, an exported `BASH_FUNC_…`, `LD_PRELOAD` or a git config/diff-program variable makes the
75
+ command not read-only.
76
+ - An exit and a restart asked of one worker at once (an attached terminal's `/exit` with "stop them and exit" while
77
+ chi web's "Restart workers" runs) no longer both hear yes: the first one taken decides, and the other is refused
78
+ ("the worker is already leaving as a plain exit"), so a restart no longer reports "restarted" for a worker that
79
+ left with no successor.
80
+ - A delegate child's `task_create` whose `env:` redefines the shell (`BASH_ENV`, an exported `BASH_FUNC_cat%%`,
81
+ `CDPATH`, …) now asks as `child-boundary` from its own folder too: `cat a.txt` there with a startup file or an
82
+ exported function that writes the main checkout no longer ran unasked.
83
+
84
+ ### Security
85
+
86
+ - A participant's pair turn (`chi share`, a working session) can no longer call this machine's chi web on a loopback
87
+ name (`curl -X POST 127.0.0.1:4567/api/sessions/ID/participants`, `localhost`, `[::1]`, `0.0.0.0`, the host name,
88
+ with chi web's recorded or configured port, by any client or script): a LAN chi web takes a token-less loopback
89
+ request as the owner's, so the command could mint a grant or queue an owner turn. chi web's own auth is unchanged.
90
+ - A pair turn's "a write outside the project always asks the owner" now fails closed. A command runs unasked only when
91
+ it only reads, only writes (`>`, `cp`, `tee`) into the project or a tmp dir, or stays inside the project (it runs
92
+ there and every path it names lands there or in a tmp dir). `rm -rf ~/x`, `sed -i … ~/x`, `curl -o ~/x`,
93
+ `dd of=~/x`, `touch`/`mkdir` outside, `perl -pi`, `python -c "open('/…', 'w')"`, `cd .. && make`, and a command with
94
+ a `$var`, a `$(…)` (but a commit message's heredoc), `xargs` or a `cd` it can't follow now ask. A tmp path is one by
95
+ the delegate child's rule (`/var/tmp` too, not the tmp dir the project is in, never through a dangling symlink or
96
+ onto a file with another hard link).
97
+ - A pair turn in a session whose folder is the home folder or above it asks before every write (the review boundary
98
+ already read nothing there).
99
+ - A pair command that makes a link (`ln`, `cp -s`/`-l`, a script's `symlink`) asks the owner, and one linking to home
100
+ or a folder above a secret is denied: `ln -s ~/.config g && cat g/gh/hosts.yml` passed, as the link didn't exist yet
101
+ when the paths were judged.
102
+ - A pair turn can no longer write git's hooks (`.git/hooks/*`, by `write`/`edit` or a command), schedule a command
103
+ (`crontab`, `at`, `batch`), run a credential printer (`gh auth token`, `git credential fill`, `gcloud … print-access-token`,
104
+ `op`, `aws configure get`, …) or `ps -E`, read the owner's background tasks (`task_wait`, `task_get`, `task_list`), or
105
+ run a program whose name is a `$var`, a `$(…)` or a glob (`bin/ch? …`).
106
+ - A guardrail check no longer cuts a heredoc's body out of the command it then runs (since 0.81.0,
107
+ `ShellLex.code_text` built its result on the caller's string): with the guardrails bundle's `command:` rules, an
108
+ owner's `git commit -m "$(cat <<'EOF' … EOF)"` or `git commit -F - <<EOF` could run with an empty message, and a pair
109
+ turn's `cat > docs/x.md <<EOF … EOF` with an empty body.
110
+ - Guardrails take a path's `..` the way the kernel does: after a symlinked folder it climbs out of the link's target.
111
+ A participant's review turn could read `node_modules/foo/../../.config/gh/hosts.yml` (with `foo` an `npm link` to
112
+ `~/src/foo`) as a project file while the read opened `~/.config/gh/hosts.yml`; a pair turn's secret check, the
113
+ protected paths (`mem/../config.yml` with `mem` a link to chi's memories), `outside_repo`, the delegate child's
114
+ boundary, `touches: chi_dirs` and `rm: outside_tmp` had the same gap. Each now refuses or asks when either reading of
115
+ the path does, and a write through a dangling symlink to `config.yml`, the hooks or `.bundles/` is asked or denied
116
+ like the file it creates.
117
+ - A shell `cd` or `pushd` whose `..` climbs out of a symlinked folder's target now counts as landing in either folder:
118
+ zsh (chi's shell on macOS) and bash chdir to the physical one when the logical one doesn't exist, and `cd -P`
119
+ always does. A pair turn's `cd node_modules/foo/../../.config/gh && cat hosts.yml` (or `cd -P
120
+ node_modules/foo/../.. && echo x > .zshrc`) was allowed while it read or wrote outside the project; `touches:
121
+ chi_dirs`, `git: outside_repo` and the delegate child's boundary read the same cd logically only. Later words are
122
+ now resolved and judged against both folders, and `cd -` leaves the folder unknown.
123
+ - System 1 (`on` mode) no longer auto-allows a command that climbs out of chi's memories folder into its config:
124
+ `cd …/memories; cd ..; echo 'mode: off' >> config.yml`, `cd "${PWD%/*}"` or `cd $(dirname $PWD)` passed its
125
+ own-files check, since only the text was read. The check now resolves the command's words like
126
+ `shell-touches-chi` (cd chains, symlinks, `..`, the call's `env:`), so `cd ~/.config; cd samagotchi; cat
127
+ config.yml` is refused too. A memories command keeps its exemption while its text shows no climb (`..`, `$`,
128
+ backticks, `cd -`) and every word resolves or is data (a `python3 -c` or heredoc body, `echo "- a note"`); a call
129
+ whose `env:` redefines the shell is never auto-allowed.
130
+
131
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
132
+
10
133
  ## [0.128.0] - 2026-10-28
11
134
 
12
135
  ### Changed
@@ -4810,7 +4933,9 @@ and long-lived sessions.
4810
4933
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4811
4934
  hotkey panel that send selected text or the clipboard to your sessions.
4812
4935
 
4813
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.128.0...HEAD
4936
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.130.0...HEAD
4937
+ [0.130.0]: https://github.com/dm1try/samagotchi/compare/v0.129.0...v0.130.0
4938
+ [0.129.0]: https://github.com/dm1try/samagotchi/compare/v0.128.0...v0.129.0
4814
4939
  [0.128.0]: https://github.com/dm1try/samagotchi/compare/v0.127.0...v0.128.0
4815
4940
  [0.127.0]: https://github.com/dm1try/samagotchi/compare/v0.126.0...v0.127.0
4816
4941
  [0.126.0]: https://github.com/dm1try/samagotchi/compare/v0.125.0...v0.126.0
data/docs/architecture.md CHANGED
@@ -120,7 +120,7 @@ terminal renders the events it gets over the Bridge.
120
120
  - Attached (`bin/chi`, `--attach ID`, `--shared [--resume ID]`) → `TerminalUI::AttachLauncher`: no `Engine`
121
121
  and no `OwnerLock`; finds or starts the session's worker and runs `TerminalUI::AttachedLoop`
122
122
  as a client of its Bridge (`BridgeClient#follow`, `post_turn`, `post_command`, `cancel`, `answer`,
123
- `dismiss_question`). The worker runs in the session's `working_directory`, so `!cmd` and
123
+ `dismiss_question`; opening the SSE connection answers `BridgeClient::StreamOpen::Opened`, `Refused` or `Unreachable`). The worker runs in the session's `working_directory`, so `!cmd` and
124
124
  the tools don't depend on where the terminal attached from.
125
125
  - `bin/chi web` → builds `Web::Server` (Rack+WEBrick on `127.0.0.1:4567`, `--port`/`SAMAGOTCHI_WEB_PORT`, `--open`;
126
126
  `web.host: lan` adds a LAN address, `--new-token` replaces its access token).
@@ -153,7 +153,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
153
153
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
154
154
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
155
155
  | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, localhost-only), and it answers only a request that names its secret (`X-Chi-Bridge`, from the worker's 0600 `bridge.json`). |
156
- | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
156
+ | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and with it or LAN mode chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
157
157
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
158
158
 
159
159
  - `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
@@ -173,6 +173,8 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
173
173
  but `RelineSeam` (prepended to `Reline::LineEditor`) sends its drawing to the
174
174
  `Screen`. The saved history and cards in a join or resync are rendered by
175
175
  `TerminalUI::JoinReplay` with the loop's screen, view, link, gap clock and renderer.
176
+ The loop's concerns live in mixins under `terminal_ui/attached_loop/`: `AttachedLoop::Composer`, `Questions`,
177
+ `Queueing`, `Sending` and `ExitCommands` (the `/exit` ask's reply text is `TerminalUI::ExitLines`).
176
178
  With input from a pipe (`chi -p X </dev/null`), `TerminalUI::OwnTurns` keeps this
177
179
  run's own prompts until their turns end; `#run` distinguishes an empty answer from a
178
180
  prompt this terminal removed before it ran (both exit 1), as well as chi's other outcomes.
@@ -376,7 +378,10 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
376
378
  the event log held (`WorkerIdleExit#hold_for_request`): `200 {status: "exiting"}` and it leaves
377
379
  like an idle exit, or `409 {status: "held", reason:}` with `turn_running`, `input_queued`,
378
380
  `continue_offered`, `client_connected` (a stream not named by the asker), `reminders`,
379
- `background_tasks` (with `tasks: [{id, command}]`: the running tasks a finish wake watches) or `starting`.
381
+ `background_tasks` (with `tasks: [{id, command}]`: the running tasks a finish wake watches), `starting`, or
382
+ `leaving_exit` / `leaving_delete` / `leaving_restart` when another client's exit of another kind is already taken
383
+ (`Worker#claim_exit`, claimed before a stop-all; the words are `WorkerIdleExit::LEAVING_WORDS`). A second exit of
384
+ the kind already taken is answered `200` as that one.
380
385
  - `GET /session/:id/state` — `session_state_snapshot` (JSON).
381
386
  - `GET /session/:id/stats` — `Engine#stats_snapshot` for attached `/stats`: the metrics, with the context window and prompt profile asked from the server before the first turn.
382
387
  - `GET /session/:id/snapshot` — the snapshot frame's content as one request (the web server renders
@@ -506,7 +511,8 @@ for client discovery, with the Bridge's secret (see above).
506
511
  reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
507
512
  outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
508
513
  session's Bridge, a free lock is the kernel's proof the worker died). In order: no lock holder, or a chi REPL's: a
509
- sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read), else `None`;
514
+ sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read and the lock,
515
+ read again just before, is still free: a worker that took it meanwhile is resolved as that worker), else `None`;
510
516
  a holder with no kind yet: `Starting`; a leaving worker: `Leaving` (marked before its Bridge stops, on every way out:
511
517
  `Worker#teardown` marks it too, with `TEARDOWN_LEAVE_WAIT`); a worker whose sidecar names no port or another
512
518
  pid than the holder's (a worker that has not written its own yet): `Starting`, `stuck` once it has held the lock
@@ -565,7 +571,8 @@ it holds with `update.notify: off`; a child that prints no rows fails with its o
565
571
  done install refreshes the hub's installed version (`SessionHub#refresh_installed`) before its `done`.
566
572
  Worker restarts target every worker older than the version installed now (refreshed the same way)
567
573
  and publish per-session results. Only one of an install, a worker restart pass and a web restart runs
568
- at a time (409 `update_running`). `POST /api/update/restart_web` (409 `restart_unavailable` without a
574
+ at a time (409 `update_running`). `POST /api/update/restart_workers` (`handle_restart_workers`) starts the worker pass
575
+ by itself. `POST /api/update/restart_web` (409 `restart_unavailable` without a
569
576
  chi wrapper to exec) schedules WEBrick shutdown only after its response is sent; after the hub and
570
577
  listener stop, `Server` execs the RubyGems chi wrapper with every flag chi web was started with, as
571
578
  `RunOptions`' parser recorded them (`RunOptions.web_restart_argv`), but the one-shot `--open` and
@@ -618,7 +625,7 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
618
625
  |---|---|---|---|---|
619
626
  | `GET /api/sessions/resolve?id=` | owner | an id or a unique prefix | 200 `{id}` | 404 `not_found`; 400 `ambiguous_id` with `ids` |
620
627
  | `GET /api/sessions/summaries` | owner | `dir` (a folder: its project; none: every project), `cwd`, `live=1`, `archived=1`, `tests=0`, `sort`, `order`, `limit` | 200: the rows `chi sessions list` prints (`SessionManager.session_summaries`: `status`, `live`, `busy`, `owner`, `recap`, `ctx_pct`, `waiting`, `waiting_id`, `title`, `preview`, …) | 400 `invalid_dir`, `invalid_cwd` |
621
- | `GET /api/projects?dir=` | owner | optional page-scope folder | 200: project rows with counts, roots, default/current marks; no `dir`: no current project | 400 `invalid_dir`; 503 `no_hub` |
628
+ | `GET /api/projects?dir=` | owner | optional page-scope folder | 200: project rows (`Web::ProjectList`: the hub's projects plus the start project) with counts, roots, default/current marks; no `dir`: no current project | 400 `invalid_dir`; 503 `no_hub` |
622
629
  | `POST /api/sessions` (more fields) | owner | `toolkit`, `tools`, `no_tools`, `parent_answers`; `client_id` (`cli:send`/`cli:answer`: the first line is checked as chi send checks it); `check_vision: true` (images follow) | 201 adds `model_warning`, `parent_answers_warning` (a level looser than config.yml's, capped) and `first_turn_id` (the id the prompt it starts with gets for its turn; null for an `idle` create) | 400 `invalid_toolkit`, `invalid_parent_answers`, `invalid_continues` (beside `continues`); 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
623
630
  | `POST /api/sessions/:id/turn` (more fields) | owner | `client_id` as above; `check_vision: true` with `images` | as before | 403 `parent_answers_refused`, `parent_continue_refused`; 422 `no_vision` |
624
631
  | `POST /api/sessions/:id/wait/prepare` | owner | `question_id` (absent: the pending one; `null`: none), `turn`, `created`, `busy` (+ `queue`) | 200 `{session_id, cursor, baseline: {messages, question_id, last_turn}, own_turn, known_turn, running, live, settled, last_turn_id}` (`WaitStart`) | 409 `busy`, `waiting` (`SendCommand::Busy`'s words, and its fields: `question`, `parent_strategy`) |
data/docs/cli.md CHANGED
@@ -445,7 +445,7 @@ of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
445
445
  `--attach`). They all see the same turns as they happen, and any of them can send
446
446
  a prompt, also while a turn runs (it merges into that turn as steering: the model
447
447
  is told it is a steer and who sent it). Steering waits for the model's next step
448
- and cuts nothing. `/cut TEXT` in a terminal or the web composer (or `chi send --cut`) asks for a cut:
448
+ and cuts nothing; a `task_wait` the model is blocked in ends early for it (the task keeps running). `/cut TEXT` in a terminal or the web composer (or `chi send --cut`) asks for a cut:
449
449
  once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
450
450
  20), now or while your message waits, that generation is cut and the step starts again with your message, marked
451
451
  `↪ cut in for your message` (see
@@ -1210,7 +1210,8 @@ pills there (a review turn refuses `/` lines).
1210
1210
 
1211
1211
  After the presets, both pages have a built-in **Verify**: the quote, then
1212
1212
  an ask to check the claim against the code and end on a line `Verdict:
1213
- confirmed|refuted|unclear — <reason>`. On your page a selection in a turn
1213
+ confirmed|refuted|unclear — <reason>`. A session whose worker predates Verify (started by an older chi) refuses it (409
1214
+ `verify_unavailable`); a participant's page disables Verify and Verify fresh with a hint to restart the session. On your page a selection in a turn
1214
1215
  also offers **Copy link**: a link to that turn and quote for a participant's
1215
1216
  browser (the one their review link was opened in), which opens their page
1216
1217
  scrolled to the quote and highlights it; with no one invited, a link to it
@@ -1248,6 +1249,7 @@ chi web --web-host lan
1248
1249
 
1249
1250
  ```
1250
1251
  Chi Web on http://127.0.0.1:4567/?dir=/Users/me/projects/app (public: …)
1252
+ Local: http://127.0.0.1:4567/?dir=/Users/me/projects/app#code=chc_… ← opens chi web as you once, within 10 minutes (…)
1251
1253
  LAN: http://192.168.1.55:4567/?token=… ← anyone with this link can run commands as you
1252
1254
  Plain http: the link and your traffic can be read by anyone on this Wi-Fi.
1253
1255
  <the link's QR code>
@@ -1263,13 +1265,20 @@ web app on iOS has cookies of its own: if it opens on "needs chi web's
1263
1265
  access token", paste the token there (the part of the link after
1264
1266
  `token=`), or open the link once in it.
1265
1267
 
1266
- - Every request from another machine needs the token (the cookie, or
1267
- `Authorization: Bearer <token>` for curl); this Mac (127.0.0.1) needs none.
1268
- Without it the page says how to get in and the API answers 401.
1268
+ - Every request needs the token (the cookie, or `Authorization: Bearer
1269
+ <token>` for curl), this Mac's on 127.0.0.1 too: a participant's pair turn
1270
+ runs commands here. Without it the page says how to get in (with a box to
1271
+ paste the token) and the API answers 401. On this Mac open the `Local:`
1272
+ link (`chi web --open` opens it): its one-time code (10 minutes, once) sets
1273
+ the cookie for 127.0.0.1 without the token in the address; the LAN link
1274
+ works here too. chi's own probe (`GET /api/info` with no token or cookie)
1275
+ gets a few fields (version, pid, features, the LAN address, the start
1276
+ folder), nothing of the sessions.
1269
1277
  - The token lives in `$XDG_STATE_HOME/samagotchi/web-token` (0600).
1270
1278
  `chi web --new-token` replaces it: a running chi web takes the new one at
1271
1279
  once, and every phone has to scan the new QR code.
1272
- - A second `chi web` prints the LAN link and QR code again. A plain
1280
+ - A second `chi web` prints the LAN link and QR code again, and a new
1281
+ `Local:` link (`--open` opens it). A plain
1273
1282
  `chi web --web-host lan` while a chi web without LAN access runs asks you
1274
1283
  to stop that one first. `chi self` says whether chi web runs on the LAN.
1275
1284
  - `lan` picks the first private address (10.x, 172.16–31.x, 192.168.x) on an
@@ -1413,10 +1422,11 @@ chi --remote work sessions list
1413
1422
  - Forward only chi web's port, never a worker's: a session's Bridge answers
1414
1423
  only chi's own clients on its machine (they read its secret from the
1415
1424
  session's folder), so a forwarded worker port is of no use.
1416
- - Without `web.public_url` there, a request that arrives through the tunnel
1417
- looks local and needs no token, so any process here reaching the forwarded
1418
- port acts as you there. With it set, every request needs the token, which
1419
- `chi --remote` sends as Bearer.
1425
+ - A chi web there with `web.host: lan` or `web.public_url` asks every
1426
+ request for the token, one through the tunnel too (`chi remote add` without
1427
+ `--token-file` says so), and `chi --remote` sends it as Bearer. One on
1428
+ 127.0.0.1 only takes a request through the tunnel as local, with no token,
1429
+ so any process here reaching the forwarded port acts as you there.
1420
1430
  - `-L` binds 127.0.0.1 unless you write `-g` or `0.0.0.0:`: don't.
1421
1431
  - Attaching works through the tunnel too (`chi --remote work --attach ab12`),
1422
1432
  plain http, no proxy.
@@ -1133,6 +1133,10 @@ never asks for a cut, nor does a queued one (`/queue TEXT`, `chi send --queue`,
1133
1133
  thinking is lost. A generation that has streamed visible text or a tool call is never cut, and a plugin's steer
1134
1134
  (`ctx.steer`, `ctx.sessions.send`) never cuts.
1135
1135
 
1136
+ A message that would go in at the next step (any but a queued one, one with images or a participant's) also ends a
1137
+ `task_wait` the model is blocked in, within half a second: the task keeps running, the model is told a message waits,
1138
+ reads it at once and decides whether to wait again. A plugin's steer and a delegate child's report don't end one.
1139
+
1136
1140
  The wait resolves per model, in this order: env `SAMAGOTCHI_STEER_CUT_AFTER`, then
1137
1141
  `models.<key>.steer_cut_after`, `hosts.<name>.steer_cut_after`, `steer.cut_after` in config.yml, then 20. `0` means
1138
1142
  never cut (a slow local thinker is left alone); a bad value warns and is ignored, like the other keys here.
@@ -1662,7 +1666,8 @@ web:
1662
1666
  - It opens the file as it is now, not the agent's copy: after later edits a diff line's number can point elsewhere,
1663
1667
  and a file in a worktree that was removed won't open. The row's diff and output are what the agent saw.
1664
1668
  - Only a viewer on this machine opens the editor: a loopback request, or this machine on its own LAN address
1665
- (`web.host: lan`, the LAN URL or its QR code opened here). A phone gets the targets that need no editor (an
1669
+ (`web.host: lan`, the LAN URL or its QR code opened here; with `web.host: lan` the loopback page needs the token
1670
+ too: open the `Local:` link chi web prints). A phone gets the targets that need no editor (an
1666
1671
  answer's ref shows its row in the turn; see the refs in [sessions.md](sessions.md)); a row's title stays plain
1667
1672
  text there. An SSH tunnel or a local reverse proxy (`tailscale serve`) looks like loopback unless `web.public_url`
1668
1673
  is set (then a request through it is never this machine's viewer), so without it a remote viewer behind one gets an
@@ -2071,7 +2076,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2071
2076
  | `execute.on_timeout` | `task` | yes | What happens to a command still running at that limit: `task` keeps it running as a background task (the result names its `task_id`; `task_wait` waits for it, `task_stop` stops it), `kill` stops it (`Error: command timed out after …s`). The `!cmd` bang always kills. See [Background tasks](internals/background-tasks.md). |
2072
2077
  | `execute.description` | `true` | | `execute` offers the model an optional `description` (a few words on what the command does), shown as the tool row's title in `chi web` and in place of the command in the TUI's tool line. `false`: the parameter isn't declared, and rows show the command's first step. |
2073
2078
  | `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
2074
- | `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token (`chi web --new-token` replaces it). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
2079
+ | `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token, asked of this machine's 127.0.0.1 too (its `Local:` link carries a one-time code; `chi web --new-token` replaces the token). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
2075
2080
  | `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
2076
2081
  | `web.view` | `stage` | yes | How `chi web` draws a turn: `stage` (the running turn pinned above the composer) or `turn` (one block per turn in the history). See [CLI](cli.md#web-views). |
2077
2082
  | `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |