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