samagotchi 0.120.0 → 0.121.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +116 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +36 -11
- data/docs/bundle-authoring.md +2 -1
- data/docs/cli.md +10 -4
- data/docs/configuration.md +34 -11
- data/docs/guardrails.md +48 -19
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/participants.md +6 -2
- data/docs/internals/prompt-caching.md +7 -6
- data/docs/internals/remote-sessions.md +15 -11
- data/docs/internals/web-push.md +22 -11
- data/docs/plugins.md +28 -8
- data/docs/sessions.md +15 -0
- data/docs/testing.md +44 -2
- data/lib/samagotchi/answer_command.rb +1 -1
- data/lib/samagotchi/bridge.rb +8 -8
- data/lib/samagotchi/bridge_client/event_stream.rb +11 -27
- data/lib/samagotchi/bridge_client/stream_open.rb +79 -0
- data/lib/samagotchi/bridge_client/stream_request.rb +8 -2
- data/lib/samagotchi/bridge_client.rb +52 -65
- data/lib/samagotchi/bridge_secret.rb +9 -7
- data/lib/samagotchi/bundle_command/check.rb +27 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +21 -7
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +14 -3
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -6
- data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
- data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_move.rb +5 -7
- data/lib/samagotchi/children_status.rb +2 -1
- data/lib/samagotchi/engine.rb +1 -0
- data/lib/samagotchi/guardrail_wiring.rb +19 -2
- data/lib/samagotchi/guardrails/parent_approvals.rb +9 -0
- data/lib/samagotchi/guardrails_command.rb +41 -6
- data/lib/samagotchi/image_store.rb +6 -3
- data/lib/samagotchi/interrupted_turn.rb +6 -3
- data/lib/samagotchi/kernel_loop.rb +5 -0
- data/lib/samagotchi/live_versions.rb +6 -7
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/uninstaller.rb +6 -2
- data/lib/samagotchi/plugin/api.rb +14 -8
- data/lib/samagotchi/plugin/sessions.rb +13 -4
- data/lib/samagotchi/push_command.rb +1 -1
- data/lib/samagotchi/relay_verifier.rb +2 -3
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/remote_attach_command.rb +1 -1
- data/lib/samagotchi/reply_wait/poll.rb +8 -2
- data/lib/samagotchi/reply_wait.rb +4 -3
- data/lib/samagotchi/review_command.rb +1 -1
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session.rb +4 -0
- data/lib/samagotchi/session_access/local.rb +18 -8
- data/lib/samagotchi/session_access/remote.rb +5 -0
- data/lib/samagotchi/session_access/web_bridge_client.rb +31 -19
- data/lib/samagotchi/session_access.rb +21 -9
- data/lib/samagotchi/session_continue.rb +2 -3
- data/lib/samagotchi/session_manager.rb +118 -28
- data/lib/samagotchi/sessions_command.rb +10 -5
- data/lib/samagotchi/system_one/client.rb +6 -1
- data/lib/samagotchi/system_one/fake_backend.rb +0 -2
- data/lib/samagotchi/system_one/log.rb +21 -6
- data/lib/samagotchi/system_one/self_protect.rb +39 -43
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +8 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/local_link.rb +28 -19
- data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +37 -1
- data/lib/samagotchi/toolkit.rb +117 -27
- data/lib/samagotchi/toolkit_binding.rb +32 -0
- data/lib/samagotchi/toolkit_command.rb +23 -0
- data/lib/samagotchi/tools/deferred_tools.rb +10 -2
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +9 -7
- data/lib/samagotchi/tools/registry.rb +20 -6
- data/lib/samagotchi/tools/tool_index.rb +103 -20
- data/lib/samagotchi/turn_checkpoint.rb +15 -9
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +123 -65
- data/lib/samagotchi/web/participant_access.rb +26 -7
- data/lib/samagotchi/web/participant_review.rb +11 -6
- data/lib/samagotchi/web/participant_view.rb +7 -3
- data/lib/samagotchi/web/participants_admin.rb +13 -0
- data/lib/samagotchi/web/public/app.js +1 -1
- data/lib/samagotchi/web/public/forms.css +12 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/level_chip.js +2 -2
- data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
- data/lib/samagotchi/web/public/notifications.js +21 -1
- data/lib/samagotchi/web/public/participant.html +4 -2
- data/lib/samagotchi/web/public/participant.js +34 -11
- data/lib/samagotchi/web/public/participants_panel.js +49 -14
- data/lib/samagotchi/web/public/push.js +4 -3
- data/lib/samagotchi/web/public/push_devices.js +17 -3
- data/lib/samagotchi/web/public/question_card.js +15 -1
- data/lib/samagotchi/web/public/question_cards.js +11 -2
- data/lib/samagotchi/web/public/review_history.js +24 -4
- data/lib/samagotchi/web/public/review_start.js +1 -3
- data/lib/samagotchi/web/public/session_actions.js +14 -2
- data/lib/samagotchi/web/public/storage.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
- data/lib/samagotchi/web/push/client.rb +5 -0
- data/lib/samagotchi/web/push/store.rb +16 -3
- data/lib/samagotchi/web/push_routes.rb +3 -2
- data/lib/samagotchi/web/session_access_routes.rb +25 -17
- data/lib/samagotchi/web/session_create.rb +4 -4
- data/lib/samagotchi/web/session_hub.rb +6 -4
- data/lib/samagotchi/web/session_summary.rb +8 -16
- data/lib/samagotchi/web/tunnel.rb +10 -1
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_link.rb +213 -0
- data/lib/samagotchi/worker_sidecar.rb +15 -37
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b07fb49361e67b325eaaf3190d15d3f889a6df2e590585b45257a0a2b1409ac9
|
|
4
|
+
data.tar.gz: 37e6db4dc2235fcfe9127d38261056983f609ebd0dd1a214a903ec94c8afec35
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a8e5c43eee0002c4054c6e347f75a795fa09216a7f0ea3db30770043a5eaaf57fa34f9a1ee5a4ddf64117f1e8f3bb5ed4ccf8b34c55bb5c9a2e6398c1fec997
|
|
7
|
+
data.tar.gz: 23714d86e0872fdcba52fb019b30dbf84c4531a3c44a13f0cb30b72cc95bb59506c654cb53ae395b3d8ae5d83672c29fe8624958ef968dc5648b4189d9e8731f
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,119 @@ 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.121.1] - 2026-10-27
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `chi sessions restart` can recover an unreachable, secret-refusing, newer-protocol or stuck-start worker without a
|
|
15
|
+
stop marker, but refuses when disk shows a running turn, pending question, queued input or running background task.
|
|
16
|
+
- chi web now opens a session stream before answering: a missing, leaving or unreachable worker gets 503, a Bridge
|
|
17
|
+
refusal gets 502 instead of an empty 200 or SSE frame, and the opened socket closes when the stream ends. Remote
|
|
18
|
+
stream retries distinguish 503 from a 502 `bridge_secret`, and `/stats` reports secret refusals consistently.
|
|
19
|
+
- A plugin's `ctx.sessions.fork` reports a child chi won't start as `Samagotchi::Plugin::Sessions::Error` with chi's
|
|
20
|
+
own reason (the session's model is gone from the config, or its toolkit is a `toolkit.kits` kit with an invalid
|
|
21
|
+
`groups:`) instead of handing the bundle the raw `Toolkit::Refused` or `ModelProfile::MissingModel`.
|
|
22
|
+
- A `toolkit.kits` kit whose `groups:` isn't a name or a list (a map such as `{chrome: true}`) no longer loads
|
|
23
|
+
silently with its base's groups: chi, `chi send --new`, chi web and `delegate` refuse a new session on it, naming
|
|
24
|
+
the kit and the reason, and a session already saved on it reaches no tool-index group until config.yml is fixed.
|
|
25
|
+
- A worker's liveness is one rule everywhere, the owner lock plus the sidecar's pid, with no port probe: after a
|
|
26
|
+
`kill -9` whose port another session's Bridge took, `chi send`, `--attach` and chi web no longer take the dead
|
|
27
|
+
worker for live (they wake a new one). A worker on a newer sidecar protocol is not driven by an older chi: a wake
|
|
28
|
+
answers 409 `worker_newer`.
|
|
29
|
+
- System 1 no longer auto-allows a command that changes chi's own config through a spelling its check missed
|
|
30
|
+
(`$XDG_CONFIG_HOME/samagotchi/config.yml`, `$HOME/.config/…`, `memories/../config.yml`, `cd ~/.config && …`): it
|
|
31
|
+
refuses by the same test that makes an ask the user's alone, so such a call goes to the user. Memory files still
|
|
32
|
+
can be auto-allowed.
|
|
33
|
+
- `chi guardrails replay` no longer judges an ask again that System 1 already judged live (audit or on mode), which
|
|
34
|
+
counted each such decision twice and let a rule pass `min_record` early: a live label now carries the card's
|
|
35
|
+
`question_id`, and a live line without one is matched by its session and call digest.
|
|
36
|
+
- Stopping a turn while System 1 judges one of its approvals no longer opens an approval card for the stopped turn
|
|
37
|
+
(nor logs the cancel as a failed judgment): the call is denied with "The turn was stopped".
|
|
38
|
+
- A participant no longer reads a one-time link code (`chc_…`, as in an invite link a reply quotes) or a Bridge's
|
|
39
|
+
`X-Chi-Bridge` secret: both are redacted like `chp_` tokens.
|
|
40
|
+
- An agent participant's `chi --remote … send --wait` goes on after chi web restarts: chi web reads whose turn it is
|
|
41
|
+
from the claimed input file, the last turn or the refused turn's record, not only its memory (it answered 403).
|
|
42
|
+
- chi web's participants routes (list, invite, revoke, cap) answer a grants store chi can't write (unreadable, or a
|
|
43
|
+
newer chi's) with 409 `grants_unwritable` and the store's own message, not a 500 with a backtrace on every refresh.
|
|
44
|
+
- A participant name or an annotate preset holding `\&`, `` \` `` or `\'` no longer breaks chi web's page: it was read
|
|
45
|
+
as a backreference when the page's attributes went in.
|
|
46
|
+
- One push subscription with a `p256dh` key off the P-256 curve no longer stops every push: chi web refuses such a
|
|
47
|
+
subscribe (400 `invalid_subscription`), and one already on disk fails that device alone, so the other devices still
|
|
48
|
+
get the push, `POST /api/push/test` answers that device as failed instead of a 500, and `chi web push test` prints
|
|
49
|
+
its failure instead of a backtrace.
|
|
50
|
+
- The notifications sheet's switches always show what chi web has: "this device" no longer stays on after iOS's
|
|
51
|
+
Don't Allow, nor "Long turns done" moved after its change failed.
|
|
52
|
+
- Turning push off on a flaky connection no longer drops this phone's subscription while chi web keeps the device:
|
|
53
|
+
chi web is told first, and a failure leaves the bell on with the error. The Home Screen app whose first `GET
|
|
54
|
+
/api/push` failed (chi web restarting, a phone just woken) tries again when it comes back to the front instead of
|
|
55
|
+
staying in tab mode until a reload.
|
|
56
|
+
- `chi --help` lists `chi guardrails replay` (seeding System 1's record), the one subcommand it left out.
|
|
57
|
+
- The participants panel's Cap form no longer sets a $0 cap when its field is cleared, nor sends a negative or
|
|
58
|
+
over-$1000 one: it refuses as the Invite's cap field does ("A cap is dollars, 0 to 1000.").
|
|
59
|
+
- A double tap on the web's Stop no longer asks twice (two confirms, or two stops): a second Stop of the same session
|
|
60
|
+
while the first one asks the worker what still runs, or stops it, does nothing.
|
|
61
|
+
- A participant's comment box keeps what they typed while their last comment was on its way (it was cleared when the
|
|
62
|
+
comment was accepted), and an error under the box (a refusal, "Images can't go in a review comment yet") goes at
|
|
63
|
+
their next edit or send instead of staying until a comment went through.
|
|
64
|
+
- An invite whose participants panel closed before chi web answered no longer loses its one-time link (the grant was
|
|
65
|
+
made, so the owner had to revoke and invite again): a sticky toast names it with Copy link.
|
|
66
|
+
- A participant's thread no longer jumps when a read fails and again when it recovers: "Can't reach the session —
|
|
67
|
+
retrying…" (and "This session is gone.") shows over the thread's top instead of as a new row in the header.
|
|
68
|
+
- A participant's wait on a turn whose worker died in it is no longer refused (403) after chi web restarts: the turn's
|
|
69
|
+
canceled (`worker_stopped`) record keeps whose turn it was.
|
|
70
|
+
- claude-peer (0.4.5): `pair` run as `bash pair` or by zsh no longer refuses every call with its usage (only `sh
|
|
71
|
+
pair` kept the id and the `--inbox-socket` path), and a carriage return or another control character in what it
|
|
72
|
+
pushes (the cwd) is escaped, so chi's JSON read no longer refuses the push.
|
|
73
|
+
- codex-peer (0.3.4): a carriage return or another control character in what `pair` pushes (the cwd) is escaped, as
|
|
74
|
+
claude-peer's is, so chi's JSON read no longer refuses the push.
|
|
75
|
+
- A toolkit's `groups:` match source groups in any case and with `{a,b}` braces, as the guardrails rules' `models:`
|
|
76
|
+
do (`groups: [Chrome]` missed `chrome`), and a config kit's `groups:` that isn't a name or a list (a map) is refused
|
|
77
|
+
with a line, the kit loading without it, instead of matching its stringified pairs.
|
|
78
|
+
- A plugin's `chi.toolkit(groups:)` that isn't a name, a list or `:all` (a map) is a load error ("toolkit NAME: groups
|
|
79
|
+
must be a list of group names or globs"), as a config kit's is refused, instead of matching its stringified pairs.
|
|
80
|
+
- `chi bundle check`'s `requires-chi-floor` sees a keyword added later: `chi.toolkit(prompt:)` (0.72.0),
|
|
81
|
+
`chi.toolkit(groups:)` (0.121.0), `ctx.ask_model(model:)` (0.80.0) and `ctx.sessions.send(delivery:)` (0.64.0).
|
|
82
|
+
- A remote `chi share` (or `share ls`/`cap`/`revoke`) whose chi web can't read its grants store prints the store's own
|
|
83
|
+
line, as a local one does, instead of "… answered 409 grants_unwritable: …".
|
|
84
|
+
- `chi web push test`'s line for a push that failed before the push service answered reads "failed (12 ms)", not
|
|
85
|
+
"failed (12 ms)".
|
|
86
|
+
|
|
87
|
+
Update with `chi update` (claude-peer 0.4.5, codex-peer 0.3.4, github-pr 0.5.1), then restart `chi web` and
|
|
88
|
+
`chi sessions restart` the running sessions.
|
|
89
|
+
|
|
90
|
+
## [0.121.0] - 2026-10-26
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- Toolkit kits pick the MCP servers their sessions reach: `groups: [chrome]` in `toolkit.kits` (names or globs; no
|
|
95
|
+
key = the base's; the built-ins reach every server, `review` none). `/toolkit` switches them from the next turn
|
|
96
|
+
(asking first: the cached prompt changes). A call into a server the kit doesn't reach is refused without starting
|
|
97
|
+
it, naming a kit that reaches it; its deferred stub says why, `/mcp` marks such servers, and the web's toolkit chip
|
|
98
|
+
lists a restricted kit's servers. Kits without `groups:` send the same request as before.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- `ask_user_question`'s description asks the model, when it recommends one option, to put it first and end its
|
|
103
|
+
label with " (Recommended)" (eval 0005: Flash Next and DeepSeek v4.1-flash mark it 5/5 on a decision question,
|
|
104
|
+
0/5 before; an answer comes back with the suffix, so `chi answer --option` takes that exact label or the number).
|
|
105
|
+
- The recommended option shows as such: chi web's question card marks its row in the accent colour with a small
|
|
106
|
+
"recommended" tag (the label shown, and the answered line, without the suffix; nothing is pre-checked), and the
|
|
107
|
+
terminal's question paints its row bold. Approval and step-limit cards are unchanged.
|
|
108
|
+
- chi web's select boxes have one shape in every dialog: the Invite panel's person/agent and Expires now match the
|
|
109
|
+
text fields beside them (flat box, same height and corners, one thin caret), as the Thinking and LLM context
|
|
110
|
+
dialogs' selects already did, in place of the browser's own pill.
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
|
|
114
|
+
- claude-peer (0.4.4): `pair --inbox-socket PATH` says the remote socket is forwarded to this Claude's socket, not
|
|
115
|
+
from it (`ssh -R`).
|
|
116
|
+
- An ended participant link forgets its comment history even when its first review read is refused.
|
|
117
|
+
- The participant review status stays idle when a live snapshot has no current turn and its status is stale.
|
|
118
|
+
- The participant usage row no longer says "0 queued" when no worker is running.
|
|
119
|
+
- The participant usage row counts link time down and shows its end while the page stays open.
|
|
120
|
+
|
|
121
|
+
Update with `chi update` (the mcp bundle 0.7.2, claude-peer 0.4.4, the dev profile 0.7.3).
|
|
122
|
+
|
|
10
123
|
## [0.120.0] - 2026-10-26
|
|
11
124
|
|
|
12
125
|
### Added
|
|
@@ -4518,7 +4631,9 @@ and long-lived sessions.
|
|
|
4518
4631
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4519
4632
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4520
4633
|
|
|
4521
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4634
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.121.1...HEAD
|
|
4635
|
+
[0.121.1]: https://github.com/dm1try/samagotchi/compare/v0.121.0...v0.121.1
|
|
4636
|
+
[0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
|
|
4522
4637
|
[0.120.0]: https://github.com/dm1try/samagotchi/compare/v0.119.0...v0.120.0
|
|
4523
4638
|
[0.119.0]: https://github.com/dm1try/samagotchi/compare/v0.118.0...v0.119.0
|
|
4524
4639
|
[0.118.0]: https://github.com/dm1try/samagotchi/compare/v0.117.0...v0.118.0
|
data/bin/chi
CHANGED
|
@@ -391,7 +391,7 @@ if launch == :attached
|
|
|
391
391
|
toolkit_flags: options[:toolkit_flags] || [],
|
|
392
392
|
settings: options[:settings],
|
|
393
393
|
parent_answers: options[:parent_answers])
|
|
394
|
-
rescue Samagotchi::TerminalUI::AttachLauncher::Error => e
|
|
394
|
+
rescue Samagotchi::TerminalUI::AttachLauncher::Error, Samagotchi::Toolkit::Refused => e
|
|
395
395
|
warn "Error: #{e.message}"
|
|
396
396
|
exit 1
|
|
397
397
|
rescue Interrupt
|
|
@@ -427,7 +427,8 @@ begin
|
|
|
427
427
|
# limit, or Ctrl-C cancelled it.
|
|
428
428
|
exit 1 if %i[turn_failed empty_answer iteration_limit].include?(ended)
|
|
429
429
|
exit Samagotchi::CLI::Exit::INTERRUPTED if ended == :interrupted
|
|
430
|
-
rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel
|
|
430
|
+
rescue Samagotchi::TerminalUI::SessionBusy, Samagotchi::TerminalUI::SessionNotFound, Samagotchi::ModelProfile::MissingModel,
|
|
431
|
+
Samagotchi::Toolkit::Refused => e
|
|
431
432
|
warn "Error: #{e.message}"
|
|
432
433
|
exit 1
|
|
433
434
|
rescue Samagotchi::LLM::ProviderError => e
|
data/docs/architecture.md
CHANGED
|
@@ -393,14 +393,18 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
393
393
|
worker.
|
|
394
394
|
- **The secret.** Every request, on every route (the stream, the reads such as `state` and `stats`, the writes, and
|
|
395
395
|
an unknown path), names the Bridge's secret in `X-Chi-Bridge`; one without it, or with another one, is
|
|
396
|
-
`403 bridge_secret` with one message (`BridgeSecret
|
|
397
|
-
|
|
396
|
+
`403 bridge_secret` with one session-specific message (`BridgeSecret.refused`): "session ID's worker refused
|
|
397
|
+
this chi's request (it didn't name its secret): a chi web or terminal started before chi was updated can't
|
|
398
|
+
reach it; restart chi web, re-attach with `chi --attach ID`, or restart the worker with `chi sessions restart
|
|
399
|
+
ID`." `chi sessions restart` can also recover when the worker cannot be asked: TERM + resume if disk shows no
|
|
400
|
+
running turn, pending question or approval, queued input, or running background task. The check is after the
|
|
401
|
+
browser and proxy checks and before any route (`Bridge#secret_ok?`,
|
|
398
402
|
`OpenSSL.secure_compare`). No route is left open: what the reads show is the session itself. Only a request that
|
|
399
403
|
got past it counts as client activity, so a refused one (an older chi web's polls) never holds the idle exit. Each Bridge makes its own (`SecureRandom.hex(32)`, 64 hex
|
|
400
404
|
characters) when it starts and writes it to `bridge.json`, which is 0600 from the moment it exists
|
|
401
405
|
(`AtomicFile` creates the temporary file with that mode, then renames it), so only this machine's user can read
|
|
402
406
|
it: a forward that adds no proxy header, or another local account, reaches the port but not a session. Clients get
|
|
403
|
-
it only from the sidecar: `BridgeClient.
|
|
407
|
+
it only from the sidecar: `WorkerLink.client`/`.await` and `BridgeClient.for_sidecar` take the port and the secret from the
|
|
404
408
|
same read (chi web makes its client from the sidecar for every request, the attached terminal again on a
|
|
405
409
|
`worker_changed`), so a respawned worker's new one is used and nothing passes a port by hand. A worker from before
|
|
406
410
|
the secret writes none and asks for none; a chi web or attached terminal from before it sends none, so a worker
|
|
@@ -408,7 +412,7 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
|
|
|
408
412
|
(`BridgeClient::Response#secret_refused?`) and shows the one message: chi web's relays answer 502 `bridge_secret`,
|
|
409
413
|
`chi answer` and a parent's approval relay say it instead of "refused", and an event stream ends with reason
|
|
410
414
|
`bridge_secret` once rediscovery finds no newer worker (the attached terminal's closing line), never retrying in
|
|
411
|
-
silence; chi web's stream proxy
|
|
415
|
+
silence; chi web's stream proxy answers 502 `bridge_secret`, never passing the refusal on as frames. The secret is never logged (a refusal logs
|
|
412
416
|
`secret_refused` with the path, at most once a minute), never in an event, a reply, `/api/info` or a session
|
|
413
417
|
file, and `WorkerSidecar`, `BridgeClient` and `Bridge` leave it out of `#inspect`.
|
|
414
418
|
|
|
@@ -491,10 +495,29 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
|
|
|
491
495
|
`handler_failed`) rather than dropping the connection.
|
|
492
496
|
|
|
493
497
|
The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
|
|
494
|
-
for client discovery, with the Bridge's secret (see above).
|
|
495
|
-
|
|
496
|
-
`
|
|
497
|
-
|
|
498
|
+
for client discovery, with the Bridge's secret (see above).
|
|
499
|
+
|
|
500
|
+
**Worker link.** Every client asks `WorkerLink` (`worker_link.rb`) whether a session's worker can be reached, never
|
|
501
|
+
reading `bridge.json` or probing a port itself: `WorkerLink.resolve(session_dir, unlink:, owner:)` answers a typed
|
|
502
|
+
outcome. The rule is the owner lock plus the sidecar's `pid`; there is no port probe (a port can be reused by another
|
|
503
|
+
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
|
|
504
|
+
sidecar is `Stale(:lock_free)` (removed with `unlink: true`, only if it is still the file that was read), else `None`;
|
|
505
|
+
a holder with no kind yet: `Starting`; a leaving worker: `Leaving`; a worker whose sidecar names no port or another
|
|
506
|
+
pid than the holder's (a worker that has not written its own yet): `Starting`, `stuck` once it has held the lock
|
|
507
|
+
past `START_LIMIT` (30 s) with none; a sidecar whose `protocol` is newer than `WorkerLink::PROTOCOL`: `Skew` (a
|
|
508
|
+
newer worker; never driven, `wake` answers 409 `worker_newer`); else `Live` (its sidecar; `#client` makes the one
|
|
509
|
+
`BridgeClient`). A sidecar with no `pid` is an older worker's: the lock alone says it is live. `owner:` is the
|
|
510
|
+
holder as the caller already read it (nil: free), so one read serves a summary and its status. `WorkerLink.client` is
|
|
511
|
+
the Live's client or nil; `WorkerLink.await` polls for a new worker's Live (an `until:` check tells a restart's
|
|
512
|
+
successor from the old worker); chi web's `App#worker_link` / `#await_worker_link` wrap them. The Bridge writes
|
|
513
|
+
`pid` and `protocol` with its port and removes its sidecar on stop only when the file still carries its epoch.
|
|
514
|
+
|
|
515
|
+
`chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
|
|
516
|
+
(503 `not_live` for no worker, a leaving or stuck worker, or an unreachable Bridge; 502 `worker_newer` for skew and
|
|
517
|
+
`bridge_error` / `bridge_secret` for a refused Bridge; 200 only after the Bridge answers; full history of any session
|
|
518
|
+
is served by `GET /api/sessions/:id/output`). It opens the Bridge before answering and gives the one opened socket to
|
|
519
|
+
`App#proxy_stream_body` / `ProxyStreamBody`, which closes it when the body or hijacked client ends. The owner's stream
|
|
520
|
+
is a byte pipe; the body gives it the request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
|
|
498
521
|
`?as=participant`, a `Web::ParticipantStreamFilter`, which cuts the Bridge's chunks into frames and writes each again
|
|
499
522
|
from `Web::ParticipantView.event`'s allowlist, dropping every other event; only `from_seq` and `snapshot` reach the
|
|
500
523
|
Bridge; [internals/participants.md](internals/participants.md#what-a-participant-sees)). Resume/ring-buffer state is **in-memory**, per worker: a cursor from
|
|
@@ -531,7 +554,7 @@ moved through `Session.summary_from_file`. Liveness is probed, since a killed ow
|
|
|
531
554
|
file trace: the owner lock every tick for the sessions the projection believes owned, and every
|
|
532
555
|
session every 10 s, so a `kill -9` shows within a second. The summary (`Web::SessionSummary`,
|
|
533
556
|
shared with `/api/sessions` and the session view) carries `owner`, `project_root` and
|
|
534
|
-
`bridge_up` (
|
|
557
|
+
`bridge_up` (`WorkerLink` answers Live: a worker holds the lock and its sidecar names its pid; the page attaches its stream on it), and
|
|
535
558
|
from that sidecar `worker_version` (the chi the worker runs) and `worker_features`
|
|
536
559
|
(`Bridge::FEATURES`, what a client may ask of it beyond the routes: `restart`, `task_stop`, `reparent`, `unqueue`,
|
|
537
560
|
`delivery`, a worker that queues a message for after the turn; `SessionManager.deliver_turn` and
|
|
@@ -577,12 +600,12 @@ model: [internals/remote-sessions.md](internals/remote-sessions.md).
|
|
|
577
600
|
| `GET /api/sessions/:id/worker` | owner | | 200 `{session_id, live, stopped, owner: worker \| tui \| null, worker: {epoch, started_at, version, features} \| null, default_model, parent_strategy}` (`Local#worker`; never starts one; `default_model` is this machine's) | 404 |
|
|
578
601
|
| `POST /api/sessions/:id/exit` | owner | `client_id` (passed on only when it is an attached TUI's, `tui:…`; any other is dropped), `delete` | 200 the worker's `{status: exiting, session_id, discard}` (`BridgeClient#request_exit`; never wakes one) | 409 `held` with the worker's `{status, reason, tasks, hint}`; 503 `not_live`; 501 `not_supported`; 504 `worker_timeout` |
|
|
579
602
|
| `POST /api/sessions/:id/recap` | owner | | 200 the worker's `{enabled, saved, request, min_user_turns, session_off}` (`BridgeClient#request_recap`) | 503, 501, 504 as every relay |
|
|
580
|
-
| `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_error` |
|
|
603
|
+
| `GET /api/sessions/:id/stats` | owner | | 200 `{session_id, metrics}`: the worker's stats, its state's metrics for a worker from before the stats route | 503 `not_live`; 504 `worker_timeout`; 502 `bridge_secret` or `bridge_error` |
|
|
581
604
|
| `GET /api/sessions/:id/children/counts` | owner | | 200 `{running, waiting, unreported}` (`ChildrenStatus.counts`, the delegates' files here) | 404 |
|
|
582
605
|
| `GET /api/sessions/:id/turns/:turn_id/records` | owner | | 200 `{tool_records: […], turn_record: {…} \| null}` (`SessionMetrics.saved_tool_records` / `saved_turn_record`, as saved) | 400 `invalid_turn_id`; 404 |
|
|
583
606
|
| `POST /api/sessions/:id/turn` (`no_interrupt`) | owner | `no_interrupt: true`: the turn runs with the raised iteration limit (`SessionManager.deliver_turn(no_interrupt:)`, to the Bridge or the input file) | as before | |
|
|
584
607
|
| `POST /api/sessions/:id/command` (its 501) | owner | | | 501 `not_supported` for a builtin the worker lacks adds `command` (`"/title"`) and `message` |
|
|
585
|
-
| `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404 |
|
|
608
|
+
| `GET`/`POST`/`DELETE /api/sessions/:id/participants`, `GET`/`DELETE /api/participants` | owner | `chi share`'s (`name`, `kind`, `ttl`; `?id=p_…`) | the grants, never a token but POST's own once ([internals/participants.md](internals/participants.md)) | 400 `invalid_participant`; 404; 409 `grants_unwritable` (the store doesn't read: its words, which a remote `chi share` prints as a local one does) |
|
|
586
609
|
| `GET /api/sessions/:id/apply[?turn=T]` | owner | `turn`: a turn id or a prefix (none: the latest review turn with a reply) | 200 `{text, turn_id, author, author_name}` (`ReviewApply`): the owner's Apply of a participant's review turn, for the composer, never sent ([internals/participants.md](internals/participants.md)) | 404 `not_found`; 400 `not_review_turn`; 409 `no_reply` |
|
|
587
610
|
| `POST /api/sessions` (`review`) | owner | `review: {subjects: [project files], from: a session id}`; never with `prompt`, `preview`, `idle` or `continues` (a `from` takes the source's folder and setup) | 201 as a create's: an idle review session (`ReviewStart.start`, or `ReviewStart.fork` with `from`) | 400 `invalid_review` (`ReviewStart::Refused`'s words: a subject, a gone folder); with `from` also 404 `not_found`, 400 `ambiguous_id` |
|
|
588
611
|
|
|
@@ -723,8 +746,10 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
723
746
|
| Shipped bundles | `bundles/` (`mcp`, `github-pr`, `loop-guard`, `skills`, `source-links`, …) | Bundles that ship with chi. |
|
|
724
747
|
| Hooks | `hooks/registry.rb` (`Hooks::Registry`), `hooks/loader.rb`, `hook_routes.rb` (`HookRoutes`), `event_routes.rb` (`EventRoutes`) | Hook registration and dispatch; what a hook or plugin does beyond reading its event (notice, question, stop, steer) routed to the UIs (cards and notices through one set of routes) ([hooks.md](hooks.md)). |
|
|
725
748
|
| Guardrails | `guardrails.rb`, `guardrails/shell_lex.rb`, `guardrails/data_readers.rb`, `guardrails/rules.rb`, `guardrails/protected_paths.rb`, `guardrails/parent_policy.rb` + `guardrails/parent_approvals.rb`, `guardrail_wiring.rb` | The gate before every tool call: shell lexing (and the commands that only read a heredoc as data), YAML rules, the core checks on chi's own files, approvals, what a parent agent may answer (`guardrails.parent_answers`) ([guardrails.md](guardrails.md)). |
|
|
749
|
+
| System 1 | `system_one.rb`, `system_one/` (`SystemOne::Client`, `ApprovalQuestion`, `Log`, `Record`, `SelfProtect`, `Similar`, the backends), `guardrail_wiring.rb` (`GuardrailWiring#request_approval`), `guardrails_command.rb` (`chi guardrails replay`) | The approval judge: a model's `p` for each ask, logged to `judgments.jsonl` with the user's answer; in `on` mode an ask on a listed rule with a clean record is allowed once without the user. Labelled by `/guardrails wrong` (`SessionCommands`) and the web's **should have asked** (the Bridge's `POST guardrails/wrong`) ([guardrails.md](guardrails.md#system-1-experimental)). |
|
|
726
750
|
| Delegation | `tools/delegate.rb`, `tools/delegate_result.rb`, `tools/delegate_wait.rb`, `child_reports.rb`, `tools/delegate_relay.rb`, `relay_desk.rb`, `relay_verifier.rb`, `relay_watcher.rb` | Child sessions a session starts and waits on, and their approval relay ([sessions.md](sessions.md#delegating)). |
|
|
727
751
|
| send / broadcast | `send_command.rb` (`SendCommand`), `broadcast_command.rb` (`BroadcastCommand`), `broadcast/` | `chi send` and `chi broadcast` ([broadcast.md](broadcast.md), [sub-agent.md](sub-agent.md)). |
|
|
752
|
+
| Worker link | `worker_link.rb` (`WorkerLink`: `Live`, `Starting`, `Leaving`, `Skew`, `Stale`, `None`), `worker_sidecar.rb` (`WorkerSidecar`) | Whether a session's worker can be reached and the client for it: every Bridge client asks `WorkerLink.resolve` / `.client` / `.await` (*Worker link* above); `WorkerSidecar` is the `bridge.json` it reads. |
|
|
728
753
|
| Session access | `session_access.rb` (`SessionAccess`: `NewSession`, `SetupWords`, `WaitStart`, `SessionView`, `Created`, `ContextEntry`, `ContextPush`, the errors `RemoteFailed`, `RemoteTooOld`, `Refused`, `BadRequest`), `session_access/local.rb` (`SessionAccess::Local`), `session_access/remote.rb` (`SessionAccess::Remote`), `session_access/http_transport.rb`, `session_access/web_bridge_client.rb` (`SessionAccess::WebBridgeClient`, a remote attach's worker through chi web), `session_access/dispatch.rb` (`SessionAccess::Dispatch`), `session_access/contract.rb` (`SessionAccess::Contract`), `remotes.rb` (`Remotes`), `remote_command.rb` (`chi remote`), `web/session_access_routes.rb` (`Web::SessionAccessRoutes`) | The seam between the session commands and where the sessions live: `Local` is this machine's (the `SessionManager`, `ReplyWait`, `NoteDelivery`, `ContextSources` and Bridge calls the commands made); `Remote` is another machine's, the same methods as requests to its chi web's routes of the same name (Bearer token, `HTTPTransport`), results as Local's value objects and refusals as the classes Local's callees raise, built from the fields chi web sends; waits and prefixes run on the server, a wait asked again per 30 s slice. `Dispatch` is bin/chi's front for `--remote NAME` / `CHI_REMOTE` (taken off before the subcommand lookup): it refuses what the contract table lists `local_only` (exit 2, one message) and runs the command with a `Remote` on the `Remotes` entry (`$XDG_CONFIG_HOME/samagotchi/remotes.yml`, 0600). chi web's routes answer from `Local` (*chi web's routes for the session commands* above). Checks that read this machine's config or files (vision, parent answers, the cwd's project, `SAMAGOTCHI_PARENT_SESSION`) stay in the commands and are skipped under a `Remote`, where chi web's create, turn and wait routes run the vision, parent-answers and busy checks with the server's config and the worker checks a parent's answer. See [internals/remote-sessions.md](internals/remote-sessions.md). |
|
|
729
754
|
| Idle work | `idle_scheduler.rb` (`IdleScheduler`), `idle_recap.rb` (`IdleRecap`), `idle_reminders.rb` | One poller for an idle session's recap and reminders. |
|
|
730
755
|
| Attached context | `context_sources.rb` (`ContextSources`), `context_poller.rb`, `context_absorber.rb`, `peer_sources.rb` (`PeerSources`: the sources peer bundles declare) | External sources a session keeps fresh, and the peer loop's wakes ([context.md](context.md)). |
|
data/docs/bundle-authoring.md
CHANGED
|
@@ -130,7 +130,8 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
|
|
|
130
130
|
after the last edit.
|
|
131
131
|
- **requires_chi.** Use the chi the user runs (`chi --version`). `check` warns when the plugin calls something
|
|
132
132
|
newer than the floor (`ctx.http` needs 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0). A keyword added
|
|
133
|
-
later
|
|
133
|
+
later counts too (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)`
|
|
134
|
+
0.72.0, `chi.toolkit(groups:)` 0.121.0), when the call is written on `ctx` / `chi` itself (not through a variable).
|
|
134
135
|
- **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
|
|
135
136
|
- **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
|
|
136
137
|
new session (or `chi sessions restart ID`).
|
data/docs/cli.md
CHANGED
|
@@ -518,10 +518,16 @@ chi installed, without stopping it: an attached terminal and the web's tabs
|
|
|
518
518
|
move to the new worker. It is refused, with the reason, while something would
|
|
519
519
|
be lost: a turn running or queued, a question or approval waiting, reminders,
|
|
520
520
|
a `/btw` still running, an approval relayed from a delegate, or background
|
|
521
|
-
tasks the session started.
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
521
|
+
tasks the session started. If the worker cannot be asked (a refused Bridge,
|
|
522
|
+
protocol mismatch or stuck start), chi sends it TERM and resumes only when
|
|
523
|
+
disk shows no running turn, pending question or approval, queued prompt, or
|
|
524
|
+
running background task; otherwise it says to run `chi sessions stop ID`, then
|
|
525
|
+
send any prompt. That fallback may lose reminders, a running `/btw`, and other
|
|
526
|
+
in-memory state. A session with no running worker needs no restart (its next
|
|
527
|
+
prompt starts one on the newest chi), and an older worker without restart
|
|
528
|
+
support needs `chi sessions stop ID`. The Web UI's
|
|
529
|
+
`POST /api/sessions/:id/restart` does the same (409 with `reason` and `detail`
|
|
530
|
+
when it can't). An attached
|
|
525
531
|
terminal says at the join, in one `chi>` line, when the worker or the terminal
|
|
526
532
|
runs an older chi than the newest installed (the worker moves with `chi
|
|
527
533
|
sessions restart ID`, the terminal with `/detach` and `chi --attach ID`), and
|
data/docs/configuration.md
CHANGED
|
@@ -1400,6 +1400,8 @@ toolkit:
|
|
|
1400
1400
|
kits: # your own, on a base
|
|
1401
1401
|
audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
|
|
1402
1402
|
terse: { base: lean, prompt: "- Be concise in your responses" }
|
|
1403
|
+
web: { base: lean, groups: [chrome] }
|
|
1404
|
+
coding: { base: lean, groups: [] }
|
|
1403
1405
|
```
|
|
1404
1406
|
|
|
1405
1407
|
| toolkit | declares |
|
|
@@ -1412,8 +1414,25 @@ toolkit:
|
|
|
1412
1414
|
| `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
|
|
1413
1415
|
|
|
1414
1416
|
- **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
|
|
1415
|
-
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `
|
|
1416
|
-
key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
|
|
1417
|
+
the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:` and `groups:`
|
|
1418
|
+
(any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
|
|
1419
|
+
(`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
|
|
1420
|
+
([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil)).
|
|
1421
|
+
- **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`; any case, `{a,b}` braces,
|
|
1422
|
+
as the guardrails rules' `models:`); for the MCP bundle each group is a server. Anything but a name or a list (a
|
|
1423
|
+
map such as `{chrome: true}`) refuses every new session on that kit or on a kit built on it, on every surface (chi,
|
|
1424
|
+
`chi send --new`, chi web, `delegate`, a continue or a fork): `toolkit.kits.NAME: groups must be a list of group
|
|
1425
|
+
names or globs; fix config.yml to start a session on toolkit KIT`. A session already saved on it keeps running,
|
|
1426
|
+
its kit reaching no group (with a `toolkit>` warning) until config.yml is fixed. Selectors are sorted and deduplicated and match the groups declared when plugins load.
|
|
1427
|
+
An omitted key inherits the base's selectors; `full` and `lean` (and `child`, which is on `lean`) reach all
|
|
1428
|
+
groups, `review` (the participants' kit) reaches none, `groups: ["*"]` matches every group, and `groups: []`
|
|
1429
|
+
reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
|
|
1430
|
+
every group.
|
|
1431
|
+
A selector that matches no group in that load-time snapshot warns once. `find_tools` and `call_tool` are
|
|
1432
|
+
declared only when the toolkit reaches a source group or has a reachable built-in. Calls naming an unreached
|
|
1433
|
+
group are refused before guardrails and do not start its server. The first-run init wait is unchanged: it can
|
|
1434
|
+
still block the first turn even when its server's group is unreached; `start: eager` and stale-cache refreshes
|
|
1435
|
+
also still start. `/mcp` marks servers outside the current toolkit.
|
|
1417
1436
|
- **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
|
|
1418
1437
|
that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
|
|
1419
1438
|
without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
|
|
@@ -1422,12 +1441,13 @@ toolkit:
|
|
|
1422
1441
|
fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
|
|
1423
1442
|
characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
|
|
1424
1443
|
(`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
|
|
1425
|
-
- **Saved at creation,
|
|
1444
|
+
- **Saved at creation, definitions resolve at worker start.** A session stores its toolkit as named
|
|
1426
1445
|
(`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
|
|
1427
1446
|
for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
|
|
1428
1447
|
doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
|
|
1429
|
-
`full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The
|
|
1430
|
-
|
|
1448
|
+
`full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
|
|
1449
|
+
text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
|
|
1450
|
+
worker start.
|
|
1431
1451
|
- **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
|
|
1432
1452
|
`kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
|
|
1433
1453
|
the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
|
|
@@ -1435,10 +1455,10 @@ toolkit:
|
|
|
1435
1455
|
tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
|
|
1436
1456
|
call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
|
|
1437
1457
|
starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
|
|
1438
|
-
are declared when a toolkit has a reachable built-in
|
|
1458
|
+
are declared when a toolkit has a reachable built-in or reaches a source group; `full` with no reachable source
|
|
1439
1459
|
declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
|
|
1440
1460
|
- **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
|
|
1441
|
-
built-ins,
|
|
1461
|
+
built-ins, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
|
|
1442
1462
|
(`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
|
|
1443
1463
|
A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
|
|
1444
1464
|
through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
|
|
@@ -1455,10 +1475,13 @@ toolkit:
|
|
|
1455
1475
|
the route: `Error: delegate isn't declared in this session (toolkit lean); call it with call_tool(tool:
|
|
1456
1476
|
"chi/delegate", args: {…}) (find_tools gives its inputSchema).` A removed tool's: `Error: memory_write isn't
|
|
1457
1477
|
loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
|
|
1458
|
-
memory_write).`
|
|
1478
|
+
memory_write).` An MCP tool in an unreached group gets a toolkit-specific refusal before guardrails; a deferred
|
|
1479
|
+
stub for it points to the toolkit instead of suggesting a search. `/toolkit` lists group selectors and the
|
|
1480
|
+
change; the web's toolkit chip says its source-group scope in the tooltip.
|
|
1459
1481
|
- **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
|
|
1460
|
-
turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit`
|
|
1461
|
-
("Changes that restart the prompt cache"), also
|
|
1482
|
+
turn declares it, with its kit's prompt text and group filter; once the session has a model reply, `/toolkit`
|
|
1483
|
+
asks first ("Changes that restart the prompt cache"), also when only the group selectors change and the
|
|
1484
|
+
declared tools stay the same.
|
|
1462
1485
|
- **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
|
|
1463
1486
|
with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
|
|
1464
1487
|
- **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
|
|
@@ -1928,7 +1951,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
1928
1951
|
| `guardrails.mode` | `auto` | | Which rules vote: `auto` leaves out the rules tagged `modes: [strict]` (in the guardrails bundle: `git rebase`, writes and git outside the session's repo); `strict` runs them all. An unknown value warns and is `auto`. config.yml only. See [Guardrails](guardrails.md#modes). |
|
|
1929
1952
|
| `guardrails.small_models` | `auto` | | Which models get the `models: small` rules: `auto` (32B or less by the size in the name, an MoE's active size), a list of globs, or `[]`. config.yml only. See [Guardrails](guardrails.md#rules-for-some-models). |
|
|
1930
1953
|
| `guardrails.parent_answers` | `soft` | | Which options a parent agent (`chi answer`) may pick on a session's question, by their effect: `hard` (a deny, a Stop, a plain answer), `standard` (+ Continue and other spending options), `soft` (+ "Allow once"); never a wider scope or an allow on chi's own config. An invalid value warns and is `hard`. A session's own `parent_answers` (`chi --parent-answers`, `/parent-answers`) may only tighten it. Replaces `guardrails.parent_approvals` and `turn.parent_continue`. config.yml only: no environment variable (a parent can still pick the config dir with `XDG_CONFIG_HOME`). A convention for an honest parent, not a security boundary. See [Guardrails](guardrails.md#answers-from-a-parent-agent). |
|
|
1931
|
-
| `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1). |
|
|
1954
|
+
| `guardrails.system_one.mode` | `off` | | System 1, the cheap judgment that answers an ask before the user sees it: `off` (the default), `audit` (judge every ask, log it, never decide), `on` (also allowed to allow a call once, behind the gates in [System 1](guardrails.md#system-1-experimental)). An unknown value warns and is `off`. Experimental. config.yml only. See [Guardrails](guardrails.md#system-1-experimental). |
|
|
1932
1955
|
| `guardrails.system_one.host` | none | | The `hosts:` entry serving `/v1/systemone` (a local Splash, or the hosted Jev API); unset: no judgment. config.yml only. |
|
|
1933
1956
|
| `guardrails.system_one.model` | none | | The judgment model; unset uses the host's first model, and a host that names none has no judgment. config.yml only. |
|
|
1934
1957
|
| `guardrails.system_one.rules` | none | | The rule ids System 1 may answer for in `on` mode: `"git-push|git-rebase"` or a YAML list, parsed like `guardrails.small_models`. Nothing is auto-allowed while it is unset; the `chi-answer-piped`, `chi-answer-http` and `chi-broadcast` rules never are, however they are listed. config.yml only. |
|
data/docs/guardrails.md
CHANGED
|
@@ -243,6 +243,16 @@ with its `reason`; `chi answer` exits 1 with the line), checked against the
|
|
|
243
243
|
question pending at that moment. Answers from the web and an attached
|
|
244
244
|
terminal keep every option.
|
|
245
245
|
|
|
246
|
+
If `chi answer` reaches a worker that refuses its Bridge secret (`403
|
|
247
|
+
bridge_secret`), it prints a session-specific recovery message rather than
|
|
248
|
+
treating that as a refused answer: `"session ID's worker refused this chi's
|
|
249
|
+
request (it didn't name its secret): a chi web or terminal started before chi
|
|
250
|
+
was updated can't reach it; restart chi web, re-attach with chi --attach ID,
|
|
251
|
+
or restart the worker with chi sessions restart ID"`. `chi sessions restart
|
|
252
|
+
ID` can also recover when the worker cannot be asked, but only if disk shows
|
|
253
|
+
no running turn, pending question or approval, queued prompt, or running
|
|
254
|
+
background task; when one exists, stop it explicitly and then send a prompt.
|
|
255
|
+
|
|
246
256
|
A parent may also type its answer into chi itself: a piped `chi --attach ID`
|
|
247
257
|
or `printf '2\n' | chi --no-shared -p …`. So chi treats an answer as a
|
|
248
258
|
parent's, held to the same setting, when its input isn't a terminal or an
|
|
@@ -662,8 +672,8 @@ With a client, `GuardrailWiring#request_approval` asks System 1 one `noul`
|
|
|
662
672
|
question, `allow`, before the user is asked. Its instruction is a constant
|
|
663
673
|
in code (`SystemOne::ApprovalQuestion::INSTRUCTIONS`), reviewed like a
|
|
664
674
|
prompt; the state is the ask's own facts — the rule and its source, the
|
|
665
|
-
reason, the tool, the command (or the paths) cut to 2000 characters
|
|
666
|
-
edit's diff, the cwd, the repo and branch, and the session's title when it
|
|
675
|
+
reason, the tool, the command (or the paths) cut to 2000 characters
|
|
676
|
+
(`ApprovalQuestion::TEXT_CHARS`), the edit's diff, the cwd, the repo and branch, and the session's title when it
|
|
667
677
|
has one. A memory ask also names what it is about: the entry and its scope,
|
|
668
678
|
and for a removal the description that entry's index line carries
|
|
669
679
|
(`Targets#memory`, read into the card by `Guardrails::Approval`): a path
|
|
@@ -680,11 +690,13 @@ Each judgment is a line in `judgments.jsonl`, beside `approvals.json` in
|
|
|
680
690
|
`$XDG_STATE_HOME/samagotchi/guardrails/` (`SystemOne::Log`): 0600, appended
|
|
681
691
|
under an exclusive `flock` (several workers write it), and rewritten —
|
|
682
692
|
keeping the newest half of its lines — once it passes 5 MB. A line is never
|
|
683
|
-
rewritten to add its label:
|
|
693
|
+
rewritten to add its label. Its `call` is shorter than the state's: cut to 300
|
|
694
|
+
characters (`ApprovalQuestion::CALL_CHARS`), with `call_digest` the SHA256 of
|
|
695
|
+
the whole call:
|
|
684
696
|
|
|
685
697
|
```json
|
|
686
698
|
{"kind":"judgment","id":"…","at":"…","session_id":"…","rule":"git-push","source":"config","tool":"execute","call":"git push origin main","call_digest":"<sha256 of the whole call>","repo_name":"samagotchi","p":0.97,"threshold":0.8,"model":"Qwen3.8-Flash","latency_ms":180,"decision":"audit","origin":"live"}
|
|
687
|
-
{"kind":"label","id":"…","at":"…","answer":"allow_once"}
|
|
699
|
+
{"kind":"label","id":"…","at":"…","answer":"allow_once","question_id":"…"}
|
|
688
700
|
```
|
|
689
701
|
|
|
690
702
|
`decision` is what the judgment led to: `audit` (audit mode), `asked` (on
|
|
@@ -692,7 +704,9 @@ mode, the user was asked), `auto` (on mode, System 1 answered for the user).
|
|
|
692
704
|
A judgment that never arrived is logged too, with `p` null, so the failures
|
|
693
705
|
count as well. The label line, keyed by the judgment's id, is what the user
|
|
694
706
|
did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
|
|
695
|
-
`none` for an ask that was cancelled.
|
|
707
|
+
`none` for an ask that was cancelled. A live label carries the card's
|
|
708
|
+
`question_id` when the user answered (the judgment is written before the card
|
|
709
|
+
exists), so a replay knows the ask was judged. **A parent agent's answer is not the
|
|
696
710
|
user's: its allow labels nothing. Its deny is a label only where the parent
|
|
697
711
|
could have allowed instead** — the ask is not `user_only` and the session's
|
|
698
712
|
parent level permits a permit (`Guardrails::ParentPolicy`); a parent that was
|
|
@@ -704,6 +718,10 @@ their last label.
|
|
|
704
718
|
Everything about the log is best-effort: a write that fails is one
|
|
705
719
|
`Log.warn(:guardrails, "system_one_log_failed", …)` and the approval goes on
|
|
706
720
|
as if System 1 were not there.
|
|
721
|
+
A turn stopped while the judge is asked is not a failed judgment: the cancel
|
|
722
|
+
(`LLM::RequestCancelled`) goes through `SystemOne::Client#ask`, no line is
|
|
723
|
+
written, and the call is denied without a card; a turn that stopped as the
|
|
724
|
+
judgment came back opens no card either.
|
|
707
725
|
|
|
708
726
|
`ruby script/system-one-probe HOST [MODEL]` asks three fixed approval-like
|
|
709
727
|
questions through the client and prints each probability and the latency of
|
|
@@ -722,13 +740,18 @@ gate below passes, in this order:
|
|
|
722
740
|
questions — `chi-answer-piped`, `chi-answer-http`, `chi-broadcast` — which
|
|
723
741
|
are never auto-allowed, even when listed
|
|
724
742
|
(`SystemOne::SelfProtect::NEVER_AUTO`);
|
|
725
|
-
4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`)
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
`
|
|
730
|
-
|
|
731
|
-
|
|
743
|
+
4. the call does not change chi itself (`SystemOne::SelfProtect.refuse?`), by
|
|
744
|
+
the same test that makes an ask one only the user may answer
|
|
745
|
+
(`ParentApprovals.chi_files?`): no edit or write whose paths are in chi's
|
|
746
|
+
config dir (config.yml), the hooks dir, the installed bundles dir, the
|
|
747
|
+
guardrails store (where `approvals.json` and `judgments.jsonl` live) or the
|
|
748
|
+
attached context dir, and no `execute` or `task_create` command naming any
|
|
749
|
+
of them, however it spells them (`ParentApprovals::CHI_TEXT`:
|
|
750
|
+
`$XDG_CONFIG_HOME/samagotchi/config.yml`, `~/.config/samagotchi/…`, a
|
|
751
|
+
`cd ~/.config` then `samagotchi/config.yml`). The memories dir is
|
|
752
|
+
deliberately not one of these: reading or appending a memory file can be
|
|
753
|
+
auto-allowed, unless the command has a `..` segment (`memories/../config.yml`
|
|
754
|
+
climbs out of it). A protected rule
|
|
732
755
|
(`Verdict::PROTECTED_RULES`) may be listed by name and System 1 may answer
|
|
733
756
|
it, but a call that really writes config.yml or the hooks is refused here;
|
|
734
757
|
5. the judgment's `p` is at least `guardrails.system_one.threshold` (0.8);
|
|
@@ -744,11 +767,14 @@ Then the judgment's `decision` is `auto`, the verdict settles as
|
|
|
744
767
|
written to `approvals.json` — the next call is judged again. **No question is
|
|
745
768
|
opened, so no card appears, no `:question_asked` hook fires, and no
|
|
746
769
|
notification is sent.** The verdict's activity carries
|
|
747
|
-
`note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
the
|
|
770
|
+
`note: "auto-allowed by System 1 (p=0.97)"`, `judgment_id` and `p`, and the
|
|
771
|
+
call's tool row shows it: the terminal's tool line ends in
|
|
772
|
+
`· auto-allowed (System 1, p=0.97)`
|
|
773
|
+
(`TerminalUI::Formatting#format_tool_guardrail_suffix`, live and when
|
|
774
|
+
attaching to a saved turn), and the web's activity row carries
|
|
775
|
+
**auto-allowed · System 1** with `rule …, p=…, allowed once` on hover and the
|
|
776
|
+
owner's **should have asked** button (below). The stage trail shows no mark.
|
|
777
|
+
If the judgment line cannot be written to the log, the ask goes to the user.
|
|
752
778
|
|
|
753
779
|
A delegate child answers from its own session's settings: its System 1 can
|
|
754
780
|
auto-allow a call even when the child's `parent_answers: hard` bars a parent
|
|
@@ -778,8 +804,11 @@ whose `pending_question.approval` is there and that have an `answer` or were
|
|
|
778
804
|
`cancelled`. A label is the scope the user took — `allow_once` for "Allow once",
|
|
779
805
|
`allow_wide` for a wider one, `deny` for anything else, `none` for a cancelled
|
|
780
806
|
ask. Skipped: a question no one answered, a relay card, which carries no
|
|
781
|
-
approval of its own,
|
|
782
|
-
running it twice adds nothing
|
|
807
|
+
approval of its own, a question id a previous replay already logged — so
|
|
808
|
+
running it twice adds nothing — and an ask already judged live (audit or on
|
|
809
|
+
mode): by the `question_id` its label carries, or, for a live judgment with
|
|
810
|
+
none (a cancelled ask, an older line), by its session and `call_digest`, one
|
|
811
|
+
card per such line. A parent agent's answer is not the user's: its
|
|
783
812
|
allow is skipped, and its deny is replayed only where the parent could have
|
|
784
813
|
allowed (the card is not `user_only` and the session's parent level permits a
|
|
785
814
|
permit). A cards file that doesn't parse is skipped with one warning line,
|
data/docs/internals/anthropic.md
CHANGED
|
@@ -130,7 +130,7 @@ gets a 400 `messages.N.content.M: Invalid signature in thinking block. The block
|
|
|
130
130
|
conversation.`, and its last sentence says what changed ("The `system` prompt differs …"). chi's history is
|
|
131
131
|
append-only within a process, so what changes the prefix is:
|
|
132
132
|
|
|
133
|
-
1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `
|
|
133
|
+
1. **A system-prompt rebuild** (`SystemPrompt#build` is cached per process and key; `SystemPrompt#reset!` drops it):
|
|
134
134
|
a new process (`--resume`, a respawn after `session.idle_exit_minutes`, a crash) after a memory index line, an
|
|
135
135
|
AGENTS.md, an appended file, a model note or a bundle memory changed; `/model` (the tail's `Model:` line, its
|
|
136
136
|
overlays and notes); `/thinking` (another key); a toolkit or profile change; a plugin's `tools_changed!`; the
|
|
@@ -31,7 +31,7 @@ Behavior:
|
|
|
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
33
|
- `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
|
-
- `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 `
|
|
34
|
+
- `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
35
|
- They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
|
|
36
36
|
|
|
37
37
|
## Finish wake
|
|
@@ -94,7 +94,9 @@ participant** and **one role**, until it expires or is revoked.
|
|
|
94
94
|
block under the lock, and `App#code_identity_ok?` declines unless the trade is proxied with the grant's identity.
|
|
95
95
|
Declined, the code stays and the trade answers `token_bound`, so a forwarded link opened by someone else still
|
|
96
96
|
opens, once, for the account it was sent to. Owner codes and unbound grants' codes are used up whatever the gate
|
|
97
|
-
says, as before.
|
|
97
|
+
says, as before. Owner codes burn on purpose (user decision 2026-10-26, don't "align" them with bound grants): an
|
|
98
|
+
owner code is the whole control plane, so one presented by another identity has leaked and is killed at once;
|
|
99
|
+
the owner prints a new link (spec: proxy_gate_spec "uses up an owner's code someone else presents").
|
|
98
100
|
- **Which identity string.** The grant binds to whatever the header this run trusts carries: through Okta and
|
|
99
101
|
oauth2-proxy the work email (`alex@corp.com`), through Tailscale Serve the Tailscale login (`alex@github`, or a
|
|
100
102
|
private-relay address). The same person is two strings: bind to the one that proxy sends. A request from a
|
|
@@ -185,7 +187,9 @@ answers 429 `too_many_turns` past it; with no worker, their waiting input files
|
|
|
185
187
|
written, under the session dir's `participants.lock` (`SessionManager.deliver_turn`), so two at once can't both
|
|
186
188
|
pass. The ack is 202 with
|
|
187
189
|
`delivery: "queue"`; chi web keeps enqueued id → grant (the last 1000), which `dequeue` and `wait` check, falling
|
|
188
|
-
back
|
|
190
|
+
back after a restart to the author the session's files keep (`ParticipantAccess#turn_author`): the waiting or claimed
|
|
191
|
+
input file's, then `last_turn`'s or the settled record's (a refused turn's, `TurnCheckpoint.record_refused`; one whose
|
|
192
|
+
worker died, `TurnCheckpoint.settle`, from its checkpoint mark's `author`).
|
|
189
193
|
|
|
190
194
|
**The worker.** A participant's input file waits for its own turn whatever its delivery says
|
|
191
195
|
(`SessionInbox.waits_for_turn_end?`), so a drain never merges it into another author's turn; the Bridge queues their
|