samagotchi 0.150.0 → 0.152.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 +50 -1
- data/docs/architecture.md +2 -2
- data/docs/bundle-authoring.md +5 -5
- data/docs/cli.md +32 -7
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +4 -0
- data/docs/internals/block-renderers.md +98 -0
- data/docs/internals/bridge.md +53 -17
- data/docs/internals/bundles.md +49 -14
- data/docs/internals/objectives.md +6 -4
- data/docs/internals/web-frontend.md +2 -2
- data/docs/memory.md +7 -2
- data/docs/plugins.md +91 -14
- data/docs/releasing.md +7 -2
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/block_renderers.rb +116 -0
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +219 -122
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/check.rb +43 -3
- data/lib/samagotchi/bundle_command/installed_check.rb +6 -2
- data/lib/samagotchi/bundle_command/try/sandbox.rb +11 -3
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +88 -10
- data/lib/samagotchi/bundle_needs.rb +4 -6
- data/lib/samagotchi/bundle_settings_check.rb +14 -13
- data/lib/samagotchi/bundles/agents-md/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +3 -3
- data/lib/samagotchi/child_env.rb +15 -0
- data/lib/samagotchi/context_providers.rb +42 -10
- data/lib/samagotchi/engine.rb +47 -6
- data/lib/samagotchi/extension_load.rb +4 -3
- data/lib/samagotchi/guardrail_wiring.rb +28 -19
- data/lib/samagotchi/guardrails/approval.rb +2 -2
- data/lib/samagotchi/guardrails/rules.rb +1 -0
- data/lib/samagotchi/guardrails/verdict.rb +20 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +5 -7
- data/lib/samagotchi/memory_bundle/asset_installer.rb +18 -9
- data/lib/samagotchi/memory_bundle/builder.rb +39 -31
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +3 -2
- data/lib/samagotchi/memory_bundle/file_digest.rb +31 -0
- data/lib/samagotchi/memory_bundle/file_standing.rb +2 -2
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +55 -43
- data/lib/samagotchi/memory_bundle/installed_copy.rb +38 -14
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +224 -0
- data/lib/samagotchi/memory_bundle/installer.rb +91 -16
- data/lib/samagotchi/memory_bundle/manifest.rb +166 -5
- data/lib/samagotchi/memory_bundle/manifest_standing.rb +171 -0
- data/lib/samagotchi/memory_bundle/merger.rb +7 -8
- data/lib/samagotchi/memory_bundle/profile.rb +3 -2
- data/lib/samagotchi/memory_bundle/provenance.rb +220 -19
- data/lib/samagotchi/memory_bundle/shas.rb +4 -3
- data/lib/samagotchi/memory_bundle/shipped_update.rb +52 -24
- data/lib/samagotchi/memory_bundle/status.rb +11 -10
- data/lib/samagotchi/memory_bundle/system_bundle.rb +13 -2
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_config.rb +5 -4
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/system_one/record.rb +10 -2
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/tools/delegate.rb +20 -2
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -3
- data/lib/samagotchi/update_command.rb +13 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +24 -13
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +2 -0
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +9 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +2 -2
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/session_access_routes.rb +2 -1
- data/lib/samagotchi/web/session_hub.rb +6 -0
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d208a5aecb9745a21107ab97399c7ec4901c24fd781e3ba4ec1658a69764ded
|
|
4
|
+
data.tar.gz: abc21f8bfe39f0bdc614b70b755dd3cd195d016dd305b6d3ba28b018d0b75d54
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1105443bb7097fd93f0ab4bb1f0f9926fbbc9c89f9d57584d12ae3ac560b726498c607c1e897416e8a17f0e150076fc7de2b2b19b0919b49ad5b40645dd3883a
|
|
7
|
+
data.tar.gz: ba009b4669d67eb24ccd15cf669f84431a1738153b721c13c18633d6b05f32cdd4bfd01d25530b18ef5ab39cbf84e0b19688691f6751884c92e0a8d412935771
|
data/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,55 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
5
5
|
versions follow [Semantic Versioning](https://semver.org/); before 1.0, config
|
|
6
6
|
and commands may change between minor versions. How releases are made:
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
|
+
|
|
8
9
|
## [Unreleased]
|
|
9
10
|
|
|
11
|
+
## [0.152.0] - 2026-11-06
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `chi bundle install` and `chi bundle check` name a manifest key this chi doesn't read (`unknown-key`); `chi bundle check` also warns (`requires-chi-floor`) when a manifest uses a key newer than its `requires_chi` admits. [docs/plugins.md](docs/plugins.md)
|
|
16
|
+
- `chi bundle status NAME` has a `Manifest:` line saying where the bundle's manifest is read from, and why.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- An installed bundle keeps its `manifest.yml` and chi reads it live, so a newer chi reads keys the installing chi didn't know, with no reinstall. Bundles installed earlier get theirs on `chi update` (shipped bundles, or a folder or archive that is still there; the system bundle at the next start); for one from git, `chi update` names the `chi bundle upgrade NAME` to run. [docs/internals/bundles.md](docs/internals/bundles.md)
|
|
21
|
+
- `chi bundle install` and `chi bundle upgrade` refuse a bundle whose `requires_chi` this chi doesn't meet, and write nothing; `--force` installs it anyway. [docs/plugins.md](docs/plugins.md)
|
|
22
|
+
- `chi bundle upgrade NAME` upgrades a bundle from a folder, archive or git URL from the source its record keeps: the name alone is enough.
|
|
23
|
+
- "Stable for bundles" now covers the manifest schema. [docs/plugins.md](docs/plugins.md)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `chi bundle diff NAME FILE` shows the bundle's new text for a file that stays behind, so the sections the "stays behind" line names can be read.
|
|
28
|
+
- `chi bundle try` no longer passes the user's dropped settings (guardrails, memories, hooks, log…) to its sandboxed chi through the environment.
|
|
29
|
+
- A bundle's `context_providers:` are skipped when this chi is older than the bundle's `requires_chi`.
|
|
30
|
+
- Reinstalling a bundle from the same source says what it did: `Up to date:` for unchanged files, and a last line naming what was rewritten and which of your memories were kept.
|
|
31
|
+
- A `files:` entry with no checksum no longer warns "Checksum mismatch" on every install.
|
|
32
|
+
- Objectives proposal cards no longer appear after a canceled or failed turn.
|
|
33
|
+
- A `delegate` child in a linked worktree is no longer told to re-delegate to the main checkout.
|
|
34
|
+
|
|
35
|
+
Update with `chi update` (agents-md 0.1.2, btw 0.1.4, loop-guard 0.3.10, mcp 0.7.6, skills 0.1.9, slow-commands 0.2.5: their `requires_chi` now names the chi that reads their settings).
|
|
36
|
+
|
|
37
|
+
## [0.151.0] - 2026-11-05
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- A bundle can declare `block_renderers:` in its manifest: a fenced code block's language and the `scripts/` files that draw it in chi web. See docs/plugins.md (Block renderers).
|
|
42
|
+
- chi web draws such a block as a picture in answers and closed steps, once a bundle for its language is installed and `web.markdown` is on. A toggle shows the source; a block that can't be drawn stays source, with one muted line. The renderer runs in a sandboxed frame with no access to the page or its cookie, and a CSP that closes its network but for WebRTC.
|
|
43
|
+
- [chi-mermaid](https://github.com/dm1try/chi-mermaid), a separate bundle, draws ```` ```mermaid ```` blocks this way and tells the model it can write them: `chi bundle install https://github.com/dm1try/chi-mermaid`.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- After a reload, a card a command showed names it (`from /short`) in the web, since the command's line isn't there any more.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- A card or notice keeps its place in the web after a reload, however many turns came after it: a finished task's or an attached context's wake no longer sends it above earlier prompts, and a reminder or a Continue no longer pushes it toward the bottom. A card now stays with the turn it came after, also through a `!rollback` or a restart. A session's existing cards keep the places they have. Going back to an older chi on a session afterwards clears that session's cards.
|
|
52
|
+
- A saved recap sits where it belongs in the web after a task or context wake, or a `!cmd`.
|
|
53
|
+
- A card shown after a failed turn stays below that turn's prompt after a reload, as it was live.
|
|
54
|
+
|
|
55
|
+
Update with `chi update`.
|
|
56
|
+
|
|
10
57
|
## [0.150.0] - 2026-11-05
|
|
11
58
|
|
|
12
59
|
### Changed
|
|
@@ -5670,7 +5717,9 @@ and long-lived sessions.
|
|
|
5670
5717
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5671
5718
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5672
5719
|
|
|
5673
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5720
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.152.0...HEAD
|
|
5721
|
+
[0.152.0]: https://github.com/dm1try/samagotchi/compare/v0.151.0...v0.152.0
|
|
5722
|
+
[0.151.0]: https://github.com/dm1try/samagotchi/compare/v0.150.0...v0.151.0
|
|
5674
5723
|
[0.150.0]: https://github.com/dm1try/samagotchi/compare/v0.149.0...v0.150.0
|
|
5675
5724
|
[0.149.0]: https://github.com/dm1try/samagotchi/compare/v0.148.0...v0.149.0
|
|
5676
5725
|
[0.148.0]: https://github.com/dm1try/samagotchi/compare/v0.147.0...v0.148.0
|
data/docs/architecture.md
CHANGED
|
@@ -210,7 +210,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
|
|
|
210
210
|
| UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
|
|
211
211
|
| 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"). |
|
|
212
212
|
| Bridge (SSE/HTTP) ([internals/bridge.md](internals/bridge.md)) | `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`). |
|
|
213
|
-
| Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie, on loopback a value derived from the token (`Token.loopback_cookie`), or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'
|
|
213
|
+
| Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request needs the access token, this machine's on loopback too (`App#token_gate`: the `chi_token` cookie, on loopback a value derived from the token (`Token.loopback_cookie`), or `Authorization: Bearer`; chi web prints a `Local:` link with a one-time code; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'; frame-src 'none'`, so a page's own frames can't navigate either), 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. |
|
|
214
214
|
| Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
|
|
215
215
|
|
|
216
216
|
- `bin/chi` in REPL mode (see `LaunchMode` above) builds `TerminalUI`. `TerminalUI#run` is the single
|
|
@@ -632,7 +632,7 @@ A map of the parts the overview above doesn't name (paths under `lib/samagotchi/
|
|
|
632
632
|
| LLM context strategies | `llm_context_strategy.rb`, `llm_context_view.rb`, `llm_context_stale.rb`, `llm_context_forget.rb` | What a request sends of the history (none, stale, forget) ([internals/llm-context-forget.md](internals/llm-context-forget.md)). |
|
|
633
633
|
| Session chains | `session_continue.rb` (`SessionContinue`), `session_chain.rb` (`SessionChain`) | A session that continues an earlier one. `SessionChain.opener_for` gives the text web `continue →` prefills in the new link: the previous link's own first typed message when that link continues one itself, else nil (the 201's `opener`; not saved) ([sessions.md](sessions.md)). |
|
|
634
634
|
| Review sessions | `review_start.rb` (`ReviewStart`), `session_review.rb` (`SessionReview`, the session's `review` field), `review_command.rb` (`ReviewCommand`, `chi review`), `review_apply.rb` (`ReviewApply`) | A session made to be shared: idle, its subjects project files checked by `Guardrails::ReviewPaths`, a fork of a working session from its recap (`--from`); Apply quotes a participant's review turn into the owner's composer ([sessions.md](sessions.md#review-sessions), [internals/participants.md](internals/participants.md)). |
|
|
635
|
-
| chi web's routes | `web/app.rb` (`Web::App`: the gate, `ROUTES`, the shared helpers), `web/message_display.rb` (`Web::MessageDisplay`: saved messages and cards as display items, `POST /api/render`), `web/session_show.rb` (`Web::SessionShow`: `GET /api/sessions/:id` with its `cards`, `tail` and `timing` re-reads, `GET …/output`, the timing payload), `web/session_create.rb` (`Web::SessionCreate`: `POST /api/sessions`, a continue, a review fork), `web/session_control.rb` (`Web::SessionControl`: answer, command, turn and images, cancel, stop, restart, archive, delete), `web/push_routes.rb` (`Web::PushRoutes`: `/api/push`, its subscriptions, prefs, test and presence) | The handlers `App`'s `ROUTES` name, in the file that holds their kind; each mixin is included in `App` and reads its helpers and ivars, so `app.send(:handle_show, …)` and the specs that stub `App` methods work as before. |
|
|
635
|
+
| chi web's routes | `web/app.rb` (`Web::App`: the gate, `ROUTES`, the shared helpers), `web/message_display.rb` (`Web::MessageDisplay`: saved messages and cards as display items, `POST /api/render`), `web/session_show.rb` (`Web::SessionShow`: `GET /api/sessions/:id` with its `cards`, `tail` and `timing` re-reads, `GET …/output`, the timing payload), `web/session_create.rb` (`Web::SessionCreate`: `POST /api/sessions`, a continue, a review fork), `web/session_control.rb` (`Web::SessionControl`: answer, command, turn and images, cancel, stop, restart, archive, delete), `web/push_routes.rb` (`Web::PushRoutes`: `/api/push`, its subscriptions, prefs, test and presence), `web/block_renderer_routes.rb` (`Web::BlockRendererRoutes`: `GET /api/block_renderers` and one renderer's file, [internals/block-renderers.md](internals/block-renderers.md)) | The handlers `App`'s `ROUTES` name, in the file that holds their kind; each mixin is included in `App` and reads its helpers and ivars, so `app.send(:handle_show, …)` and the specs that stub `App` methods work as before. |
|
|
636
636
|
| Web Push | `web/push.rb` (`Web::Push`: `Runtime`, `preflight!`, the `web.push.*` settings), `web/push/crypto.rb` (`Push::Crypto`: RFC 8291 aes128gcm, RFC 8292 VAPID), `web/push/store.rb` (`Push::Store`, `Store::Sub`: `web-push/vapid.pem`, `subscriptions.json`), `web/push/client.rb` (`Push::Client`, `Client::Result`), `web/push/attention.rb` (`Push::Attention`: notify.js's rules in Ruby, shared cases in `spec/fixtures/attention_cases.json`), `web/push/presence.rb` (`Push::Presence`: the pages' "in front" reports), `web/push/notifier.rb` (`Push::Notifier`, a hub subscriber with a sender thread), `web/push_routes.rb` (`Web::PushRoutes`), `web/private_files.rb` (`Web::PrivateFiles`, shared with the grants), `public/sw.js`, `public/push.js`, `public/manifest.webmanifest` | chi web pushes what the tab notifies for (approvals, questions, step-limit questions, cards, failed turns; done turns to a device that asks) to the owner's Home Screen app over an https public URL: the Notifier turns the hub's new attentions into show pushes after a hold, unless a chi web page is in front, at most 30 an hour, with one retry and a restart catch-up (`sent.json`) (each carries the keys still open, and the badge; no close push: every push shows a notification); the bell (`notifications.js`) subscribes the device, and the service worker and the page close the answered ones. See [internals/web-push.md](internals/web-push.md). |
|
|
637
637
|
| chi web's updates | `web/update_check.rb` (`Web::UpdateCheck`), `web/update_ops.rb` (`Web::UpdateOps`), `web/auto_update.rb` (`Web::AutoUpdate`) | The RubyGems checker and its `update_check.json`; the install, worker-restart and chi-web-restart operations (one at a time) the owner's routes and AutoUpdate share; `update.auto: install`'s thread (install when no turn runs, then idle workers and chi web). See [configuration](configuration.md#unattended-updates-updateauto). |
|
|
638
638
|
| Behind an auth proxy | `web/public_url.rb` (`Web::PublicUrl`: `web.public_url`, `web.proxy.identity_header`, `web.proxy.owner`), `web/proxy_gate.rb` (`Web::ProxyGate`, mixed into App; `RawHeaderNames`, prepended to WEBrick's request by `Web::Server`), `web/link_codes.rb` (`Web::LinkCodes`: the links' one-time codes), `proxy_trace.rb` (`ProxyTrace`), `web/public/token_fragment.js`, `web/public/auth_lost.js`, `web/tunnel.rb` + `web/tunnel/tailscale.rb` (`Web::Tunnel`, `Web::Tunnel::Tailscale`: `chi web --tunnel tailscale`, Tailscale Serve set for one run and the PublicUrl it makes) | Experimental: chi web behind an auth proxy. Every request needs the token; a proxied one's owner also needs the identity header (one clean value under one name); the `__Host-` cookie, an absolute `Location` from the public URL, https links with a one-time code in the fragment; the page's lost-sign-in banner. Bridges refuse proxy traces ([configuration.md](configuration.md#behind-an-auth-proxy)). |
|
data/docs/bundle-authoring.md
CHANGED
|
@@ -60,7 +60,7 @@ plugin:
|
|
|
60
60
|
`bundles: my-bundle:` as a Hash with string keys, once, when the plugin loads. A value a session may set
|
|
61
61
|
(`chi --set my-bundle.KEY=V`, listed in the manifest's `session_settings:`, typed with `{KEY: number}` so a
|
|
62
62
|
value of another type is refused) is read from `ctx.settings` at call time. List what config.yml may set in `settings:`
|
|
63
|
-
(`[greeting]`, or `{greeting: string}`): a typo there then warns. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting.
|
|
63
|
+
(`[greeting]`, or `{greeting: string}`): a typo there then warns. `chi bundle check --set KEY=VALUE` gives the sandboxed load a setting. A bundle can also draw a fenced block in chi web's answers, with no plugin code: `block_renderers:` and a script calling `chi.defineRenderer` ([plugins.md](plugins.md#block-renderers)).
|
|
64
64
|
|
|
65
65
|
## What a hook sees
|
|
66
66
|
|
|
@@ -129,10 +129,10 @@ class, a hand-built `ctx`), that is a gap in the kit: say so to the user rather
|
|
|
129
129
|
- **Shas.** Every file the manifest names has a sha256 line, and an edit makes it stale: the install then warns
|
|
130
130
|
of a checksum mismatch, and a line with no sha installs unchecked. `chi bundle check --fix` rewrites them; run it
|
|
131
131
|
after the last edit.
|
|
132
|
-
- **requires_chi.** Use the chi the user runs (`chi --version`)
|
|
133
|
-
newer than the floor (`ctx.http`
|
|
134
|
-
|
|
135
|
-
0.
|
|
132
|
+
- **requires_chi.** Use the chi the user runs (`chi --version`): a bundle whose `requires_chi` that chi doesn't
|
|
133
|
+
meet is now **refused at install** (`--force` is the only way past it), not just unloaded. `check` warns when the plugin calls something newer than the floor (`ctx.http` 0.80.0, `ctx.cost` 0.83.0, `ctx.usage` 0.100.0, `ctx.sessions.children` 0.34.0),
|
|
134
|
+
or a keyword added later (`ctx.ask_model(model:)` 0.80.0, `ctx.sessions.send(delivery:)` 0.64.0, `chi.toolkit(prompt:)` 0.72.0, `chi.toolkit(groups:)` 0.121.0), written on `ctx` / `chi` itself rather than through a variable,
|
|
135
|
+
or when the manifest itself uses a key newer than the floor (`block_renderers:` 0.151.0, `settings:` 0.143.0, `session_settings:` 0.84.0, `context_providers:` 0.32.0): an older chi ignores that key.
|
|
136
136
|
- **A typo in an API call.** `check` warns (`api`) on a call or keyword this chi lacks, on any path; for a newer chi on purpose, guard it with `ctx.respond_to?(:name)`.
|
|
137
137
|
- **trust_level.** Leave it out. `reviewed` / `trusted` are for someone who reviewed the bundle, not its author.
|
|
138
138
|
- **Restart.** A session loads bundles when it starts: after an install or a settings change, the user starts a
|
data/docs/cli.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
- `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `big memories` the memories over `memory.entry_warn_tokens` ([Memory](memory.md#a-memorys-size)), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
|
|
37
37
|
- `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, and check the bundles from elsewhere, in one table (see [Updating](#updating))
|
|
38
38
|
- `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
|
|
39
|
-
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
|
|
39
|
+
- `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `upgrade <name>` upgrades an installed bundle by name from the source it came from (its folder, archive or git URL), `upgrade <source>` from a new one; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Bundles](bundles.md#the-btw-bundle), [the mcp bundle](bundles.md#the-mcp-bundle) [the loop-guard bundle](bundles.md#the-loop-guard-bundle), [the check-in bundle](bundles.md#the-check-in-bundle) and [the skills bundle](bundles.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). `install`/`upgrade` `--force` overwrites existing entries and also installs a bundle whose `requires_chi` this chi doesn't meet; without it such a bundle is refused (exit 1), nothing written. A plain `install` of a bundle that is already installed from the same source rewrites its hooks, rules, plugin, scripts and record: the summary lists a file that already holds the bundle's bytes as `Up to date: …`, one you wrote as `Skipped: …`, and ends with `NAME was already installed from SOURCE: its hooks, rules, plugin and scripts now match it and its record was rewritten` (plus, when a memory was kept, `your own copies of its memories were kept (chi bundle upgrade NAME merges the bundle's changes into them)`). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
|
|
40
40
|
|
|
41
41
|
- `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
|
|
42
42
|
- `chi guardrails rule <id>` — print exactly `asks` when that rule is loaded, not disabled and votes `ask` in this `guardrails.mode` for every model; any other line says why not (no such rule, disabled, another mode, it denies, for some models only, rules that failed to load, `guardrails.enabled: false`). Exit 0 either way, 2 for a usage error. It reads config.yml and the installed bundles as a worker does (`GuardrailWiring#rules`) and takes the rule's id, never a command; the [coordinator skill](bundles.md#the-coordinator-bundle) reads it before it merges without asking
|
|
@@ -133,8 +133,10 @@ The loop for a bundle you're writing (the whole path: [bundle-authoring.md](bund
|
|
|
133
133
|
chi's Ruby (minitest is one of chi's gems; rspec is optional). The tests run the bundle's code as written, with
|
|
134
134
|
your environment (the kit sandboxes each test's HOME and XDG dirs, not the process).
|
|
135
135
|
- `chi bundle install --dev PATH` installs the bundle in the local directory `PATH` (not a git URL, archive or name:
|
|
136
|
-
exit 2) as a link to it: its plugin, hooks, rules and
|
|
137
|
-
next worker or `chi web` restart with no re-install
|
|
136
|
+
exit 2) as a link to it: its plugin, hooks, rules, scripts and `manifest.yml` are `PATH`'s own files, so an edit applies at the
|
|
137
|
+
next worker or `chi web` restart with no re-install (a manifest edit to `block_renderers:` shows in chi web's next
|
|
138
|
+
request; a new `scripts:`, `hooks:` or `plugin:` entry needs `chi bundle install --dev PATH` again, since the record
|
|
139
|
+
has no entry for it), and their sha256 isn't checked, for this bundle only (the
|
|
138
140
|
record says `dev`; every other bundle is checked as before). Its memories are copied at install. `chi bundle
|
|
139
141
|
status` shows `NAME vX dev → /abs/path`. `chi bundle upgrade NAME` refuses it (it follows its source already; to
|
|
140
142
|
leave dev mode: `chi bundle uninstall NAME`, then `chi bundle install` without `--dev`), as does a plain install
|
|
@@ -147,9 +149,13 @@ The loop for a bundle you're writing (the whole path: [bundle-authoring.md](bund
|
|
|
147
149
|
so run it (and `test`) on code you'd install. The codes, in order: `manifest` (a parse error, verbatim; the run
|
|
148
150
|
stops), `missing-file`, `unlisted` (warn: a file an install leaves out, by `MemoryFiles`' rules, so a README
|
|
149
151
|
isn't one), `sha` (a stale or placeholder sha, or a plugin / hook / script entry with no sha line; `--fix`
|
|
150
|
-
rewrites the first two and prints `fixed sha FILE → sha256:…`), `
|
|
152
|
+
rewrites the first two and prints `fixed sha FILE → sha256:…`), `unknown-key` (warn: a manifest key this chi's
|
|
153
|
+
parser never reads, so it ignores it, one row per key at its line; the install keeps the manifest, so a newer
|
|
154
|
+
chi reads the key from it with no reinstall, and `chi update`'s re-check names it too), `requires-chi` (this chi doesn't satisfy it:
|
|
151
155
|
error; missing on a bundle with code: warn), `requires-chi-floor` (warn: the plugin calls an API newer than the
|
|
152
|
-
oldest chi `requires_chi` admits, from `Plugin::Api::SINCE
|
|
156
|
+
oldest chi `requires_chi` admits, from `Plugin::Api::SINCE`, or the manifest itself uses a key newer than it, from
|
|
157
|
+
`Manifest::SINCE`; an installed bundle gets the unknown-key row but never the floor, as it's the author's fix),
|
|
158
|
+
`trust` (warn: `trust_level: reviewed|trusted` on a
|
|
153
159
|
bundle chi doesn't ship), `clash` (warn: a name chi ships, or one installed from elsewhere; the sandboxed install
|
|
154
160
|
then forces), `install` (each Installer warning, a `needs` one as warn, and each plugin or hook load failure at
|
|
155
161
|
the source `file:line`; skipped while a placeholder sha is unfilled), `registered` (info: each command, tool,
|
|
@@ -313,7 +319,7 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
|
|
|
313
319
|
file in the bundle trash and `merged 0.8.0 into your FILE` in the row. A real
|
|
314
320
|
conflict, no `git`, or no recorded base keeps your file untouched, and the row
|
|
315
321
|
says `FILE stays behind 0.8.0:` with why and what it missed (up to three headings
|
|
316
|
-
and a count, or `2 places`). `chi bundle diff NAME FILE` shows it; `chi bundle
|
|
322
|
+
and a count, or `2 places`). `chi bundle diff NAME FILE` shows it (the base, your file, then the bundle's new text); `chi bundle
|
|
317
323
|
upgrade NAME --force` takes the bundle's, with a copy of yours in the bundle
|
|
318
324
|
trash first. A later run retries such a file at the same version: its row reads
|
|
319
325
|
`0.8.0 merged 0.8.0 into your FILE` when it merges, and names it as behind when
|
|
@@ -367,6 +373,26 @@ Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install
|
|
|
367
373
|
update` again). A failed gem install checks against this chi, since it stays.
|
|
368
374
|
- `--no-bundles` (`update.bundles: false`) leaves these out too: no check, no row
|
|
369
375
|
but `bundles skipped`.
|
|
376
|
+
- **Bundles installed by an older chi** have no kept `manifest.yml` (it didn't
|
|
377
|
+
keep it), so chi reads what their install recorded, and a manifest
|
|
378
|
+
key that chi didn't know (`block_renderers:` before 0.151.0) is missing. Where
|
|
379
|
+
chi has the bundle's own manifest at the installed version, the update keeps it,
|
|
380
|
+
once: a shipped bundle from chi's shipped copy (`up to date (kept its manifest)`;
|
|
381
|
+
the system bundle at the next chi start), a bundle from a folder or a zip/tar
|
|
382
|
+
that is still there from it (a `kept its manifest` row; the archive is extracted
|
|
383
|
+
and the extract removed). Only when the source is what's installed: the same
|
|
384
|
+
name and version, the same manifest values as the install recorded (a key the
|
|
385
|
+
older chi didn't know aside), and the same memories, plugin, hooks, scripts and rules. A git source
|
|
386
|
+
isn't cloned: the row names the command,
|
|
387
|
+
`installed by an older chi, which didn't keep its manifest: chi bundle upgrade NAME reads it again from URL`.
|
|
388
|
+
A source that's gone or doesn't match gets its own row (why, and what to run: for
|
|
389
|
+
a gone one, `NAME's recorded source S is gone: chi bundle upgrade <path or URL of a copy>`). The
|
|
390
|
+
same goes for a record an older chi rewrote after a newer one kept the manifest
|
|
391
|
+
(`an older chi rewrote its record after the install, so its manifest.yml may be an
|
|
392
|
+
earlier version's`). `--dry-run` says `would keep its manifest` and writes nothing.
|
|
393
|
+
An edited `manifest.yml` isn't kept over: its row says so (`chi bundle upgrade
|
|
394
|
+
NAME`). These rows are warnings and fail nothing; `chi bundle status NAME` says the
|
|
395
|
+
same on its `Manifest:` line.
|
|
370
396
|
- **Profiles** (`core`, `dev`): an installed one installs the bundles a new
|
|
371
397
|
chi added to it (`updated (+ name)`), even at the same version, and retries
|
|
372
398
|
one that failed before; one you uninstalled stays out. See [Bundle
|
|
@@ -907,7 +933,6 @@ the model's `objectives` tool); [Objectives](sessions.md#objectives) has what th
|
|
|
907
933
|
- `/objectives done O1 [O2…]`, `/objectives drop O1 [O2…]`: mark items. An id it doesn't have refuses the whole line, and
|
|
908
934
|
so does `done` on a dropped item. The model is shown a done item as `O1 [done] …` and no dropped one.
|
|
909
935
|
- `/objectives undo O1 [O2…]`: back to active, after a mistaken `done` or `drop`.
|
|
910
|
-
|
|
911
936
|
- `/objectives check O1`: an independent read of O1's latest move, one side request; the verdict comes as a card and as
|
|
912
937
|
`check: …` under the move. Information only: it edits nothing, and it is refused for an objective with no move or a dropped one, and during a participant's turn. See [Objectives](sessions.md#objectives).
|
|
913
938
|
|
data/docs/configuration.md
CHANGED
|
@@ -1892,7 +1892,7 @@ What changes once `web.public_url` is set:
|
|
|
1892
1892
|
and a crafted link could swap your session): a link's one-time code and the 401 page's paste box do it, the paste
|
|
1893
1893
|
box's redirect going to the public URL (never to a client-sent `X-Forwarded-Host`). The page accepts `POST`s from the public `https://` origin only (a sibling port on the same
|
|
1894
1894
|
domain, or `http://`, is refused). No page of chi web's may be framed by another (`X-Frame-Options: DENY`,
|
|
1895
|
-
`frame-ancestors 'none'`, in every mode).
|
|
1895
|
+
`frame-ancestors 'none'`, in every mode), and frames inside its pages can't navigate (`frame-src 'none'`).
|
|
1896
1896
|
- Links carry a **one-time code** in the fragment, never a token: `chi web` prints `Proxied: https://…/#code=chc_…`
|
|
1897
1897
|
(the owner's: once, within 10 minutes; `chi web` again prints a new one), and a participant's link (`chi share`,
|
|
1898
1898
|
the Invite dialog) is `https://<public url>/#code=chc_…&s=ID` (once, until the grant expires). A fragment isn't sent
|
data/docs/guardrails.md
CHANGED
|
@@ -1157,6 +1157,10 @@ the whole call:
|
|
|
1157
1157
|
|
|
1158
1158
|
`decision` is what the judgment led to: `audit` (audit mode), `asked` (on
|
|
1159
1159
|
mode, the user was asked), `auto` (on mode, System 1 answered for the user).
|
|
1160
|
+
When two rules ask about one call the line's `rule` is the one the card names
|
|
1161
|
+
and `other_rules` lists the rest (`Verdict#other_ask_rules`), so a second
|
|
1162
|
+
rule's record (`SystemOne::Record`) counts the ask too; a line with one rule
|
|
1163
|
+
has no `other_rules` at all.
|
|
1160
1164
|
A judgment that never arrived is logged too, with `p` null, so the failures
|
|
1161
1165
|
count as well. The label line, keyed by the judgment's id, is what the user
|
|
1162
1166
|
did: `allow_once`, `allow_wide` (any scope wider than once), `deny`, or
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Block renderers: bundles that draw fenced blocks in chi web
|
|
2
|
+
|
|
3
|
+
A bundle's JS turns a fenced block in an answer (```` ```mermaid ````) into a picture in the owner's page. No tool, no
|
|
4
|
+
toolkit. The bundle's side is [plugins.md](../plugins.md#block-renderers); this page is the mechanism and the security
|
|
5
|
+
model. The TUI is untouched: it shows a fence as source.
|
|
6
|
+
|
|
7
|
+
## The path
|
|
8
|
+
|
|
9
|
+
1. `MarkdownRenderer` keeps the language: syntect drops it from the HTML, so a second parse walks the `code_block` nodes
|
|
10
|
+
in document order and zips them with the fragment's `<pre>`s, setting `data-lang` after sanitize's walk. Only the first
|
|
11
|
+
word, downcased, matching `BlockRenderers::LANG`. A count that differs sets nothing. **`web.markdown` off means no
|
|
12
|
+
`<pre data-lang>`, so nothing draws.**
|
|
13
|
+
2. `BlockRenderers.for_installed` (manifest `block_renderers:`, read through `InstalledBundle#manifest`: the bundle's
|
|
14
|
+
kept `manifest.yml`, else its record's mirror, so chi web needs no plugins, and a bundle installed by a chi that
|
|
15
|
+
didn't know the key draws once a kept manifest is there) lists
|
|
16
|
+
who draws what: `GET /api/block_renderers`, `:owner`. `GET /api/block_renderers/:bundle/:file` serves a file, `:owner`:
|
|
17
|
+
the URL's names go through `BlockRenderers.script_path` (membership in a listed renderer's `load` before any path is
|
|
18
|
+
made), then `Provenance.sha_matches?`; a changed file is 409 `renderer_changed`. ETag + `no-cache`, as static files.
|
|
19
|
+
3. `block_host.js` (`createBlockHost`, wired in `app.js`; `participant.js` isn't): one `MutationObserver` per turn root,
|
|
20
|
+
filtered to `TEXTS` (`text_roots.js`), so cards, the asking slot, question replies and delegate reports never draw, and
|
|
21
|
+
every path that adds a block to a root is seen the same way. One `IntersectionObserver` per scroller, with an explicit `root`
|
|
22
|
+
(with `root: null`, `rootMargin` doesn't pass an `overflow: auto` in between). Closed `<details>` fire on open. **A
|
|
23
|
+
move isn't over when it is seen:** a block waiting to come near is watched by its scroller's observer, so one that moves
|
|
24
|
+
to another scroller (the stage's `.ts-scroll` hands a long answer to `#history`) is watched there, or it never draws.
|
|
25
|
+
4. `block_frame.js` (`createBlockFrame`): one hidden frame per language, made on first need, one render at a time, 15 s per
|
|
26
|
+
call. A timeout, a dead frame or a session switch drops it. Handshake `loaded` → `code {files}` → `ready | failed`,
|
|
27
|
+
then `render {id, source, theme}` → `done {id, svg} | error {id, message}`.
|
|
28
|
+
5. On success a `<figure class="block-drawn">` goes right before the `<pre>`, which stays in the DOM, `hidden`, so whatever
|
|
29
|
+
reads text (copy, a claim's walk, fold thresholds) sees what it did. **The DOM is the state** (`block_source.js`): a Revise
|
|
30
|
+
clone arrives with both. On failure the `<pre>` stays, marked `data-block-failed`, with one muted line: an attribute CSS
|
|
31
|
+
shows, never markup, and not in the bubble's `textContent`, which the page's dedupe and an annotation read (`copy.js`'s rule). CSS content can't be selected, so a click on the line copies its words.
|
|
32
|
+
The toggle has an icon and an `aria-label`, no text (`copy.js`'s rule). A jump into the source shows it (`revealInHistory`).
|
|
33
|
+
6. Cache: a `Map` on `lang + theme + source`, about 200 entries, holding the `data:` URL; a later `bubble.innerHTML` wipes the
|
|
34
|
+
figure and the observer redraws in the same pass. No `crypto.subtle`: it's undefined on a plain-http LAN address. A system
|
|
35
|
+
theme change redraws through the figure (a hidden `<pre>` never intersects).
|
|
36
|
+
7. The view: a viewer at the bottom stays there (`scroll.js keepFollowing`); a block drawn above a reader's view keeps
|
|
37
|
+
what they read in place (checked in Chromium and WebKit).
|
|
38
|
+
8. **A maximum picture, in the page, for any renderer** (`block_host.js oversize`): 50,000 px a side and 100M px. Mermaid's
|
|
39
|
+
secure keys don't cover diagram sizes, so a model's `%%{init:{"sequence":{"height":50000000}}}%%` otherwise gives an image
|
|
40
|
+
33M px tall, which breaks the session's scrolling. Eval 0012's real diagrams reach about 2,600 px a side and 3.5M px;
|
|
41
|
+
a chain of 240 edges or a sequence of 1,000 messages still fits. **The decoded and displayed size is what counts.** The root's box comes
|
|
42
|
+
from `DOMParser`, never a regex over model-influenced XML (exponents, a comment holding `<svg`, a `>` in an attribute, entities and
|
|
43
|
+
prefixes all fooled one), and a vast one isn't decoded. Then the decoded size is checked: over the maximum, or a width or
|
|
44
|
+
height of 0 (WebKit's 0 x 150 for a viewBox it can't hold), the block stays source with its line. `.block-img`'s CSS
|
|
45
|
+
`max-height` and natural `max-width` say the same number, a backstop whatever the parsing gets wrong. The decode is bounded too
|
|
46
|
+
(15 s, the mark on until it ends). A very wide picture stays at least 120 px tall and scrolls sideways instead of a sliver.
|
|
47
|
+
|
|
48
|
+
## Security model
|
|
49
|
+
|
|
50
|
+
The bundle's JS is trusted (installing a bundle already means trusting it, [plugins.md](../plugins.md#trust)). **Its input
|
|
51
|
+
is model-written**, so a renderer bug or an injected diagram must not reach a page that controls sessions.
|
|
52
|
+
|
|
53
|
+
| Layer | What it stops |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `<iframe sandbox="allow-scripts">`, never `allow-same-origin` | reading the parent; reading gated responses |
|
|
56
|
+
| CSP meta in the srcdoc (`block_frame.js FRAME_CSP`) | fetch, POST, beacon, image, CSS url, prefetch/preload. Without it WebKit sent the session cookie on the frame's GET |
|
|
57
|
+
| Page header `frame-src 'none'` (`App::FRAME_HEADERS`; srcdoc frames still load and run under it) | self-navigation, meta refresh, anchor click, which leak past the frame's CSP in both browsers |
|
|
58
|
+
| `delete window.RTCPeerConnection` + `webkitRTCPeerConnection` in the bootstrap, before any bundle code | the plain WebRTC call only |
|
|
59
|
+
| Output only as `<img src="data:…">` | scripts and remote images inside the SVG don't run or load |
|
|
60
|
+
|
|
61
|
+
**Hard rules.**
|
|
62
|
+
|
|
63
|
+
1. Never `allow-same-origin`. Never `innerHTML` the frame's output. SVG goes only into a `data:` URL for an `<img>`.
|
|
64
|
+
2. **Never `blob:` for the SVG.** A blob inherits chi web's origin, and the browser's own "Open Image in New Tab" and drag-to-tab
|
|
65
|
+
navigate to it, where its script reads a gated route with chi's origin and cookie. Opened as a document, a `data:` SVG
|
|
66
|
+
has origin `null` and can't read chi's API; WebKit may still attach a same-host cookie to a request it makes (seen with a
|
|
67
|
+
test cookie to another port), so what holds is that the answer stays unreadable, not that no cookie is ever sent.
|
|
68
|
+
3. The page accepts messages only where `e.source === frame.contentWindow`, of known types, and treats them as data
|
|
69
|
+
(`frameMessage`). No other window `message` listener exists on the page.
|
|
70
|
+
4. Code enters the frame by postMessage from the page's own authenticated fetch (the frame's CSP closes its network but for
|
|
71
|
+
WebRTC, the residual risk below, and it has no cookie),
|
|
72
|
+
then as `script.text = code`. `eval` doesn't work: a library's `"use strict"` keeps its global local. A syntax error doesn't
|
|
73
|
+
throw: it reaches the frame's error event, as a failed load. `fetchRendererFile` requires `res.ok` and a JS content type, so a
|
|
74
|
+
proxy's sign-in HTML is never passed in as code.
|
|
75
|
+
|
|
76
|
+
**Residual risk, accepted for v1.** Code running in the frame can send what the frame holds over WebRTC: the diagram sources it
|
|
77
|
+
has rendered. A nested `srcdoc` frame gets a fresh `RTCPeerConnection` and still sends packets, in both browsers, and no CSP
|
|
78
|
+
stops WebRTC. It can't reach the page, cookies or sessions, and it needs a renderer compromised through its input. To limit
|
|
79
|
+
what it holds, the frame is dropped on a session switch.
|
|
80
|
+
|
|
81
|
+
**The hang.** The frame shares the page's thread, so a timer can't fire during a hang, and the model's input drives the
|
|
82
|
+
renderer's runtime. So a source over 64 KB stays source, and before each render a `sessionStorage` mark (`chi_block_rendering:`,
|
|
83
|
+
per block) is written and cleared after; a block whose mark is still there on load stays source with its muted line. A timeout
|
|
84
|
+
keeps the mark too. It is per tab: a new tab tries again. A reload during a render isn't a hang: `pagehide` clears the marks of renders
|
|
85
|
+
under 15 s old. A page truly hung never runs it. A render that ends after holding on over 10 s (`SLOW_RENDER_MS`) draws now and keeps its
|
|
86
|
+
mark, so it stalls the page once and not on every load; several such blocks still stall once each, and one under the bound stalls every
|
|
87
|
+
load. Real Mermaid's worst was about 1 s, so that takes a bad renderer. A mark of a render still running isn't a leftover: a bubble
|
|
88
|
+
rewritten or cloned meanwhile has its blocks wait for that render (`coming`), and each draws into whichever is connected.
|
|
89
|
+
No real hang has been produced; this is a guard.
|
|
90
|
+
|
|
91
|
+
A navigation a hostile renderer tries is blocked, but in Chromium it leaves the frame on an error page, so that render times out at
|
|
92
|
+
15 s and the frame is dropped. Not a concern for a working renderer, and the page stays usable meanwhile.
|
|
93
|
+
|
|
94
|
+
## Not done
|
|
95
|
+
|
|
96
|
+
Firefox isn't tested: if its CSP blocks srcdoc under `frame-src 'none'`, the frame never answers and blocks stay source.
|
|
97
|
+
Interactive output (a visible frame running the model's own code) isn't built: nothing secret could be in such a frame, and a busy
|
|
98
|
+
loop would stall the page.
|
data/docs/internals/bridge.md
CHANGED
|
@@ -15,7 +15,8 @@ rescues its own errors: an observer must never break the running turn.
|
|
|
15
15
|
|-------|------------------|-------|
|
|
16
16
|
| `RingBuffer` | `ring_buffer.rb` | The last `ring_capacity` (256) raw events by `event_seq`, in memory, for a reconnect's replay. The capture observer feeds it. |
|
|
17
17
|
| `TurnAccumulator` | `turn_accumulator.rb` | The turn in progress, folded from events. |
|
|
18
|
-
| `CardStore` | `card_store.rb` | The last cards and notices, saved in `cards.json
|
|
18
|
+
| `CardStore` | `card_store.rb` | The last cards and notices, saved in `cards.json`, each anchored to its turn. |
|
|
19
|
+
| `CardPlaces` | `card_places.rb` | Pure: where `CardStore`'s entries sit in the messages the page draws (`turns_since`). |
|
|
19
20
|
| `PendingCard` | `pending_card.rb` | `pending_card.json` `{id:, bundle:}`: the running turn's open card with actions (check-in's Nudge), written and removed with that card, so chi web's hub, which watches files and not workers, can notify. |
|
|
20
21
|
|
|
21
22
|
Per connection there is an `SSEWriter` (with a `BoundedQueue`); `ClientEvent` and `EventId` are pure helpers. The
|
|
@@ -49,25 +50,60 @@ The cards a plugin or the Engine shows (`Engine#show_card`), hook notices, load
|
|
|
49
50
|
the "asking again" and steer-cut rows, ✂ rows and questions (with how they were answered), for a UI that joins later:
|
|
50
51
|
`snapshot[:cards]` and `GET /tail`'s `cards`.
|
|
51
52
|
|
|
52
|
-
- **
|
|
53
|
-
`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
- **Anchored to its turn.** The store keeps a **turn log**, every id it saw at `:turn_started`, oldest first. An entry
|
|
54
|
+
holds `turn_id`: the running turn's, else the log's last (nil before any); a load warning, which the Engine announces
|
|
55
|
+
inside the first turn, takes the one before it. `list(messages:)` computes `turns_since` from the messages the page draws
|
|
56
|
+
(`CardPlaces`, the snapshot's own list; a stopped session's file for `.saved`): the turn openings (`Steer.turn_opening?`, as
|
|
57
|
+
the page's `TURN_PROMPT` and the recap count) after the last one whose id is in the log at or before the entry's, else from
|
|
58
|
+
the first one whose id is later in the log, else 0. The log only orders; the messages count. So a turn with no opening (a
|
|
59
|
+
reminder, a Continue, a failed or rolled-back one, a prompt stopped before the model) moves nothing, a wake's note does, and
|
|
60
|
+
an opening with no id (`/continue no`'s reason, Qwen's recovery line) counts as on the page. Nothing orders such an opening
|
|
61
|
+
against an entry, so the store reads the session's messages (its `messages:` lambda) when an entry is shown between turns, and
|
|
62
|
+
at the turn's end for what a running turn showed that isn't its own: `past_idless` is how many of them, right after the opening
|
|
63
|
+
it counts from, the entry already came after, and they don't count for it. A `!rollback` usually only shortens that run, and the count
|
|
64
|
+
is capped at the run still there, so a stale number doesn't eat a later turn. It can also move an entry's counting point back to a
|
|
65
|
+
run the entry never recorded (T1, `/continue no, too slow`, a card, a held T2 stopped, a second card, `!rollback`: the second card,
|
|
66
|
+
anchored to T2, now counts from T1 and reads above the reason and the first card). That rare inversion is accepted. A turn's own rows don't read it: they count such a line, which sits below them. `current` is a card of the running turn. A card that
|
|
67
|
+
comes mid-turn but isn't the turn's (an anytime command's, such as `/btw`) goes after that turn (`during`), and so does a
|
|
68
|
+
between-turns notice. A card with an earlier card's id replaces it in place and keeps its anchor. A `list` with no messages
|
|
69
|
+
(`?cards=1`'s bodies, `chi bundle try`'s trace) has no `turns_since`.
|
|
70
|
+
- **A turn that leaves nothing** is read, not told: an in-turn entry not `current` whose turn has no opening in the messages
|
|
71
|
+
(a `!rollback`, a failed turn with nothing kept, a prompt stopped before the model) lists as a between-turns entry at its
|
|
72
|
+
place, a card without its actions and a hook's notice without its step; its other rows, having no step to sit in, aren't
|
|
73
|
+
listed. Nothing in the entry changes. A kept Continue or reminder turn never has an opening, so its entries are `continued`
|
|
74
|
+
and stay rows of the turn before; a failed one loses the mark at its end. The page draws a failed prompt at the history's
|
|
75
|
+
end, so an entry pushed idle after it lists `after_failed` (the store's saved `failed_turn`) and goes below it, until the
|
|
76
|
+
next turn starts.
|
|
77
|
+
- **A command's card.** `SessionCommands#run` wraps its dispatch in `Engine#running_command`, so a card shown on that thread
|
|
78
|
+
carries `command:` (the event's field, `show_card` adds it). The store keeps the first show's (a replacement keeps it, even
|
|
79
|
+
none) and lists `label: "from /short"`, never `command`. The page draws it after a reload, where the command's line is gone.
|
|
58
80
|
- **Caps.** `CAPACITY` (20) each for notices, ✂ rows (`LLM_CONTEXT_TYPES`) and cards, so one burst cannot push out
|
|
59
81
|
another kind. An open question (an approval too) is never pushed out.
|
|
60
82
|
- **Saved.** With a `path` (`cards.json` in the session's folder) the store rewrites the file after each event in
|
|
61
|
-
`SAVED_ON`, and a later worker's store starts from it, so a turn's cards outlive the worker; `CardStore.saved(dir)`
|
|
62
|
-
what chi web reads for a session no worker runs.
|
|
63
|
-
and an
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
`SAVED_ON`, and a later worker's store starts from it, so a turn's cards outlive the worker; `CardStore.saved(dir, messages:)`
|
|
84
|
+
is what chi web reads for a session no worker runs. The file is `{version: 2, turn_log:, entries:}`. The log is pruned at
|
|
85
|
+
a turn's start and end and at an idle show (a running turn's rows reuse the start's, as finding it walks the messages) to start at the opening the earliest anchor counts from (all of it while an entry has none, or for a store with no
|
|
86
|
+
`messages:`), so a restart reads an entry as the live store did; an old card therefore keeps it whole. `seed` drops an entry
|
|
87
|
+
whose `turn_id`, `base_since` or `past_idless` isn't an id or a non-negative integer. Load warnings
|
|
88
|
+
are the worker's own (each start announces them) and an unanswered question waits for no one: neither is saved or seeded. An
|
|
89
|
+
entry from an earlier worker lists with `earlier: true`; one shown during a turn comes back **without its actions** (their
|
|
90
|
+
turn no longer runs), while a between-turns card keeps its buttons.
|
|
91
|
+
- **Version 1.** A file from before the log (top-level `turns`, each entry its own, maybe `running`) keeps its places, drift
|
|
92
|
+
included: `seed` turns each count into `base_since`, new openings add to it, and such an entry never reads as having left
|
|
93
|
+
nothing (no id to check). The next save writes version 2 with `base_since` kept. `SessionManager.cards_empty?` reads both.
|
|
94
|
+
- **An older chi on a version 2 file.** Its `seed` wants an integer `turns`, finds none, and starts with no entries. A chi web not
|
|
95
|
+
yet restarted only reads: it lists no cards for that session until restarted, and the file stays whole. An older **writer** does
|
|
96
|
+
damage: at its first saved event, `:turn_started`, it writes `{"version":1,"turns":0,"running":true,"entries":[]}` over the file
|
|
97
|
+
(`{"version":1,"turns":1,"entries":[]}` after that first turn), so that session's cards go, and with them the answered approvals
|
|
98
|
+
`chi guardrails replay` reads. A plain upgrade doesn't reach this: a worker already running seeded before and keeps its own version 1
|
|
99
|
+
file, and a spawn runs the newest chi installed (`SessionManager.worker_command`), unless that activation raises LoadError: the boot
|
|
100
|
+
line then falls back to the spawner's or the home's pinned version, which can be older. Otherwise it takes an older process starting
|
|
101
|
+
on a session a newer one wrote: a downgrade, a chi home whose newest chi is older (`home_older_than_spawner`), or a REPL or `bin/chi`
|
|
102
|
+
from an older gem or checkout on the same state dir. Nothing on this side stops
|
|
103
|
+
it: an older chi can't be changed, and having it read the file would mean writing fake `turns` counters into every file.
|
|
104
|
+
- **A killed turn needs nothing special.** `TurnCheckpoint` keeps a running turn's prompt in the session file, with its id from
|
|
105
|
+
the start (`Worker#run_engine_turn` settles it first), so it counts as any opening. A REPL keeps no running turn in the
|
|
106
|
+
session, so a kill takes the prompt along and there's nothing to count.
|
|
71
107
|
|
|
72
108
|
## SSEWriter
|
|
73
109
|
|