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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +29 -11
- data/docs/configuration.md +7 -2
- data/docs/desktop.md +28 -9
- data/docs/guardrails.md +60 -16
- data/docs/internals/background-tasks.md +12 -0
- data/docs/internals/engine.md +4 -3
- data/docs/internals/participants.md +25 -6
- data/docs/internals/remote-sessions.md +5 -4
- data/docs/sessions.md +5 -3
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos.rb +99 -19
- data/lib/samagotchi/desktop_command.rb +7 -2
- data/lib/samagotchi/engine.rb +20 -4
- data/lib/samagotchi/engine_peers.rb +3 -1
- data/lib/samagotchi/guardrails/chi_dirs.rb +64 -0
- data/lib/samagotchi/guardrails/child_boundary.rb +5 -5
- data/lib/samagotchi/guardrails/outside.rb +22 -20
- data/lib/samagotchi/guardrails/pair_boundary.rb +14 -3
- data/lib/samagotchi/guardrails/pair_command.rb +70 -10
- data/lib/samagotchi/guardrails/parent_approvals.rb +8 -11
- data/lib/samagotchi/guardrails/path_resolve.rb +231 -0
- data/lib/samagotchi/guardrails/protected_paths.rb +17 -116
- data/lib/samagotchi/guardrails/review_paths.rb +13 -11
- data/lib/samagotchi/guardrails/rm_targets.rb +5 -5
- data/lib/samagotchi/guardrails/rules.rb +23 -5
- data/lib/samagotchi/guardrails/scratch_writes.rb +3 -2
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +17 -12
- data/lib/samagotchi/guardrails/shell_paths.rb +18 -29
- data/lib/samagotchi/guardrails/targets.rb +24 -13
- data/lib/samagotchi/remote_command.rb +13 -3
- data/lib/samagotchi/remotes.rb +3 -2
- data/lib/samagotchi/review_start.rb +3 -2
- data/lib/samagotchi/session.rb +49 -4
- data/lib/samagotchi/session_inbox.rb +17 -0
- data/lib/samagotchi/system_one/self_protect.rb +34 -19
- data/lib/samagotchi/terminal_ui.rb +2 -0
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/builtins.rb +10 -1
- data/lib/samagotchi/tools/execute.rb +5 -2
- data/lib/samagotchi/tools/peers.rb +4 -0
- data/lib/samagotchi/tools/task_runtime.rb +4 -2
- data/lib/samagotchi/tools/task_wait.rb +26 -1
- data/lib/samagotchi/turn_state.rb +23 -4
- data/lib/samagotchi/update_command.rb +9 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +69 -25
- data/lib/samagotchi/web/participant_review.rb +9 -2
- data/lib/samagotchi/web/proxy_gate.rb +14 -6
- data/lib/samagotchi/web/server.rb +29 -4
- data/lib/samagotchi/web/token.rb +20 -1
- data/lib/samagotchi/worker.rb +16 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcca1391e17e266cf83e1c56dec7ab52d21a1928a4c5190d2bf594884cca936f
|
|
4
|
+
data.tar.gz: fc59a582d279b19352c3bda61bb5d2fc3796ae0e079e52b3dcb26dc85988ea33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
|
294
|
-
|
|
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
|
|
1268
|
-
|
|
1269
|
-
Without it the page says how to get in
|
|
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
|
|
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
|
-
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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.
|
data/docs/configuration.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
|
261
|
-
rewrites `launch.json
|
|
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
|
|
291
|
-
using it. `
|
|
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
|
|
335
|
-
|
|
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
|
|
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`),
|
|
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
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
709
|
-
|
|
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::
|
|
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
|
|
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
|
-
|
|
993
|
-
|
|
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)
|
|
996
|
-
attached context dir
|
|
997
|
-
|
|
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
|
|
data/docs/internals/engine.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
142
|
-
cookie, so that browser is the owner's again at its next
|
|
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
|
-
(`
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
(`Remotes::Entry#token` is nil, and no header is
|
|
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).
|