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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91648ee9b8cf9906ded9d6f865f424b10148222e8007516d8d74718a5a4a9c41
4
- data.tar.gz: 0ca9cd428118fd52e4c2dc3f1b5e1750d6d7f4bf41ca0d039c8769660a2db2e9
3
+ metadata.gz: dcca1391e17e266cf83e1c56dec7ab52d21a1928a4c5190d2bf594884cca936f
4
+ data.tar.gz: fc59a582d279b19352c3bda61bb5d2fc3796ae0e079e52b3dcb26dc85988ea33
5
5
  SHA512:
6
- metadata.gz: 1fa243881749c0b856bcb0c40f42937a253f33b0a0904d6145950cbf81c52262bfd3bdcf883389041f06383d7796150d7ee74a74bb9b8a8118d90ac3b4807a1c
7
- data.tar.gz: dc1b3dafdca94da6af0338e1a76b3df6e914847514f6eeb9e61359de941cc789955e5a4b04f6e04417402fcdc221d460940051dcde3101737694cb080b038e1a
6
+ metadata.gz: b5cc1d80ca1b0d64f809360e111bdae2778772c5ca3c8cae7d6f339f650a55fb65b796ccc5ca3038b25c637223568a7facbc90a351ac5df77bee25c24fac3e59
7
+ data.tar.gz: 4e149d518537a529c69494f14b0d07435c6e3327d27ae34a42c0e30504c74fee543282c4023fbd44587a301f189c24bd7d3aebdff40235784bd45f95cfd63217
data/CHANGELOG.md CHANGED
@@ -7,6 +7,80 @@ 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.1] - 2026-10-29
11
+
12
+ ### Fixed
13
+
14
+ - Sessions started from the desktop helper find the tools your terminal finds (mise, Homebrew, `codex`):
15
+ `chi desktop install` writes your shell's PATH into the helper's launch file, `chi update` adds it to a
16
+ launch file that has none (it never changes a baked one; `chi desktop install --force` does), and
17
+ `chi desktop status` shows it. Run `chi update` once from your terminal, then restart the sessions the
18
+ panel started.
19
+ - `chi update` no longer repoints the desktop helper at the Ruby or chi it runs from, nor refreshes the launch file
20
+ from an older chi: only `chi desktop install --force` and `chi desktop upgrade` pick the current shell's Ruby,
21
+ chi and PATH. When the helper's Ruby or chi is gone, `chi update` says so and names that command.
22
+ - An older chi that saves a session a newer chi wrote (waking it from a project whose Ruby has an older chi, say)
23
+ keeps the session's fields it doesn't know instead of dropping them, and the session file names the chi that saved
24
+ it last (`written_by`; a worker woken on a file a newer chi saved logs `newer_writer`). This works from this release
25
+ on: chi 0.130.0 and older still drop them, so update the chi in every Ruby you run it from.
26
+ - `execute` (and a user's `!cmd`) no longer hands a project's commands chi's own Bundler setup: run under
27
+ `bundle exec`, chi passed on `RUBYOPT=-rbundler/setup` and its `BUNDLE_GEMFILE`, so `ruby -e` or `bundle exec`
28
+ in another project loaded chi's Gemfile. It now scrubs them the way `task_create` always has.
29
+
30
+ ### Security
31
+
32
+ - On the LAN (`web.host: lan`), the cookie chi web sets for this machine's 127.0.0.1 no longer holds the access
33
+ token: a server on another local port gets every 127.0.0.1 cookie, so a dev server that logs headers caught the
34
+ token. It now holds a value that works only as that cookie on this machine and ends with `chi web --new-token`.
35
+ An open 127.0.0.1 tab lands on the 401 page once: open the `Local:` link. If you ran a participant's pair turns
36
+ with dev servers on 0.130.0, run `chi web --new-token`. The LAN address's cookie (your phone's) still holds the
37
+ token.
38
+
39
+ Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
40
+
41
+ ## [0.130.0] - 2026-10-29
42
+
43
+ ### Changed
44
+
45
+ - A message you send while the model is blocked in `task_wait` ends the wait within half a second instead of
46
+ waiting for the task: the task keeps running, the result says `wait_result: message_waiting`, and the model reads
47
+ your message at once and decides whether to wait again. A queued message, a plugin's steer and a delegate child's
48
+ report don't end it. A worker started before the upgrade keeps the old behaviour until it is restarted.
49
+
50
+ - `chi web --web-host lan` prints a `Local:` link for this machine with an owner's one-time code (10 minutes, once),
51
+ and `--open` opens it; a second `chi web` prints a new one. `chi remote add` without a token refuses a chi web that
52
+ asks for one (through an SSH tunnel to its loopback) instead of saving a remote that would 401.
53
+
54
+ ### Security
55
+
56
+ - 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
57
+ loopback request was the owner, so a participant's pair turn could `curl` it (by a decimal IP or a DNS alias past
58
+ the pair boundary's text check) to mint grants, queue owner turns and read every session. Only chi's token-less
59
+ `GET /api/info` probe (its version, pid, features, LAN address and start folder) is answered without it. A chi web
60
+ 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
61
+ the 401 page: open the `Local:` link chi web prints (its one-time code sets the cookie; `chi web --open` opens
62
+ it), the LAN link, or paste the token. A browser left with an ended participant cookie gets the ended page there
63
+ instead of becoming the owner again.
64
+ - A participant's pair turn can no longer open a URL or a file in the owner's apps (`open`, `xdg-open`, a browser,
65
+ `osascript`'s `open location`) or listen on a port (`nc -l`, `socat …LISTEN`, `python -m http.server`, `php -S`, a
66
+ static server): a page opened at a port it listens on would catch the owner's chi web cookie.
67
+ - System 1 no longer auto-allows a `write`/`edit` anywhere in chi's state dir (`state/sessions/x.json`), as it already
68
+ refused a command naming one: a file tool's paths are judged by the same dirs as a command's words.
69
+ - On a disk that folds case (macOS) a write to `<state dir>/CONTEXT/…` before the attached context dir exists is denied
70
+ as one to it (`chi-context`), and a command naming it touches chi: the protected-path checks compared the typed case.
71
+ - `git -C $XDG_CONFIG_HOME …` (an XDG dir or `$TMPDIR`) counts as the folder it names for `git: outside_repo` and the
72
+ delegate child's boundary, as a `cd` there already did; it was an unknown folder that never asked.
73
+ - A guardrail rule's `path:` glob also matches the path as it resolves: `write hl/pre-commit` with `hl` a symlink to
74
+ `.git/hooks` is denied by `git-hooks-write` (a relative glob is read from the repo root as it resolves, `/tmp` being
75
+ `/private/tmp` on macOS). Rules only ask or deny, so this only adds votes.
76
+ - A participant's pair turn can no longer glob or `$var` its way into a secret through an XDG dir: `ls
77
+ $XDG_CONFIG_HOME/samago*` is denied like `ls ~/.config/samago*`. After `$TMPDIR` only what lands in a denied folder
78
+ counts, so `ls $TMPDIR/*` stays allowed when chi's state dir is in tmp (isolated test runs).
79
+ - A `hooks.hooks_dir` configured outside chi's config dir is one of chi's dirs for participants too: neither a review
80
+ nor a pair turn reads it (the owner's own checks already counted it).
81
+ - `outside_repo` and `rm: outside_tmp` compare a path with an existing root in any case on a disk that folds case,
82
+ as the protected-path checks do; a session root that doesn't exist yet keeps the case it was typed in.
83
+
10
84
  ## [0.129.0] - 2026-10-28
11
85
 
12
86
  ### Changed
@@ -4890,7 +4964,9 @@ and long-lived sessions.
4890
4964
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4891
4965
  hotkey panel that send selected text or the clipboard to your sessions.
4892
4966
 
4893
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.129.0...HEAD
4967
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.130.1...HEAD
4968
+ [0.130.1]: https://github.com/dm1try/samagotchi/compare/v0.130.0...v0.130.1
4969
+ [0.130.0]: https://github.com/dm1try/samagotchi/compare/v0.129.0...v0.130.0
4894
4970
  [0.129.0]: https://github.com/dm1try/samagotchi/compare/v0.128.0...v0.129.0
4895
4971
  [0.128.0]: https://github.com/dm1try/samagotchi/compare/v0.127.0...v0.128.0
4896
4972
  [0.127.0]: https://github.com/dm1try/samagotchi/compare/v0.126.0...v0.127.0
data/docs/architecture.md CHANGED
@@ -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, on loopback a value derived from the token (`Token.loopback_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
data/docs/cli.md CHANGED
@@ -290,8 +290,12 @@ done
290
290
  one that failed before; one you uninstalled stays out. See [Bundle
291
291
  profiles](memory.md#bundle-profiles-core-and-dev).
292
292
  - **The desktop helper** (macOS) is rebuilt and restarted only when its Swift
293
- sources changed (or the Ruby it runs moved); otherwise only its launch file
294
- is refreshed. See [Desktop helper](desktop.md).
293
+ sources changed; otherwise only its launch file is refreshed (a newer chi or
294
+ other terminal settings). It keeps the Ruby, chi and PATH the helper runs (a
295
+ PATH is filled in only when the file has none): `chi desktop install --force`
296
+ and `chi desktop upgrade` bake this shell's. A helper whose Ruby or chi is gone
297
+ is reported as failed, naming `chi desktop install --force`. See
298
+ [Desktop helper](desktop.md).
295
299
  - **Running processes** are reported, never stopped. A live worker keeps its
296
300
  chi until it is restarted: `chi sessions restart ID` (one from before
297
301
  restarts: `chi sessions stop ID`, and the next prompt starts it on the new
@@ -445,7 +449,7 @@ of UIs at once: the Web UI and attached terminals (`chi`, `--resume`,
445
449
  `--attach`). They all see the same turns as they happen, and any of them can send
446
450
  a prompt, also while a turn runs (it merges into that turn as steering: the model
447
451
  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:
452
+ 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
453
  once the model has streamed only thinking for `steer.cut_after` seconds (per host/model: `steer_cut_after`; default
450
454
  20), now or while your message waits, that generation is cut and the step starts again with your message, marked
451
455
  `↪ cut in for your message` (see
@@ -1249,6 +1253,7 @@ chi web --web-host lan
1249
1253
 
1250
1254
  ```
1251
1255
  Chi Web on http://127.0.0.1:4567/?dir=/Users/me/projects/app (public: …)
1256
+ Local: http://127.0.0.1:4567/?dir=/Users/me/projects/app#code=chc_… ← opens chi web as you once, within 10 minutes (…)
1252
1257
  LAN: http://192.168.1.55:4567/?token=… ← anyone with this link can run commands as you
1253
1258
  Plain http: the link and your traffic can be read by anyone on this Wi-Fi.
1254
1259
  <the link's QR code>
@@ -1264,13 +1269,25 @@ web app on iOS has cookies of its own: if it opens on "needs chi web's
1264
1269
  access token", paste the token there (the part of the link after
1265
1270
  `token=`), or open the link once in it.
1266
1271
 
1267
- - Every request from another machine needs the token (the cookie, or
1268
- `Authorization: Bearer <token>` for curl); this Mac (127.0.0.1) needs none.
1269
- Without it the page says how to get in and the API answers 401.
1272
+ - Every request needs the token (the cookie, or `Authorization: Bearer
1273
+ <token>` for curl), this Mac's on 127.0.0.1 too: a participant's pair turn
1274
+ runs commands here. Without it the page says how to get in (with a box to
1275
+ paste the token) and the API answers 401. On this Mac open the `Local:`
1276
+ link (`chi web --open` opens it): its one-time code (10 minutes, once) sets
1277
+ the cookie for 127.0.0.1 without the token in the address; the LAN link
1278
+ works here too. The cookie for 127.0.0.1 holds a value derived from the
1279
+ token, not the token: the browser sends it to a server on any other local
1280
+ port too (a dev server that logs headers), and that server can't use it
1281
+ from the network (unless something forwards it to this machine's
1282
+ loopback), as Bearer, or as the token; `chi web --new-token` ends it. The LAN address's cookie (your phone's, or this Mac's when it opens
1283
+ the LAN link) is the token itself. chi's own probe (`GET /api/info` with no token or cookie)
1284
+ gets a few fields (version, pid, features, the LAN address, the start
1285
+ folder), nothing of the sessions.
1270
1286
  - The token lives in `$XDG_STATE_HOME/samagotchi/web-token` (0600).
1271
1287
  `chi web --new-token` replaces it: a running chi web takes the new one at
1272
1288
  once, and every phone has to scan the new QR code.
1273
- - A second `chi web` prints the LAN link and QR code again. A plain
1289
+ - A second `chi web` prints the LAN link and QR code again, and a new
1290
+ `Local:` link (`--open` opens it). A plain
1274
1291
  `chi web --web-host lan` while a chi web without LAN access runs asks you
1275
1292
  to stop that one first. `chi self` says whether chi web runs on the LAN.
1276
1293
  - `lan` picks the first private address (10.x, 172.16–31.x, 192.168.x) on an
@@ -1414,10 +1431,11 @@ chi --remote work sessions list
1414
1431
  - Forward only chi web's port, never a worker's: a session's Bridge answers
1415
1432
  only chi's own clients on its machine (they read its secret from the
1416
1433
  session's folder), so a forwarded worker port is of no use.
1417
- - Without `web.public_url` there, a request that arrives through the tunnel
1418
- looks local and needs no token, so any process here reaching the forwarded
1419
- port acts as you there. With it set, every request needs the token, which
1420
- `chi --remote` sends as Bearer.
1434
+ - A chi web there with `web.host: lan` or `web.public_url` asks every
1435
+ request for the token, one through the tunnel too (`chi remote add` without
1436
+ `--token-file` says so), and `chi --remote` sends it as Bearer. One on
1437
+ 127.0.0.1 only takes a request through the tunnel as local, with no token,
1438
+ so any process here reaching the forwarded port acts as you there.
1421
1439
  - `-L` binds 127.0.0.1 unless you write `-g` or `0.0.0.0:`: don't.
1422
1440
  - Attaching works through the tunnel too (`chi --remote work --attach ab12`),
1423
1441
  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). |
data/docs/desktop.md CHANGED
@@ -194,7 +194,8 @@ grant:
194
194
 
195
195
  `tmux.agents` picks the programs listed (kitty.agents' default; `"*"` lists every pane), `tmux.binary` the tmux
196
196
  to run (default: the `tmux` on your shell's PATH when the launch file is written, e.g. `/opt/homebrew/bin/tmux`;
197
- the helper itself has no PATH). See [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
197
+ the helper itself keeps macOS's default PATH, and tmux is resolved when the file is written). See
198
+ [Configuration](configuration.md#desktop-helper-agents-in-kitty-and-tmux).
198
199
  2. Run `chi update` (or `chi desktop upgrade`), and again after every edit of `tmux:`. With no tmux found they say
199
200
  "tmux.enabled but no tmux found on PATH: set tmux.binary", and the panel "tmux not found…".
200
201
 
@@ -256,9 +257,16 @@ don't break it. From a checkout it runs **that** checkout's `bin/chi`; installin
256
257
  warning, because the helper stops working once that worktree is removed. After switching between a checkout and a
257
258
  gem install, run `chi desktop upgrade` from the one you now use.
258
259
 
260
+ The helper runs the Ruby and chi of **the shell you installed from**, and only `chi desktop install --force` and
261
+ `chi desktop upgrade` change that: they pick the Ruby and chi of the shell you run them in. `chi update` never
262
+ repoints it, so running it from a project with another Ruby (or an older chi) leaves the helper where it was. If
263
+ the Ruby or chi the helper runs is gone (a Ruby upgrade, a removed worktree), `chi update` fails the helper's row
264
+ with "chi not found at …: run chi desktop install --force from the shell whose chi it should run".
265
+
259
266
  `chi update` keeps it current: it rebuilds and restarts the helper only when its Swift sources changed since the
260
- build (`launch.json` records their digest) or the Ruby it runs moved. A new chi that left the sources alone only
261
- rewrites `launch.json`, which the app reads at each send, so the app keeps its older version number and that's fine.
267
+ build (`launch.json` records their digest), keeping the Ruby, chi and PATH it runs. A newer chi that left the
268
+ sources alone only rewrites `launch.json` (its version and settings), which the app reads at each send, so the app
269
+ keeps its older version number and that's fine. An older chi than the one that wrote the file changes nothing.
262
270
 
263
271
  ## Commands
264
272
 
@@ -267,7 +275,7 @@ rewrites `launch.json`, which the app reads at each send, so the app keeps its o
267
275
  | `chi desktop install [--force] [--login]` | builds, installs and starts it; `--force` replaces an existing copy |
268
276
  | `chi desktop upgrade` | rebuilds it for this chi and restarts it, keeping the login setting (always; `chi update` does it only when needed) |
269
277
  | `chi desktop uninstall` | quits it and removes the app, its login item, launch file and settings |
270
- | `chi desktop status` | version against chi's, how it runs chi, state dirs, Service, hotkey, process, login item, host keys |
278
+ | `chi desktop status` | version against chi's, how it runs chi, state dirs, PATH, Service, hotkey, process, login item, host keys |
271
279
 
272
280
  `chi self` has a `desktop` line: `0.1.x (matches)`, `0.1.w (up to date for chi 0.1.x)` (an older build whose sources
273
281
  haven't changed), `0.1.w (chi is 0.1.x: chi update)` (a rebuild is due) or `not installed`. `chi desktop status`
@@ -278,17 +286,24 @@ says the same.
278
286
  Apps started by macOS get a bare environment: no shell rc files, so no rbenv/chruby/mise/asdf and none of your
279
287
  exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.json` with:
280
288
 
281
- - the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`);
289
+ - the absolute path of the Ruby running chi and of chi itself (the gem's wrapper, or a checkout's `bin/chi`), as of
290
+ the last `chi desktop install` or `upgrade` (`chi update` keeps them);
282
291
  - `LANG=en_US.UTF-8`;
283
292
  - only these variables, and only when they are set: `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `GEM_HOME`, `GEM_PATH`,
284
293
  `RUBYLIB` (a relative `XDG_*` dir made absolute from the install's current folder: the helper starts in `/`).
285
294
  No tokens and no `SAMAGOTCHI_*` settings go in;
295
+ - `PATH`: the shell's PATH when the file is written, with only absolute folders, in order, each once. The helper's
296
+ chi, the sessions it starts and their `execute` commands can find the tools your terminal finds (mise, Homebrew,
297
+ `codex`, `node`);
286
298
  - the settings the panel needs from config.yml, as chi read them then: the `kitty:` section (when
287
299
  `kitty.listen_on` is set), the `tmux:` section (when `tmux.enabled`: the tmux binary and the socket dir resolved
288
300
  then, and the agents) and `broadcast_timeout` (`broadcast.triage_deadline` + 10 s).
289
301
 
290
- The file freezes the install shell's values. If you install with a temporary `XDG_STATE_HOME`, the helper keeps
291
- using it. `status` prints the baked dirs.
302
+ The file freezes the install shell's values, including PATH. If you install with a temporary `XDG_STATE_HOME`, the
303
+ helper keeps using it. `chi update` fills in a PATH only when the file has none (as in a launch file written before
304
+ this change); it never changes an existing baked PATH. To change it (for example, after a mise upgrade changes the
305
+ versioned folders on PATH), run `chi desktop install --force` or `chi desktop upgrade` from the shell whose PATH
306
+ you want. `status` prints the baked state dirs and PATH.
292
307
 
293
308
  **Host API keys** don't go in the launch file either: with the variable a host's `api_key_env:` names unset, chi
294
309
  reads the key from the login Keychain (service `chi`, account the variable's name; see
@@ -331,8 +346,12 @@ broadcast` says the note may be partly delivered.
331
346
 
332
347
  ## Troubleshooting
333
348
 
334
- - **"chi not found at …, run `chi desktop upgrade`"**: the Ruby or checkout in `launch.json` moved (a Ruby upgrade,
335
- a removed worktree). Run `chi desktop upgrade` (or `chi update`) from the chi you use now.
349
+ - **"chi not found at …"**: the Ruby or checkout in `launch.json` moved (a Ruby upgrade, a removed worktree).
350
+ `chi update` won't repoint it: run `chi desktop install --force` (or `chi desktop upgrade`) from the shell whose
351
+ chi the helper should run.
352
+ - **A session started from the panel can't find a tool (`codex`, `node`):** `chi desktop status` shows the PATH it
353
+ gets. With none, run `chi update`; to change it, run `chi desktop install --force` from your terminal. Then
354
+ restart that session (`chi sessions restart ID`).
336
355
  - **No "Send to chi" in the Services menu:** check `chi desktop status` (service). Try
337
356
  `/System/Library/CoreServices/pbs -update`, start the app again, or log out and back in. It must be ticked in
338
357
  System Settings → Keyboard → Keyboard Shortcuts… → Services → Text.
data/docs/guardrails.md CHANGED
@@ -346,7 +346,8 @@ participant's turn may call only the `review` kit's tools:
346
346
 
347
347
  - `read`, under the session's project root (the work tree top of its folder)
348
348
  and never a secret: chi's state and config dirs (sessions, the grants, the
349
- web token, memories and their `.bundles`, `config.yml`, hooks), `~/.ssh`,
349
+ web token, memories and their `.bundles`, `config.yml`, hooks, and a
350
+ `hooks.hooks_dir` configured outside the config dir), `~/.ssh`,
350
351
  `~/.gnupg`, `~/.aws`, and the project's `.env*`, `.netrc`, `*.pem`, `*.key`,
351
352
  `*.p12`, anything named `credentials` (`.git-credentials` too), `.npmrc`,
352
353
  `.pypirc`, ssh keys (`id_rsa`, `id_ed25519`, …), `*.tfstate`,
@@ -408,7 +409,11 @@ message:
408
409
  each word resolved as `touches: chi_dirs` resolves it; a glob that matches a
409
410
  secret or whose fixed part leads into a denied folder (`~/.ss?/id_*`,
410
411
  `~/.config/*/config.yml`, `.en?`), a `$var` word whose fixed part does
411
- (`~/.ss${d}h`), and a text match for the rest. A prefix that comes through a
412
+ (`~/.ss${d}h`), with a leading `~`, `$HOME`, XDG dir (its default when
413
+ unset) or `$TMPDIR` expanded (`ls $XDG_CONFIG_HOME/samago*`); after `$TMPDIR`
414
+ the fixed part counts only inside a denied folder, not as the start of one's
415
+ name (an isolated run's state dir is in tmp: `ls $TMPDIR/*` isn't denied);
416
+ and a text match for the rest. A prefix that comes through a
412
417
  variable (`x=~/.s; cat ${x}sh/config`) isn't followed: such a command isn't
413
418
  denied, it asks (its `$var` word and the assignment's path place it outside
414
419
  the project);
@@ -441,10 +446,17 @@ message:
441
446
  (`localhost` and its subdomains, `127.x`, `0.0.0.0`, `[::1]`, `::ffff:127.x`,
442
447
  `0:PORT`) or this machine's host name together with chi web's port (the one
443
448
  the running chi web recorded in `web.json`, config's `web.port`, 4567), by
444
- `curl`, `wget`, `nc`, `/dev/tcp`, a script's URL or anything else: a loopback
445
- request without a token is the owner on a LAN chi web
446
- (`PairBoundary::CHI_WEB`). A Bridge answers only with its secret, which sits
447
- in chi's state dir;
449
+ `curl`, `wget`, `nc`, `/dev/tcp`, a script's URL or anything else
450
+ (`PairBoundary::CHI_WEB`): defense in depth, as a LAN chi web asks loopback
451
+ for the token too, and a loopback-only one still takes it as the owner. A
452
+ Bridge answers only with its secret, which sits in chi's state dir;
453
+ - opening a URL or a file in the owner's apps (`open`, `xdg-open`, `gio open`,
454
+ a browser, `osascript`'s `open location`, a script's `webbrowser`) and
455
+ listening on a port (`nc -l`, `socat …LISTEN`, `python -m http.server`,
456
+ `ruby -run -e httpd`, `php -S`, a static server, a script's `listen(`/
457
+ `HTTPServer`/`createServer`): the owner's browser sends its cookies (chi
458
+ web's on 127.0.0.1 or the LAN address, kept for every port) to the page
459
+ it opens (`PairBoundary::OPENER`, `PairBoundary::LISTENER`);
448
460
  - a program whose name the shell expands: a `$var`, a `$(…)` or a glob in a
449
461
  command's name (`bin/ch? …`, `e=ch; ${e}i …`, `$CMD x`), whatever it would
450
462
  turn out to be (`PairBoundary::VERB`);
@@ -705,8 +717,14 @@ the toolkit removes stays refused before the gate.
705
717
  ```
706
718
 
707
719
  All the fields a rule gives must match. Absolute and `**/` globs match the
708
- resolved path; other globs match the path relative to the repo root. Paths
709
- resolve the way the tools resolve them (against the cwd; `~` expanded).
720
+ absolute path; other globs match the path relative to the repo root. A glob
721
+ matches the path as the call gives it (against the cwd, `~` expanded) and as it
722
+ resolves (symlinks, `..` the kernel's way: see How paths resolve), a relative
723
+ glob then from the repo root as it resolves (`/tmp` is `/private/tmp` on
724
+ macOS): `write hl/pre-commit` with `hl` a link to `.git/hooks` matches
725
+ `**/.git/hooks/**`. Rules only ask or deny, so the resolved path only ever adds
726
+ a vote; were a verdict ever to let a call through, a symlink into its glob
727
+ would not count.
710
728
 
711
729
  `outside_repo` is measured from the session's repo: the git work tree its
712
730
  folder is in (a linked worktree is its own), or the folder itself outside a
@@ -732,7 +750,7 @@ name too) is always outside, in the repo or a tmp folder alike
732
750
 
733
751
  Every path check (protected paths, `outside_repo`, `touches`, `rm`, `memory:
734
752
  prompt`, the child, review and pair boundaries, System 1's own-files test)
735
- resolves paths with `Guardrails::ProtectedPaths`:
753
+ resolves paths with `Guardrails::PathResolve` (the map below):
736
754
 
737
755
  - `..` is taken the kernel's way (`.expand`): after a symlinked folder it
738
756
  climbs out of the link's target, so `repo/link/../x` with `link` pointing
@@ -756,7 +774,29 @@ resolved from one isn't resolved), and judged both ways as above; past eight
756
774
  folders, or after `cd -`, the folder is unknown. After `cd link` a `../x`
757
775
  climbs out of the link's target as well. git's `-C`, `--git-dir` and
758
776
  `--work-tree` (and `GIT_DIR`/`GIT_WORK_TREE`) are counted both ways, from each
759
- folder the command may be in.
777
+ folder the command may be in, with a leading `~`, `$HOME`, XDG dir or `$TMPDIR`
778
+ expanded as a `cd` expands it (`PathResolve.expand_word`).
779
+
780
+ On a disk that folds case (macOS), a path is in a protected folder however its
781
+ letters are cased, also where the rest doesn't exist yet (`PathResolve.under?`):
782
+ a write to `<state dir>/CONTEXT/x` is a write to the attached context dir once it
783
+ exists.
784
+
785
+ For whoever changes these checks, one shared helper per primitive, each surface
786
+ keeping its own predicate (`spec/guardrails/path_consistency_spec.rb` pins what
787
+ every surface answers for the same paths):
788
+
789
+ | Primitive | Where | Used by |
790
+ |---|---|---|
791
+ | expand (`..` the kernel's way), real, lexical_real, reals | `PathResolve` (`ProtectedPaths` keeps the old names) | every surface |
792
+ | in a dir: `under?` (case-folded on a folding disk; `fold: false` exact), `strictly_under?`, `within?` (root resolved too) | `PathResolve` | `under?`: ShellPaths, ReviewPaths, PairCommand, ChildBoundary, SelfProtect's words; `Outside.within?` and RmTargets' `strictly_under?` fold only when the dir exists (a missing session root keeps the case it was typed in); `within?`: ProtectedPaths, ScratchWrites, ParentApprovals, SelfProtect's paths |
793
+ | tmp dirs and "not one the session is in" | `PathResolve.tmp_roots(session:)` (exact case) | Outside (`Outside.tmp_roots` is the list it and Targets read), RmTargets; `touches_chi?` takes all of them |
794
+ | a shell word's `~`, `$HOME`, XDG dirs, `$TMPDIR` | `PathResolve.expand_word` | ShellPaths::Walk, ShellGitDirs; PairCommand has its own `~`/`$HOME` (`expand_tilde`) |
795
+ | chi's dirs | `ChiDirs.protected` / `.touches` / `.self_protect` / `.review` | ParentApprovals, Targets, SelfProtect, ReviewPaths (`ProtectedPaths` and `ScratchWrites` get theirs from the wiring) |
796
+
797
+ Left apart on purpose: `Targets.prompt_memory_path?` compares in any case on
798
+ every disk, `PairCommand#leads_into_denied?` matches a partial name (`~/.ss`),
799
+ and each surface resolves its root its own way.
760
800
 
761
801
  `git: outside_repo` (shell tools only) reads the command for git subcommands
762
802
  that change a checkout: `commit`, `add`, `reset`, `checkout`, `switch`,
@@ -766,7 +806,8 @@ that change a checkout: `commit`, `add`, `reset`, `checkout`, `switch`,
766
806
  and the call's `cwd:`, and matches when one of them runs outside the session's
767
807
  repo (the same `outside_repo` as above). `cd /other && git status`, `git -C
768
808
  /other log` and `cd /other && bundle exec rspec` don't match. A folder the text
769
- doesn't tell (`cd $REPO`, `cd "$(…)"`, `cd -`) doesn't either; see Limits.
809
+ doesn't tell (`cd $REPO`, `cd "$(…)"`, `cd -`; a `$XDG_CONFIG_HOME` or
810
+ `$TMPDIR` is told) doesn't either; see Limits.
770
811
 
771
812
  To switch off single rules (a bundle's, say) without editing its files, list
772
813
  them under `disable:`. A plain id switches off every rule with that id; `bundle:id`
@@ -989,12 +1030,15 @@ gate below passes, in this order:
989
1030
  are never auto-allowed, even when listed
990
1031
  (`SystemOne::SelfProtect::NEVER_AUTO`);
991
1032
  4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`), by
992
- the same test that makes an ask one only the user may answer
993
- (`ParentApprovals.chi_files?`): no edit or write whose paths are in chi's
1033
+ its own set of chi's dirs (`Guardrails::ChiDirs.self_protect`: the dirs whose
1034
+ asks only the user may answer, `ParentApprovals.chi_files?`, plus the whole
1035
+ state dir), wider than that test: no edit or write whose paths are in chi's
994
1036
  config dir (config.yml), the hooks dir, the installed bundles dir, the
995
- guardrails store (where `approvals.json` and `judgments.jsonl` live) or the
996
- attached context dir, and no `execute` or `task_create` command naming any
997
- of them (or chi's state dir). A command is checked twice: by its text
1037
+ guardrails store (where `approvals.json` and `judgments.jsonl` live), the
1038
+ attached context dir or anywhere else in chi's state dir (the sessions), and
1039
+ no `execute` or `task_create` command naming any of them: a write's paths and
1040
+ a `cat` of them answer alike (`SelfProtect.chi_dirs`). `chi_files?` itself
1041
+ still applies on top (its text match, the command spelling a dir). A command is checked twice: by its text
998
1042
  (`ParentApprovals::CHI_TEXT`: `$XDG_CONFIG_HOME/samagotchi/config.yml`,
999
1043
  `~/.config/samagotchi/…`, a `cd ~/.config` then `samagotchi/config.yml`),
1000
1044
  and by its words resolved as `shell-touches-chi` resolves them
@@ -30,7 +30,19 @@ Behavior:
30
30
  - Which tasks are a session's own (a cancelled turn's note, a worker's restart hold, the finish wake below, the web's stop-task button): the ids its `task_create` results and its handed-over executes name (`TaskRuntime.created_ids_in`, `created_id`). An execute's id is read only after its first line (`Execute::HANDED_OVER`, through `TaskRuntime.handed_over_id`; the web has a JS twin in `task_stop.js`), not from a `task_id:` line inside a run, which the command can print. (A command that prints the `---` separator chi joins a turn's results with, then a hand-over line, still fakes one: a known gap.) Either result may start with the `ran as: …` line of a gate-corrected call.
31
31
  - `task_get` returns metadata and `output_path`; use `read` for output contents.
32
32
  - `task_wait` accepts `timeout`, `tail_lines` (maximum 100), and `done_pattern` (a regular expression string).
33
+ - `task_wait`'s results: a finished task's status and tail (no `wait_result`), or one still running with
34
+ `wait_result:` `pattern_matched`, `timeout`, `canceled` (the user's Stop of the turn) or `message_waiting`. One poll
35
+ (0.5 s) checks in that order: finished, pattern matched, Stop, a message waiting, the deadline. `message_waiting`
36
+ ends the wait when a person's message waits for the turn's next step (`Engine#input_waiting?`, run_turn's
37
+ `input_waiting:` probe through the kernel's `PeerView`): in a worker an input file that the next boundary merges
38
+ from the user, `chi send` or a delegating parent (`SessionInbox.steer_waiting?`; never a queued, image or
39
+ participant's one, never a plugin's steer or a child's report, never during a participant's turn), in the REPL
40
+ (`--no-shared`) a line in its steering queue. The probe only peeks: the drain still merges the message at the
41
+ next boundary. The result says `note: a message from the user is waiting; the task is STILL RUNNING.`, how to wait
42
+ again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
43
+ (below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe.
33
44
  - `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
45
+ - `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB`, `BUNDLE_GEMFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION` are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile when chi runs under `bundle exec`. `task_create` refuses those keys in the model's `env`; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
34
46
  - `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
35
47
  - They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
36
48
 
@@ -16,13 +16,14 @@ Engine#run_turn → backend.complete(...) → KernelLoop#run (native) or ChatLoo
16
16
 
17
17
  ```
18
18
  Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:, max_tool_output_chars:,
19
- pending_input:, continue:, origin:, images:, id:, resumed:, resume_from:)
19
+ pending_input:, continue:, origin:, images:, id:, resumed:, resume_from:, input_waiting:)
20
20
  │
21
- ├── begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id:)
21
+ ├── begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id:, input_waiting:)
22
22
  │ # (then, on a continue, turn.resumed = resumed (a step-limit Continue) and
23
23
  │ # turn.resume_from = resume_from (its run's start; nil: it starts cold))
24
24
  │ # @session = session, session.status = RUNNING, used memories absorbed,
25
- │ # TurnState#begin!(controller:, sink:) (a new CancellationController when none is given),
25
+ │ # TurnState#begin!(controller:, sink:, input_waiting:) (a new CancellationController when none
26
+ │ # is given; input_waiting: the caller's peek, Engine#input_waiting?, which task_wait polls),
26
27
  │ # a recap in flight invalidated, GuardrailWiring#begin_turn(origin)
27
28
  │ # Returns a Turn {session, prompt (nil on a continue), continue, on_event, controller,
28
29
  │ # origin, started_at, id (the caller's id:, else a new uuid), resumed, resume_from,
@@ -122,11 +122,28 @@ participant** and **one role**, until it expires or is revoked.
122
122
  the detail (only the requester's own identity). Unproxied, every request needs a token too (no "loopback is the
123
123
  owner"): rule 3 doesn't apply.
124
124
  1. The owner's token (Bearer, then the cookie; `?token=` on the page's trade): the **owner**. Checked first, so the
125
- owner's requests never touch the grants store (measured: no change to an owner route's time).
125
+ owner's requests never touch the grants store (measured: no change to an owner route's time). On a LAN chi web
126
+ (no `web.public_url`) a loopback peer's owner cookie is `Token.loopback_cookie` instead (`chl_`, an HMAC of a
127
+ label keyed by the token, `App#loopback_cookie_peer?` / `#loopback_cookie`): it stands for the token only as that
128
+ cookie from a loopback peer, never as Bearer, a trade or from the LAN address, and `chi web --new-token` ends it.
129
+ The token itself as a loopback cookie (set by 0.130.0) is refused, and its 401 clears it (`#refused_token`). The
130
+ token as Bearer on loopback is the owner as before (SSH tunnels, `chi --remote` over `ssh -L`, curl, the e2e
131
+ harness).
126
132
  2. A `chp_` token (`Grants.token?`) whose grant is active: a **participant** (`Principal.new(kind: :participant,
127
133
  grant:)`), from any peer: loopback and a chi web not on the LAN too. An ended or unknown one is refused
128
134
  (below), never the owner.
129
- 3. No `chp_` token from a loopback peer, or on a chi web not on the LAN: the **owner** (unchanged).
135
+ 3. No `chp_` token on a chi web on loopback only (no `web.host: lan`, no `web.public_url`): the **owner**. On the LAN
136
+ this machine's loopback needs the owner's token too (`App#token_required?`; a pair participant's shell runs here,
137
+ and a token-less `curl 127.0.0.1` was the owner, finding A1); only `GATE_FREE_PATHS` and chi's token-less
138
+ `GET /api/info` probe (`ProxyGate#probe_info?`: no Authorization, no Cookie, its checked fields and `start_dir`)
139
+ are answered without it. `chi web` prints and opens a `Local:` link for 127.0.0.1 with an owner's one-time code
140
+ (`Server.local_owner_link`), the fragment trade sets the cookie there. `PairCommand` still denies a pair command
141
+ naming chi web on a loopback name (defense in depth), and URL openers and listeners: a page opened in the owner's
142
+ browser at a port the shell listens on would catch the owner's cookie (one on 127.0.0.1 or the LAN address goes
143
+ to every port: on 127.0.0.1 it holds the loopback value of rule 1, usable only as that cookie from a loopback peer with no proxy trace (anything that can reach loopback, such as an `ssh -L` user or code running as the owner, can still replay it); on
144
+ the LAN address it is the token, a known gap, as a cookie there is replayable from any LAN device anyway). A
145
+ server on another local port can also answer `Set-Cookie: chi_token=…` and overwrite the owner's cookie (cookie
146
+ tossing: it logs the owner out, or swaps in a grant's token; visible, nothing leaks; not fixed).
130
147
  4. Anything else: nil. `#token_gate` answers a bound grant's token without its identity **401 `token_bound`**, an
131
148
  ended grant's token **401 `token_revoked`** or **401
132
149
  `token_expired`** (the page says the same words as a sentence: "This review link was revoked by the session's
@@ -135,11 +152,13 @@ participant** and **one role**, until it expires or is revoked.
135
152
 
136
153
  The `?token=` trade (and `POST /api/token`, with a link's one-time `{code}` or the 401 page's pasted `{token}`; with
137
154
  `web.public_url` only through the proxy, as a direct request takes Bearer only) sets the `chi_token` cookie (`__Host-chi_token`, Secure, on a proxied request) to the token that was
138
- presented, the owner's or the participant's
155
+ presented, the owner's or the participant's (the owner's loopback value instead, on a LAN chi web's loopback peer,
156
+ `App#owner_cookie_value`)
139
157
  (one at a time per browser: a participant link opened in the owner's browser replaces the owner's cookie there, and
140
158
  makes that browser the participant, on loopback too). A grant's token is traded on any peer; the owner's only where
141
- the gate asks for it. On a peer the gate doesn't ask, the 401 for a grant's cookie that no longer works clears the
142
- cookie, so that browser is the owner's again at its next request.
159
+ the gate asks for it (on the LAN, this machine's loopback too). On a chi web the gate doesn't ask (loopback only), the
160
+ 401 for a grant's cookie that no longer works clears the cookie, so that browser is the owner's again at its next
161
+ request; on the LAN that browser gets the ended page and opens the owner's link again.
143
162
 
144
163
  Then `#route`, for each matching `ROUTES` row:
145
164
 
@@ -277,7 +296,7 @@ queued, as `--turn T` does.
277
296
  reads of the project ([guardrails](../guardrails.md#a-participants-review-boundary)). In a working session
278
297
  `Guardrails::PairBoundary` takes its seat: the owner's checks and rules decide the rest, as for the owner's turns
279
298
  ([guardrails](../guardrails.md#a-participants-pair-boundary)). Both judge a path every way it resolves
280
- (`ProtectedPaths.reals`: a `..` after a symlinked folder climbs out of its target, as the read opens it;
299
+ (`PathResolve.reals`: a `..` after a symlinked folder climbs out of its target, as the read opens it;
281
300
  [guardrails](../guardrails.md#how-paths-resolve)). Approvals and questions are owner-only: either
282
301
  boundary denies the model's own question, and `/answer` is an owner route. A pair turn's ask reaches the owner's card
283
302
  naming who asks (`approval.author`, `author_name`; `Guardrails::Approval::PARTICIPANT_HEADER`) with Allow once as its
@@ -195,10 +195,11 @@ with a "Machine-local" comment; the rest branch on `@access.remote?` (`SendComma
195
195
 
196
196
  ## Trust model
197
197
 
198
- - **The token is the owner.** In LAN mode (`web.host: lan`) every non-loopback request needs the `Web::Token` access
199
- token, as the `chi_token` cookie or `Authorization: Bearer` (`App#token_gate`). This runs over plain http, which is
200
- fine on a home LAN, the same as the phone page. A loopback chi web without LAN mode needs no token
201
- (`Remotes::Entry#token` is nil, and no header is sent). Behind an auth proxy (`web.public_url`, experimental) every
198
+ - **The token is the owner.** In LAN mode (`web.host: lan`) every request needs the `Web::Token` access token,
199
+ loopback too (but chi's token-less `/api/info` probe, `ProxyGate#probe_info?`), as the `chi_token` cookie or
200
+ `Authorization: Bearer` (`App#token_gate`). This runs over plain http, which is fine on a home LAN, the same as the
201
+ phone page. A loopback chi web without LAN mode needs no token (`Remotes::Entry#token` is nil, and no header is
202
+ sent); `chi remote add` with no token refuses one that answers only the probe (no `cwd`: `RemoteCommand#check`). Behind an auth proxy (`web.public_url`, experimental) every
202
203
  request needs it, loopback too, and a proxied owner also the configured identity header
203
204
  ([configuration.md: Behind an auth proxy](../configuration.md#behind-an-auth-proxy), `Web::ProxyGate`); `chi
204
205
  --remote` through such a proxy isn't supported yet (the CLI can't pass its sign-in, and streams need TLS).