samagotchi 0.129.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 +45 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +18 -9
- data/docs/configuration.md +7 -2
- data/docs/guardrails.md +60 -16
- data/docs/internals/background-tasks.md +11 -0
- data/docs/internals/engine.md +4 -3
- data/docs/internals/participants.md +13 -4
- data/docs/internals/remote-sessions.md +5 -4
- data/docs/sessions.md +1 -1
- 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/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 +11 -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_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/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 +23 -18
- data/lib/samagotchi/web/proxy_gate.rb +8 -6
- data/lib/samagotchi/web/server.rb +29 -4
- data/lib/samagotchi/worker.rb +9 -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: 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,49 @@ 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
|
+
|
|
10
53
|
## [0.129.0] - 2026-10-28
|
|
11
54
|
|
|
12
55
|
### Changed
|
|
@@ -4890,7 +4933,8 @@ and long-lived sessions.
|
|
|
4890
4933
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4891
4934
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4892
4935
|
|
|
4893
|
-
[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
|
|
4894
4938
|
[0.129.0]: https://github.com/dm1try/samagotchi/compare/v0.128.0...v0.129.0
|
|
4895
4939
|
[0.128.0]: https://github.com/dm1try/samagotchi/compare/v0.127.0...v0.128.0
|
|
4896
4940
|
[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 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
|
@@ -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
|
|
@@ -1249,6 +1249,7 @@ chi web --web-host lan
|
|
|
1249
1249
|
|
|
1250
1250
|
```
|
|
1251
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 (…)
|
|
1252
1253
|
LAN: http://192.168.1.55:4567/?token=… ← anyone with this link can run commands as you
|
|
1253
1254
|
Plain http: the link and your traffic can be read by anyone on this Wi-Fi.
|
|
1254
1255
|
<the link's QR code>
|
|
@@ -1264,13 +1265,20 @@ web app on iOS has cookies of its own: if it opens on "needs chi web's
|
|
|
1264
1265
|
access token", paste the token there (the part of the link after
|
|
1265
1266
|
`token=`), or open the link once in it.
|
|
1266
1267
|
|
|
1267
|
-
- Every request
|
|
1268
|
-
|
|
1269
|
-
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.
|
|
1270
1277
|
- The token lives in `$XDG_STATE_HOME/samagotchi/web-token` (0600).
|
|
1271
1278
|
`chi web --new-token` replaces it: a running chi web takes the new one at
|
|
1272
1279
|
once, and every phone has to scan the new QR code.
|
|
1273
|
-
- 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
|
|
1274
1282
|
`chi web --web-host lan` while a chi web without LAN access runs asks you
|
|
1275
1283
|
to stop that one first. `chi self` says whether chi web runs on the LAN.
|
|
1276
1284
|
- `lan` picks the first private address (10.x, 172.16–31.x, 192.168.x) on an
|
|
@@ -1414,10 +1422,11 @@ chi --remote work sessions list
|
|
|
1414
1422
|
- Forward only chi web's port, never a worker's: a session's Bridge answers
|
|
1415
1423
|
only chi's own clients on its machine (they read its secret from the
|
|
1416
1424
|
session's folder), so a forwarded worker port is of no use.
|
|
1417
|
-
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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.
|
|
1421
1430
|
- `-L` binds 127.0.0.1 unless you write `-g` or `0.0.0.0:`: don't.
|
|
1422
1431
|
- Attaching works through the tunnel too (`chi --remote work --attach ab12`),
|
|
1423
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). |
|
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,6 +30,17 @@ 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.
|
|
34
45
|
- `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
46
|
- 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.
|
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,
|
|
@@ -126,7 +126,15 @@ participant** and **one role**, until it expires or is revoked.
|
|
|
126
126
|
2. A `chp_` token (`Grants.token?`) whose grant is active: a **participant** (`Principal.new(kind: :participant,
|
|
127
127
|
grant:)`), from any peer: loopback and a chi web not on the LAN too. An ended or unknown one is refused
|
|
128
128
|
(below), never the owner.
|
|
129
|
-
3. No `chp_` token
|
|
129
|
+
3. No `chp_` token on a chi web on loopback only (no `web.host: lan`, no `web.public_url`): the **owner**. On the LAN
|
|
130
|
+
this machine's loopback needs the owner's token too (`App#token_required?`; a pair participant's shell runs here,
|
|
131
|
+
and a token-less `curl 127.0.0.1` was the owner, finding A1); only `GATE_FREE_PATHS` and chi's token-less
|
|
132
|
+
`GET /api/info` probe (`ProxyGate#probe_info?`: no Authorization, no Cookie, its checked fields and `start_dir`)
|
|
133
|
+
are answered without it. `chi web` prints and opens a `Local:` link for 127.0.0.1 with an owner's one-time code
|
|
134
|
+
(`Server.local_owner_link`), the fragment trade sets the cookie there. `PairCommand` still denies a pair command
|
|
135
|
+
naming chi web on a loopback name (defense in depth), and URL openers and listeners: a page opened in the owner's
|
|
136
|
+
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
|
|
137
|
+
to every port).
|
|
130
138
|
4. Anything else: nil. `#token_gate` answers a bound grant's token without its identity **401 `token_bound`**, an
|
|
131
139
|
ended grant's token **401 `token_revoked`** or **401
|
|
132
140
|
`token_expired`** (the page says the same words as a sentence: "This review link was revoked by the session's
|
|
@@ -138,8 +146,9 @@ The `?token=` trade (and `POST /api/token`, with a link's one-time `{code}` or t
|
|
|
138
146
|
presented, the owner's or the participant's
|
|
139
147
|
(one at a time per browser: a participant link opened in the owner's browser replaces the owner's cookie there, and
|
|
140
148
|
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
|
|
149
|
+
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
|
|
150
|
+
401 for a grant's cookie that no longer works clears the cookie, so that browser is the owner's again at its next
|
|
151
|
+
request; on the LAN that browser gets the ended page and opens the owner's link again.
|
|
143
152
|
|
|
144
153
|
Then `#route`, for each matching `ROUTES` row:
|
|
145
154
|
|
|
@@ -277,7 +286,7 @@ queued, as `--turn T` does.
|
|
|
277
286
|
reads of the project ([guardrails](../guardrails.md#a-participants-review-boundary)). In a working session
|
|
278
287
|
`Guardrails::PairBoundary` takes its seat: the owner's checks and rules decide the rest, as for the owner's turns
|
|
279
288
|
([guardrails](../guardrails.md#a-participants-pair-boundary)). Both judge a path every way it resolves
|
|
280
|
-
(`
|
|
289
|
+
(`PathResolve.reals`: a `..` after a symlinked folder climbs out of its target, as the read opens it;
|
|
281
290
|
[guardrails](../guardrails.md#how-paths-resolve)). Approvals and questions are owner-only: either
|
|
282
291
|
boundary denies the model's own question, and `/answer` is an owner route. A pair turn's ask reaches the owner's card
|
|
283
292
|
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).
|
data/docs/sessions.md
CHANGED
|
@@ -6,7 +6,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
6
6
|
|
|
7
7
|
**Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
|
|
8
8
|
|
|
9
|
-
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
9
|
+
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
10
10
|
|
|
11
11
|
**Removing a queued message:** a message queued for after the running turn (`chi send --queue`, the web's Alt+Enter, `/queue TEXT` in a terminal), or one with images (it waits for the turn's end too), can be taken back while it still waits: the worker's Bridge (`POST /session/:id/turn/dequeue {enqueued_id}`, the `unqueue` feature) deletes its input file and every UI drops it (`:turn_dequeued`). It is a race with the turn's end, decided by the same rename the worker uses to take the file: once a turn took it the answer is `409 already_started` (it runs), and an id the worker no longer knows is `404 unknown_enqueued_id`. A plain message sent without `--queue` is not removable (`409 not_queued`): a running turn's next step may take it at any moment. A wait for its turn (`chi send --queue --wait`, a delegate follow-up's wait) ends at once with `removed` (the tombstone `input/removed/<enqueued_id>` says so), and a `chi -p` run whose queued prompt went ends with exit 1. The `200` carries the removed `prompt`: the web and the attached terminal put it back into the composer to edit (not its images). The images a removed message named stay in the session's `images/`. Not removable: a queued session command (`/queue /model x`: it waits in the worker, not in `input/`), a message in a stopped worker's inbox (the next worker runs it), and the plain REPL's (`--no-shared`) own queue.
|
|
12
12
|
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -664,6 +664,19 @@ module Samagotchi
|
|
|
664
664
|
@turn_state.controller
|
|
665
665
|
end
|
|
666
666
|
|
|
667
|
+
# Whether a message a person sent waits for the running turn's next
|
|
668
|
+
# boundary (run_turn's input_waiting: probe): a task_wait returns on it
|
|
669
|
+
# (PeerView#input_waiting?). False outside a turn, for a turn given no
|
|
670
|
+
# probe, and when the probe raises (warned once a turn; the tool never
|
|
671
|
+
# fails for it).
|
|
672
|
+
def input_waiting?
|
|
673
|
+
probe = @turn_state.input_waiting
|
|
674
|
+
!!probe&.call
|
|
675
|
+
rescue StandardError => e
|
|
676
|
+
Log.warn(:turn, "input_waiting_failed", error: e.class.name, msg: e.message) if @turn_state.input_waiting_failed!
|
|
677
|
+
false
|
|
678
|
+
end
|
|
679
|
+
|
|
667
680
|
# Cancel the currently running turn, if any.
|
|
668
681
|
# @param reason [Symbol] cancellation reason
|
|
669
682
|
# @return [Boolean] whether a cancellation was triggered
|
|
@@ -2132,6 +2145,9 @@ module Samagotchi
|
|
|
2132
2145
|
# @param pending_input [#call, nil] optional drain proc returning
|
|
2133
2146
|
# Array<String> of steering messages queued while the turn runs; drained
|
|
2134
2147
|
# by the agentic loop at iteration boundaries (see KernelLoop#run).
|
|
2148
|
+
# @param input_waiting [#call, nil] optional probe: true while a message
|
|
2149
|
+
# a person sent waits for the next boundary (it peeks, never drains);
|
|
2150
|
+
# a running task_wait returns on it (#input_waiting?). nil: never.
|
|
2135
2151
|
# @param continue [Boolean] resume the conversation without appending a
|
|
2136
2152
|
# user prompt (continue after the iteration limit, reminder turns);
|
|
2137
2153
|
# +prompt+ is ignored and :turn_started carries `continue: true`
|
|
@@ -2170,9 +2186,9 @@ module Samagotchi
|
|
|
2170
2186
|
# re-raises; a provider error adds error_kind:, retryable:, host: and a
|
|
2171
2187
|
# one-line summary:.
|
|
2172
2188
|
def run_turn(session, prompt, on_event: nil, max_iterations: nil, cancel_controller: nil, max_tool_output_chars: nil, pending_input: nil, continue: false,
|
|
2173
|
-
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil, verify: nil)
|
|
2189
|
+
origin: TurnOrigin::NONE, images: [], id: nil, resumed: false, resume_from: nil, verify: nil, input_waiting: nil)
|
|
2174
2190
|
turn = begin_turn(session, prompt, on_event: on_event, cancel_controller: cancel_controller, origin: origin,
|
|
2175
|
-
continue: continue, id: id, verify: verify)
|
|
2191
|
+
continue: continue, id: id, verify: verify, input_waiting: input_waiting)
|
|
2176
2192
|
if continue
|
|
2177
2193
|
turn.resumed = resumed
|
|
2178
2194
|
turn.resume_from = resume_from
|
|
@@ -2220,7 +2236,7 @@ module Samagotchi
|
|
|
2220
2236
|
# The turn's state before anything can fail it: the session running,
|
|
2221
2237
|
# the turn flag, its cancel controller and sink. The cross-thread state
|
|
2222
2238
|
# is set here and cleared in #release_turn only.
|
|
2223
|
-
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil)
|
|
2239
|
+
def begin_turn(session, prompt, on_event:, cancel_controller:, origin:, continue:, id: nil, verify: nil, input_waiting: nil)
|
|
2224
2240
|
origin = TurnOrigin.of(origin)
|
|
2225
2241
|
# A participant's Verify fresh child: every turn is theirs (their
|
|
2226
2242
|
# author, boundary and cap), whoever started it.
|
|
@@ -2240,7 +2256,7 @@ module Samagotchi
|
|
|
2240
2256
|
# invalidated recap) while the model is working. A hook's notice goes
|
|
2241
2257
|
# where the turn's events go (the REPL renders only its sink; a
|
|
2242
2258
|
# worker's observers carry it to the bridge).
|
|
2243
|
-
@turn_state.begin!(controller: effective_controller, sink: on_event)
|
|
2259
|
+
@turn_state.begin!(controller: effective_controller, sink: on_event, input_waiting: input_waiting)
|
|
2244
2260
|
# Drop a recap already in flight: the turn makes it stale.
|
|
2245
2261
|
@recap&.invalidate!
|
|
2246
2262
|
# The gate's context: who queued this turn, and git asked afresh.
|
|
@@ -8,7 +8,8 @@ module Samagotchi
|
|
|
8
8
|
# The kernel's Tools::Peers, following the current session. cancelled?
|
|
9
9
|
# is the running turn's cancel, for a tool that waits (delegate_result):
|
|
10
10
|
# the controller is set from another thread and a tool gets no other
|
|
11
|
-
# way to see it.
|
|
11
|
+
# way to see it. input_waiting? is whether a person's message waits for
|
|
12
|
+
# the running turn's next boundary (task_wait returns on it).
|
|
12
13
|
PeerView = Struct.new(:engine) do
|
|
13
14
|
def session_id = engine.session&.id
|
|
14
15
|
def cwd = engine.session&.working_directory
|
|
@@ -16,6 +17,7 @@ module Samagotchi
|
|
|
16
17
|
def state_dir = engine.peer_state_dir
|
|
17
18
|
def cancelled? = !!engine.active_cancel_controller&.cancelled?
|
|
18
19
|
def cancel_reason = engine.active_cancel_controller&.reason
|
|
20
|
+
def input_waiting? = engine.input_waiting?
|
|
19
21
|
def relay = engine.relay_peer
|
|
20
22
|
def model_ref = engine.effective_model_ref
|
|
21
23
|
def thinking = engine.thinking_override
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config"
|
|
4
|
+
require_relative "../paths"
|
|
5
|
+
require_relative "../memory_paths"
|
|
6
|
+
require_relative "../hooks/loader"
|
|
7
|
+
require_relative "path_resolve"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
module Guardrails
|
|
11
|
+
# chi's own dirs, the sets the guardrails judge a path or a command's
|
|
12
|
+
# words against, as this process's environment and config.yml name them
|
|
13
|
+
# (read each call). Each surface keeps its set:
|
|
14
|
+
# .protected the config dir, the hooks dir, the approval store's
|
|
15
|
+
# dir, the installed bundles' dir, attached context: an
|
|
16
|
+
# ask only the user may answer (ParentApprovals.chi_files?)
|
|
17
|
+
# .touches .protected and the session repo's git hooks dir: a
|
|
18
|
+
# rule's touches: chi_dirs (Targets#touches_chi?)
|
|
19
|
+
# .self_protect .protected and the whole state dir, resolved: what
|
|
20
|
+
# System 1 never answers for (SelfProtect)
|
|
21
|
+
# .review the state dir, the config dir and a hooks dir
|
|
22
|
+
# configured outside it: what a participant's turn never
|
|
23
|
+
# reads (ReviewPaths)
|
|
24
|
+
# ProtectedPaths' deny/ask lists and ScratchWrites' memories dir are
|
|
25
|
+
# built by the wiring from the same places.
|
|
26
|
+
module ChiDirs
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# chi's config dir, the hooks dir, the approval store's dir, the
|
|
30
|
+
# installed bundles' dir and attached context.
|
|
31
|
+
def protected
|
|
32
|
+
hooks = begin Hooks::Loader.hooks_dir rescue nil end
|
|
33
|
+
bundles = begin MemoryPaths.bundles_dir rescue nil end
|
|
34
|
+
[ConfigFile.config_dir, hooks, File.join(Paths.state_dir, "guardrails"), bundles, File.join(Paths.state_dir, "context")]
|
|
35
|
+
.compact.map { |dir| dir.to_s.chomp("/") }.reject(&:empty?).uniq
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# .protected (as ParentApprovals.chi_dirs, the name its callers and
|
|
39
|
+
# specs know) and the git hooks dir of +base+'s repo (+context+).
|
|
40
|
+
def touches(context, base)
|
|
41
|
+
require_relative "parent_approvals"
|
|
42
|
+
ParentApprovals.chi_dirs + [context.hooks_dir(base)].compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# .protected (as ParentApprovals.chi_dirs) and the whole state dir,
|
|
46
|
+
# resolved.
|
|
47
|
+
def self_protect
|
|
48
|
+
require_relative "parent_approvals"
|
|
49
|
+
[*ParentApprovals.chi_dirs, Paths.state_dir]
|
|
50
|
+
.map { |dir| PathResolve.real(dir.to_s.chomp("/")) }.reject(&:empty?).uniq
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# chi's state dir and config dir, as Paths has them now, and the hooks
|
|
54
|
+
# dir when config.yml puts it outside the config dir (hooks run as the
|
|
55
|
+
# owner: a participant reads none of them).
|
|
56
|
+
def review
|
|
57
|
+
config = File.join(Paths.config_home, Paths::APP_DIR)
|
|
58
|
+
hooks = begin Hooks::Loader.hooks_dir rescue nil end
|
|
59
|
+
hooks = nil if hooks.to_s.empty? || PathResolve.reals(hooks).any? { |real| PathResolve.within?(real, config) }
|
|
60
|
+
[Paths.state_dir, config, hooks].compact
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|