samagotchi 0.149.0 → 0.151.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 +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- 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/public/app.js +7 -3
- 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_open.js +13 -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 +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24e1f2b0d0bd36a11e7fc2de449630696a6d7e3431c24b6def08a70dd3d27487
|
|
4
|
+
data.tar.gz: 42ae4e62a4474d254ee294a887a856fde273d99f7a7d6b5adc961f08eaf2fb7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fde71cbc5d64e63d34a0c85cae516c8f0f37aecda9d1b099803f023b00e31a95653374c3f8725fa82e4dcfd72aefd65b1de4d36f3a6823f0a55f6028a3a25aa4
|
|
7
|
+
data.tar.gz: 5ead8e63f46e4b8fdb10ea3b19706c8733a0ff1e30d5f129fc3dd4e1595c92d13d6a0da424335963bcc1683e3309eb73958470951b02607b7255ba960b201405
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,72 @@ 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.151.0] - 2026-11-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- 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).
|
|
15
|
+
- 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.
|
|
16
|
+
- [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`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
- A saved recap sits where it belongs in the web after a task or context wake, or a `!cmd`.
|
|
26
|
+
- A card shown after a failed turn stays below that turn's prompt after a reload, as it was live.
|
|
27
|
+
|
|
28
|
+
Update with `chi update`.
|
|
29
|
+
|
|
30
|
+
## [0.150.0] - 2026-11-05
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The web's steps block closes to its summary line after a turn and after a reload, whatever its steps said; opened, it shows the outline as before. A canceled or failed turn with a narrated step stays open. [docs/cli.md](docs/cli.md#how-the-page-draws-a-turn)
|
|
35
|
+
- The web's working-set footprint is now the steps block's summary line, in body colour, the steps' count at its right (a phone shows the footprint alone); there is no separate line under the block. A click on its words opens the turn's list, the chevron or the count opens the steps. [docs/cli.md](docs/cli.md#the-working-set)
|
|
36
|
+
- Check this also reads the agent's visible answers, never its thinking, so a claim about what the agent said or asked can be confirmed. They settle only that: a claim of work with nothing but the agent's words behind it isn't `confirmed`. With `objectives.check_model` they go to that host too. [docs/sessions.md](docs/sessions.md#objectives)
|
|
37
|
+
- An upgrade now merges a memory you edited with the bundle's new version instead of leaving it behind. `chi bundle upgrade`, `chi update` and the automatic system bundle update run a text merge (needs `git`): a clean merge replaces the file, with the old one in the bundle trash and the report saying `merged 0.8.0 into your x.md`, and `--dry-run` says `would merge`. When a chi start updates the system bundle, a terminal launch says so in one line. Entries both sides added under a `Changelog` heading are kept, yours first. A real conflict, no `git`, or no recorded base leaves your file untouched, as before, and a file marked by an older chi can stay behind too (see the next line).
|
|
38
|
+
- A memory left behind as a conflict gets another try without waiting for the bundle's next version: the system bundle retries its files marked conflict at every chi start and on `chi update`, the other bundles chi ships on `chi update`, and any other bundle on `chi bundle upgrade`. It touches memory files only, and a row reads `0.8.0 merged 0.8.0 into your x.md`. A file marked by an older chi merges normally when its recorded base is a version chi shipped, checked against the samagotchi gems still installed. Otherwise that base may be your own text, so the file merges only if the bundle takes out or rewords no line it still holds. If it does, it stays behind until resolved by hand, with `--agent` or with `--force`, and the retry can't fix that.
|
|
39
|
+
- Everything that reports an edited bundle file now says what happened in one wording: `merged 0.8.0 into your x.md`, or `x.md stays behind 0.8.0: missed ## Steps, ## Rules and 2 more` with the reason when there is one (`no merge possible (git not found)`). The record keeps why and what was missed, and the bundle's text is saved at `.bundles/<name>/incoming/<file>` until the file settles. `kept your edits` is gone from `chi update`, whose `--json` rows gain `merged` next to `kept`, and from `chi bundle upgrade`, `chi bootstrap` and the system bundle's line at a start.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- `bundle exec chi home set --ruby X` no longer records the bundle's vendor folder as X's gems when the bundle has a `path` and holds samagotchi: the probe of X still took `BUNDLER_SETUP`. A home set that way keeps its wrong `gem_home` until you run `chi home set` again.
|
|
44
|
+
- Under a `bundle exec chi`, a tool's plain `ruby` no longer loads Bundler: `BUNDLER_SETUP` still reached it, so in a folder whose Gemfile needs a missing gem `ruby -e` exited 1, and with Bundler 4 (`BUNDLE_LOCKFILE`) a folder with any other Gemfile had its lock written over chi's own `Gemfile.lock`. `execute`, tasks, `!cmd`, context sources and MCP servers all run without them, and `task_create` refuses them in `env`, saying to write `KEY=value cmd` in the command instead.
|
|
45
|
+
- `chi bundle install <git url>` run from a git hook or `git rebase --exec` no longer takes the repo git's variables name for its own: `GIT_WORK_TREE` failed the clone, `GIT_OBJECT_DIRECTORY` wrote the bundle's objects into that repo, and `GIT_DIR` alone recorded that repo's HEAD as the bundle's `source_commit`. Settings given around chi (`git -c url.….insteadOf=…`, `GIT_CONFIG_COUNT`) still apply to the clone. `delegate`'s `cwd:` check also asks about the folder it was given, as do the known-names (the git name), github-pr (the branch, `gh pr view`) and source-links (the remote) bundles.
|
|
46
|
+
- The web no longer loses a reply you steered: a line sent while a text-only step streamed left that step's whole text hidden, with no row or summary to open, live and after a reload. The step is now a row of the steps block (`1 step`), like any other text step.
|
|
47
|
+
- A web card updated just as a finished turn slid into the history no longer stays stuck on its first text until a reload (`/short` could stay on "Short version: …").
|
|
48
|
+
- A web card no longer lands a turn too early for good after `!rollback` of a canceled or failed turn: the rollback takes the turn out of the count. A rolled-back or failed turn's own cards stay where the turn was, without their buttons, instead of being read as the next turn's.
|
|
49
|
+
- A web card no longer lands one turn late for good after a worker was killed in the next turn (`kill -9`, a pod that didn't wait): the turn is counted, since its prompt stays in the session.
|
|
50
|
+
- A web card keeps its place after a reload when the turn after it failed with nothing kept: above the failed prompt, as it showed live, not below it, and an older card no longer lands one turn too late. A failed prompt isn't counted as a turn any more.
|
|
51
|
+
- `chi bundle list` and `chi self` say when a bundle has a file behind (`1 file behind`), and `chi bundle status` says it per file. `issues=` now counts files that stay behind, are missing or can't be read: an edit alone is no issue. A file `chi bundle status` can't read no longer crashes it.
|
|
52
|
+
- Every merge now leaves a note on the file's record, since a worker's reaches only the log and an automatic update's rows no one reads: `chi bundle status` shows it (with the old copy's folder), `chi update` names an earlier one in its row, and a row printed to you, or `chi bundle upgrade`'s output, ends the note. An automatic update names it without clearing it.
|
|
53
|
+
- `chi update` no longer crashes on an edited memory it can't read at a newer version: that bundle's row fails and says which file, and the others go on.
|
|
54
|
+
- The start-up hint `… can be updated: chi update` counts a bundle whose file would now merge at the same version.
|
|
55
|
+
- The update sheet shows `chi update`'s own words for an edited file, and the phone's push after an automatic update counts both sides (`Merged the update into 1 file you edited. 2 files stay behind.`): a merge used to go unmentioned, and every kept file read as `Kept your edits`.
|
|
56
|
+
- `chi update` no longer reads `up to date` for a file that stays behind at the same version: a real conflict, no `git`, a timeout, a file it can't read, or one left for the bundle's next version now name the file and why, and the summary counts them.
|
|
57
|
+
- A file marked by an older chi whose base chi can't vouch for no longer claims to hold your edits: it says chi can't tell, and why.
|
|
58
|
+
- `chi update`'s table no longer carries the trash's long path: the row says `old copies: chi bundle trash`, and `--json`'s `trash_copy` has the full line.
|
|
59
|
+
- The `--force` suggestions say what happens to your file: it is copied to the trash first.
|
|
60
|
+
- `chi bundle install --force` and `chi bundle upgrade --force` no longer destroy what they overwrite: a memory that differs from the bundle's, your own same-name file too, is copied to the bundle trash first, and the output names it (`Copied to the trash first: …`; `chi bundle trash` lists it).
|
|
61
|
+
- An install, upgrade or `chi update` that replaces or removes a hook, rule, plugin or script you edited copies it to the bundle trash first, under its own subfolder (`hooks/x`, `scripts/sub/x`) at any depth, and says so. A `--dev` install's links are left out.
|
|
62
|
+
- `chi bundle uninstall`, with `--force` or without, copies a hook, rule, plugin or script you edited to the bundle trash before removing it, and names it. Only memories reached the trash before.
|
|
63
|
+
- An install, upgrade or uninstall that can't read a file or write the bundle trash fails with a message naming the file, not a backtrace, and running it again after fixing that picks up where it stopped.
|
|
64
|
+
- A memory whose conflict was resolved through `chi bundle upgrade --agent` keeps its description in the bundle's record.
|
|
65
|
+
- `chi bundle upgrade --agent` no longer calls a conflict resolved when nothing merged it. A file the session didn't change, or one still holding conflict markers, stays a conflict, the output says so (`Nothing changed in X: still behind.`), and the command exits 2. It used to mark the file resolved, so later upgrades kept it without a word and the bundle's change never arrived.
|
|
66
|
+
- An edited memory with no recorded base keeps surviving upgrades as a conflict. Its own text used to become the base, so the following upgrade overwrote it with nothing in the trash. A record an older chi already left that way (marked conflict, the base being your text) stays behind when the update would take out a line it holds. Otherwise it takes the update, with your text copied to the bundle trash first and a line saying so.
|
|
67
|
+
- A plain `chi bundle install` keeps the conflict mark on a file it skipped, so `chi bundle status` still shows the file missed an update.
|
|
68
|
+
- `chi bundle upgrade --dry-run` no longer prints a false `Checksum mismatch` for a memory you edited, and its `Kept` line now names those files. This only changes a dry run's output.
|
|
69
|
+
- `chi bundle upgrade --agent` no longer takes your file as the new base when the bundle's copy disappears during the session. The next upgrade used to overwrite the edit with nothing in the trash; the file now stays a conflict and the command says so.
|
|
70
|
+
- An install or upgrade no longer fails at its last step on a memory that isn't valid UTF-8.
|
|
71
|
+
- A Changelog merge compares whole entries, nested items included: your `- a` / ` - note` beside the bundle's `- b` / ` - note` no longer loses the bundle's ` - note`.
|
|
72
|
+
- Putting back a deleted system memory at the same version now restores that file alone. It used to run a whole upgrade, replacing hooks, rules and the plugin under live sessions.
|
|
73
|
+
|
|
74
|
+
Update with `chi update` (github-pr 0.6.3, known-names 0.1.9, source-links 0.6.3: their git calls ask about the session's folder, and they need this chi; dev profile 0.7.6).
|
|
75
|
+
|
|
10
76
|
## [0.149.0] - 2026-11-04
|
|
11
77
|
|
|
12
78
|
### Added
|
|
@@ -5624,7 +5690,9 @@ and long-lived sessions.
|
|
|
5624
5690
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5625
5691
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5626
5692
|
|
|
5627
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5693
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.151.0...HEAD
|
|
5694
|
+
[0.151.0]: https://github.com/dm1try/samagotchi/compare/v0.150.0...v0.151.0
|
|
5695
|
+
[0.150.0]: https://github.com/dm1try/samagotchi/compare/v0.149.0...v0.150.0
|
|
5628
5696
|
[0.149.0]: https://github.com/dm1try/samagotchi/compare/v0.148.0...v0.149.0
|
|
5629
5697
|
[0.148.0]: https://github.com/dm1try/samagotchi/compare/v0.147.0...v0.148.0
|
|
5630
5698
|
[0.147.0]: https://github.com/dm1try/samagotchi/compare/v0.146.0...v0.147.0
|
data/README.md
CHANGED
|
@@ -53,8 +53,10 @@ chi update # the gem, then the bundles and the desktop helper
|
|
|
53
53
|
`chi update` installs the newest samagotchi from rubygems.org (old versions
|
|
54
54
|
stay installed: running sessions still use them), then brings the rest up to
|
|
55
55
|
the new version: the system bundle, the bundles chi ships that you installed,
|
|
56
|
-
and the macOS helper (rebuilt only when its sources changed).
|
|
57
|
-
edited
|
|
56
|
+
and the macOS helper (rebuilt only when its sources changed). A memory file you
|
|
57
|
+
edited takes the update in a text merge (the old file goes to the bundle trash),
|
|
58
|
+
or stays behind as it is when no merge is possible, and the table says which
|
|
59
|
+
and why. Running sessions keep their chi
|
|
58
60
|
until restarted (`chi sessions restart ID`, or the Restart badge on the web);
|
|
59
61
|
new and woken ones start on the new chi. A running `chi web` needs a restart,
|
|
60
62
|
and its page says so. From a checkout, `git pull` instead. See
|
data/docs/architecture.md
CHANGED
|
@@ -102,7 +102,7 @@ terminal renders the events it gets over the Bridge.
|
|
|
102
102
|
| Model loops | `KernelLoop` (via `LLM::NativeBackend`), `LLM::ChatLoop` | The model↔tool loop: raw prompt, or a chat API (OpenAI chat, Anthropic Messages), chosen per host (see below). |
|
|
103
103
|
| Adapters | `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | Raw-prompt servers, the OpenAI chat API, Anthropic's Messages API, and the HTTP they share. |
|
|
104
104
|
| Tools | `lib/samagotchi/tools/*` | Execute, read, edit, write, memory, task_*, web_fetch, delegate/delegate_result, ask_user_question, reminders, send_note, list_sessions, context_read, objectives, forget_outputs, find_tools/call_tool (the tool index), plus runtime/output-guardrails. |
|
|
105
|
-
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way merge), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
105
|
+
| Bundles | `lib/samagotchi/memory_bundle/*`, `BundleCommand` | `chi bundle`: install/upgrade (3-way, with a text merge of edited memories), build, uninstall; the install record under `.bundles/<name>/` that the hook, guardrail and plugin loaders read ([internals/bundles.md](internals/bundles.md)). |
|
|
106
106
|
| Background | `Samagotchi::Worker`, `Samagotchi::SessionManager` | `SessionManager` forks/spawns one worker process per session; its `Worker` builds the `Engine` and `Bridge` (no terminal rendering) and runs queued turns. The spawned process (own process group) forks the worker, which `setsid`s and is reparented to init once its starter exits (it waits until the worker holds `owner.lock`, so the spawner's `Process.detach` waiter tells a starting worker): nothing that walks the spawner's process tree reaches it (`SAMAGOTCHI_WORKER_DETACH=1`, `detach_from_spawner`). The lock's pid is the worker's; `spawn`'s `child_pid` is its starter's. |
|
|
107
107
|
| Web | `Samagotchi::Web::App`, `Samagotchi::Web::Server`, `Samagotchi::Web::SessionHub` | Rack+WEBrick on one port, `127.0.0.1:4567` (index.html + `/api/*` + SSE); with `web.host: lan` also on this machine's LAN address, behind an access token (`Web::Lan`, `Web::Token`). The hub is chi web's projection of the session list, pushed to every tab over `GET /api/events`. |
|
|
108
108
|
| Sessions | `Samagotchi::Session`, `SessionManager` | File `sessions/<uuid>.json` + sidecar `input/`/`output/`; retention 14d/500, `updated_at desc`, lazy sweep. |
|
|
@@ -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
|
|
@@ -394,7 +394,7 @@ record starts at `phase: installed` and the run goes on as below. Ahead of every
|
|
|
394
394
|
`SessionHub#auto_offer_restart` (`auto_offers`, in memory) when a run on record for the newest installed chi is closed with
|
|
395
395
|
this chi web still older, or that chi has waited `HELD_FOR` (1 h): the trying goes on, and the page has its buttons back;
|
|
396
396
|
`restart_failed` and a failed install call it at once. Its record is `update_check.json`'s `auto`:
|
|
397
|
-
`{version, from, at, kept, phase: installed|workers|web|retrying|done|failed, workers: {restarted, left}, detail, pushed}`,
|
|
397
|
+
`{version, from, at, kept, merged, phase: installed|workers|web|retrying|done|failed, workers: {restarted, left}, detail, pushed}`,
|
|
398
398
|
in Ruby the value object `Web::AutoUpdateRecord`, which owns that shape (`from_h`, `to_store`).
|
|
399
399
|
With `update.restart: never` the run ends at the install (`chi_notice` `installed`). With `idle` it goes on
|
|
400
400
|
(`phase` `installed` → `workers` → `web` → `retrying` → `done`): `UpdateOps#restart_workers(select:)` over the stale workers whose
|
|
@@ -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
|
|
data/docs/cli.md
CHANGED
|
@@ -228,9 +228,11 @@ $ chi models qwen # only the names containing "qwen" (any case)
|
|
|
228
228
|
```
|
|
229
229
|
component from to status
|
|
230
230
|
chi (gem) 0.2.0 0.3.0 updated
|
|
231
|
-
system bundle 0.2.0 0.3.0 updated (
|
|
231
|
+
system bundle 0.2.0 0.3.0 updated (identity.md stays behind 0.3.0: missed ## Who you are (chi bundle diff samagotchi-system identity.md))
|
|
232
232
|
btw 0.1.1 up to date
|
|
233
|
-
known-names 0.1.0 0.1.1 updated
|
|
233
|
+
known-names 0.1.0 0.1.1 updated (merged 0.1.1 into your known_names.md; old copies: chi bundle trash)
|
|
234
|
+
coordinator 0.8.0 merged 0.8.0 into your skill_coordinator.md; old copies: chi bundle trash
|
|
235
|
+
loop-guard 0.2.0 notes.md stays behind 0.2.0: no merge possible (git not found) (chi bundle diff loop-guard notes.md)
|
|
234
236
|
core 0.1.0 0.2.0 updated (+ new-bundle)
|
|
235
237
|
infra_tools 1.0.0 skipped (not from chi)
|
|
236
238
|
infra_tools warn api plugin.rb:3: ctx.old_name isn't in chi 0.3.0's plugin API (fix: docs/plugins.md has the API)
|
|
@@ -238,15 +240,17 @@ Chi Helper 0.2.0 up to date (launch file refreshed)
|
|
|
238
240
|
workers 2 live on 0.2.0: they keep it until restarted (chi sessions restart 2ea8c1f0 91b0d2aa); or they idle out after 30 min with nothing attached
|
|
239
241
|
Bundles not from chi are left as they are: their author's next version, or chi bundle upgrade NAME, is what changes them
|
|
240
242
|
Also shipped, not installed: dev (mcp, skills, source-links) (chi bundle install NAME)
|
|
241
|
-
|
|
243
|
+
2 files stay behind (see above)
|
|
242
244
|
```
|
|
243
245
|
|
|
244
246
|
- `--to VERSION` installs exactly that gem version instead of checking for the
|
|
245
247
|
newest release. It must be newer than the running chi; an equal or older
|
|
246
248
|
version is refused.
|
|
247
249
|
- `--json` writes the component rows as one JSON array on stdout. Each row has
|
|
248
|
-
`component`, `from`, `to`, `status`, `failed`, `kept` and `level` fields;
|
|
249
|
-
`kept` is an array of edited filenames
|
|
250
|
+
`component`, `from`, `to`, `status`, `failed`, `kept`, `merged`, `earlier`, `trash_copy` and `level` fields;
|
|
251
|
+
`kept` is an array of edited filenames left behind, `merged` of those this run
|
|
252
|
+
merged, `earlier` of those an earlier merge's note names, `trash_copy` the full line
|
|
253
|
+
saying where the old copies went (the table says `chi bundle trash`), and `level` is `warn` or `error` on a
|
|
250
254
|
re-check's row (below), `null` on the others. Install progress is written to
|
|
251
255
|
stderr.
|
|
252
256
|
|
|
@@ -304,11 +308,36 @@ done
|
|
|
304
308
|
the row says what it did.
|
|
305
309
|
- **Shipped bundles**: each one you installed from chi (`chi bundle install
|
|
306
310
|
NAME`) is upgraded when chi ships a newer version. Memory files get the
|
|
307
|
-
3-way merge of `chi bundle upgrade`: an unedited file is updated, an
|
|
308
|
-
one that the new version also changes
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
3-way merge of `chi bundle upgrade`: an unedited file is updated, and an
|
|
312
|
+
edited one that the new version also changes takes a text merge, with the old
|
|
313
|
+
file in the bundle trash and `merged 0.8.0 into your FILE` in the row. A real
|
|
314
|
+
conflict, no `git`, or no recorded base keeps your file untouched, and the row
|
|
315
|
+
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
|
|
317
|
+
upgrade NAME --force` takes the bundle's, with a copy of yours in the bundle
|
|
318
|
+
trash first. A later run retries such a file at the same version: its row reads
|
|
319
|
+
`0.8.0 merged 0.8.0 into your FILE` when it merges, and names it as behind when
|
|
320
|
+
it doesn't. So does a file left for the bundle's next version (it matches its old
|
|
321
|
+
base), or one the retry can't read, and the row never says `up to date` while a file
|
|
322
|
+
stays behind. Once a file has the bundle's bytes it isn't behind, however it got
|
|
323
|
+
them. A file marked by an older chi
|
|
324
|
+
merges normally when its recorded base is a version chi shipped (an installed
|
|
325
|
+
samagotchi gem still has it). Otherwise that base may be your own text, so it merges only if the bundle takes out or
|
|
326
|
+
rewords no line it still holds. If it does, chi can't tell whether the file holds
|
|
327
|
+
edits of yours, says so, and the file stays behind until resolved by
|
|
328
|
+
hand, with `--agent` or with `--force`; the retry can't fix that. Every merge
|
|
329
|
+
leaves a note on the file's record, since a worker's reaches only the log and an
|
|
330
|
+
automatic update's rows may be read by no one. `chi bundle status` shows it while it
|
|
331
|
+
exists. A `chi update` row names an earlier one (`up to date (merged 0.8.0 into your
|
|
332
|
+
FILE on 2026-11-05 14:03; …)`), and a row printed to you ends the note for what it
|
|
333
|
+
printed, that run's own merges included; so does `chi bundle upgrade`, which prints
|
|
334
|
+
the merge itself, and `--force` replacing the file. A dry run and an automatic update
|
|
335
|
+
(`update.auto`) show it and leave it, and so does a failed row or one with no place
|
|
336
|
+
for it (a skipped system bundle). A memory that can't be read at a
|
|
337
|
+
newer version fails that bundle's row with the file's name. Hooks, rules and the plugin are replaced; an edited one didn't
|
|
338
|
+
load anyway (its sha no longer matched), the row says it was replaced, and
|
|
339
|
+
a copy goes to the bundle trash first (`chi bundle trash`; `hooks/x`,
|
|
340
|
+
`guardrails/x`, `plugin/x`, `scripts/x` inside the dir).
|
|
312
341
|
A bundle of the same name from elsewhere (a zip, git) is skipped ("not from
|
|
313
342
|
chi"), a newer installed one is left, one whose new version needs a newer
|
|
314
343
|
chi is skipped, and bundles you didn't install stay uninstalled.
|
|
@@ -367,9 +396,10 @@ this version's view: bundles that only a newer gem ships newer show up once
|
|
|
367
396
|
that gem is installed (the real run installs it first and hands over).
|
|
368
397
|
`--no-gem`, `--no-bundles` and `--no-desktop` leave a part alone for one run;
|
|
369
398
|
`update.gem`, `update.bundles` and `update.desktop: false` in config.yml turn
|
|
370
|
-
one off for good. It exits 0 when nothing failed (
|
|
399
|
+
one off for good. It exits 0 when nothing failed (merged edits, files that stay behind and skips are
|
|
371
400
|
fine), 1 when a part failed, 2 on a usage error. A second run changes nothing
|
|
372
|
-
and ends with "everything is up to date"
|
|
401
|
+
and ends with "everything is up to date", or with `N files stay behind (see above)`
|
|
402
|
+
when some do.
|
|
373
403
|
|
|
374
404
|
From a checkout it refuses (`git pull`, or `chi bundle upgrade NAME` for one
|
|
375
405
|
bundle). After a gem update, the first interactive start of the new version
|
|
@@ -1189,14 +1219,17 @@ again). When a step ends its thinking closes to a plain `thinking` line
|
|
|
1189
1219
|
(unless you opened it); a step that only thought shows that line alone. The
|
|
1190
1220
|
live narration is a box of about three lines that fills sentence by
|
|
1191
1221
|
sentence (a sentence shows once it is complete) and scrolls to the newest
|
|
1192
|
-
when full. When the turn ends the block
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1222
|
+
when full. When the turn ends the block closes to its summary (`3 steps
|
|
1223
|
+
· 8 tool calls · execute ×7 · read ×1`, after the turn's
|
|
1224
|
+
[footprint](#the-working-set) when it has one). Opened, it shows its outline when any of
|
|
1225
|
+
its steps said something: one line per step that wrote text (that text's first
|
|
1226
|
+
line), and each run of steps that only called tools or thought sits behind one
|
|
1227
|
+
`· N steps` line that shows them. That includes a reply a line was steered into: its
|
|
1228
|
+
step is a row like any other, and the answer that follows is the bubble. A canceled or failed turn with such a step
|
|
1229
|
+
stays open, as its last narrated step is the only text you get. A reload draws
|
|
1230
|
+
the same from what was saved: a failed turn whose stream dropped mid-narration
|
|
1231
|
+
saved no such step, so it reloads closed. Either way the answer is a normal bubble under the block (Markdown,
|
|
1232
|
+
annotate). A plain answer
|
|
1200
1233
|
without tools ends exactly as it does today. Rows the code collapses stay
|
|
1201
1234
|
as you toggled them. A reloaded session shows the same block
|
|
1202
1235
|
from the saved messages (each step's thinking, narration, tool parameters
|
|
@@ -1239,12 +1272,14 @@ session bar shows the `mem N` chip again.
|
|
|
1239
1272
|
folds it to one line (`SET 17 + ◆ name`), kept per browser; a click opens the
|
|
1240
1273
|
list. In a stage narrower than 760 px it is one line under the last calls;
|
|
1241
1274
|
on a phone it isn't shown.
|
|
1242
|
-
- **The footprint**,
|
|
1275
|
+
- **The footprint**, each finished turn's block's summary line, live and after
|
|
1243
1276
|
a reload: `touched …/web/public/ · 4 files · ◆ 2 memories s2–10 · +1 url ·
|
|
1244
1277
|
edited 1`, then `n stale`, `n forgotten` or `n dropped` while some are (a file
|
|
1245
|
-
changed after the turn read it, even at its very end, says so here)
|
|
1246
|
-
tag (`s2`) goes to that call's row; the rest
|
|
1247
|
-
|
|
1278
|
+
changed after the turn read it, even at its very end, says so here), with the
|
|
1279
|
+
steps' count at the right. A step tag (`s2`) goes to that call's row; the rest
|
|
1280
|
+
of its words opens the turn's list; the chevron or the count opens the steps.
|
|
1281
|
+
A phone shows the footprint alone, no count. A turn that touched nothing has
|
|
1282
|
+
none: its summary is the count.
|
|
1248
1283
|
- **The `set N` chip** in the session bar, in place of the `mem N` chip (whose
|
|
1249
1284
|
tooltip goes too): the session's whole set, preloaded (`--memory`) and muted
|
|
1250
1285
|
(`--mute`) memories included; its dot lights when the set changed since you
|
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
|
|
@@ -2141,7 +2141,7 @@ Both are read when chi web starts (restart it to apply a change). What it does,
|
|
|
2141
2141
|
install isn't blocked (a source checkout, Bundler, a RubyGems directory chi can't write: those stay a notice, with
|
|
2142
2142
|
the command to run). `chi update` also merges the shipped bundles that live sessions read, hence "no turn anywhere".
|
|
2143
2143
|
2. **Installs** exactly the release the notice named, as the **Update** button does (the system bundle and shipped
|
|
2144
|
-
bundles are merged three ways;
|
|
2144
|
+
bundles are merged three ways; a file you edited takes a text merge, one with a real conflict stays behind, and the push counts both). The page's sticky toast says
|
|
2145
2145
|
"Automatic chi update in progress" and its sheet shows the rows; with `update.restart: idle` no "installed"
|
|
2146
2146
|
toast with **Restart** buttons follows (the run restarts by itself). A failed install is tried **once per version**:
|
|
2147
2147
|
it pushes the failed row's reason and stays quiet until a newer release (or you, with the button) tries again.
|
|
@@ -2158,7 +2158,7 @@ Both are read when chi web starts (restart it to apply a change). What it does,
|
|
|
2158
2158
|
the install; after that they wait for their own idle exit or the session's **Restart**. A newer release in that day
|
|
2159
2159
|
installs as usual and takes them over.
|
|
2160
2160
|
4. **Says so.** The restarted chi web pushes "chi updated to X" ("chi web and 3 sessions restarted; 1 restarts when
|
|
2161
|
-
idle.
|
|
2161
|
+
idle. Merged the update into 1 file you edited. 2 files stay behind."); the later restarts of those sessions push nothing more. With
|
|
2162
2162
|
`update.restart: never` the push is "chi X is installed" and chi web and the workers move over when you restart them (the page's toast offers it). The push needs the phone set up
|
|
2163
2163
|
([Phone notifications](#phone-notifications-web-push)); without it the page's toasts and
|
|
2164
2164
|
`$XDG_STATE_HOME/samagotchi/update_check.json` (`auto`) are the record.
|
|
@@ -2273,7 +2273,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2273
2273
|
| `context.candidates.threshold` | `0.8` | | The probability at which the judge attaches a candidate in on mode (a judged line's `would: attach`). config.yml only. |
|
|
2274
2274
|
| `context.candidates.offer_threshold` | `0.6` | | The probability from which on mode shows the candidate's card, once (`would: offer`, below `threshold`). config.yml only. |
|
|
2275
2275
|
| `context.candidates.reask_after_turns` | `3` | | Owner prompts after a judged ask before the judge's one re-ask of that candidate. config.yml only. |
|
|
2276
|
-
| `objectives.check_model` | none | | The model [Check this](sessions.md#objectives) asks, its one side request (a host:id, an alias, a host's model id); none: the session's own. **Each Check this sends that model's host up to 24,000 characters of the session's tool output and
|
|
2276
|
+
| `objectives.check_model` | none | | The model [Check this](sessions.md#objectives) asks, its one side request (a host:id, an alias, a host's model id); none: the session's own. **Each Check this sends that model's host up to 24,000 characters of the session's tool output, messages and the agent's visible answers, off the machine with a hosted model even from a local session;** known key shapes and `token=`-style values are redacted first, by pattern only. config.yml only. |
|
|
2277
2277
|
| `objectives.system_one` | `off` | | `audit`: System 1 answers a yes/no per active objective on what comes in and goes out, saved as runs and acted on by nothing; see [System 1's saved runs](system-one.md#objectives-in-audit). `off` (the default): no conversation text goes to a System 1 host for this. Its host and model are `system_one.host` / `.model`. config.yml only. |
|
|
2278
2278
|
| `toolkit.default` | `full` | | The toolkit a new session is stored on: the tools its prompts declare (`full`, `lean`, `child`, one of `toolkit.kits` or a bundle's). `chi --toolkit` picks one per session. See "Toolkits". |
|
|
2279
2279
|
| `toolkit.child` | `full` | | A delegate child's toolkit, unless its `delegate` call names one (`toolkit:`). See "Toolkits". |
|
|
@@ -2305,7 +2305,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2305
2305
|
| `update.bundles` | `true` | | `false`: `chi update` leaves the shipped bundles to `chi bundle upgrade` (`--no-bundles`). |
|
|
2306
2306
|
| `update.desktop` | `true` | | `false`: `chi update` leaves the desktop helper alone (`--no-desktop`). |
|
|
2307
2307
|
| `update.notify` | `on` | | `on`: chi web checks RubyGems once a day and shows aged stable releases newer than both this chi and the newest installed chi; `off`: no thread or network check. |
|
|
2308
|
-
| `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged,
|
|
2308
|
+
| `update.auto` | `off` | | `install`: chi web installs the release its checker found (aged by `update.min_age_hours`) once no turn runs and no question waits in any session, as the **Update** button would (shipped bundles merged, a file that can't merge left behind, both reported), then pushes how it went; `update.restart` says what follows, also for a chi installed some other way (`chi update` in a terminal). An install that fails is tried once per version (a newer release, or the **Update** button, tries again). The check runs even with `update.notify: off` (which then only silences the page's and the phone's "available"). An install that can't run here (a checkout, Bundler, an unwritable gem dir) stays a notice. `off`: notice only. See [Unattended updates](#unattended-updates-updateauto). |
|
|
2309
2309
|
| `update.restart` | `idle` | | With `update.auto: install`: `idle` restarts the stale workers whose session is idle and untouched for 10 minutes, then chi web itself, once no turn runs anywhere and the newest chi has answered `chi --version`; it pushes "chi updated to X". `never`: the install only (the page's toast offers the restarts, workers also exit on their own when idle). |
|
|
2310
2310
|
| `update.check_hours` | `24` | | Hours between RubyGems checks (at least 1). The first check starts about 30 seconds after chi web starts. |
|
|
2311
2311
|
| `update.min_age_hours` | `24` | | A release must be this old before chi web offers it; `0` disables the age delay. |
|
|
@@ -2329,7 +2329,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
|
|
|
2329
2329
|
| `web.port` | `4567` | `--port` | `chi web`'s port. See [CLI](cli.md). |
|
|
2330
2330
|
| `web.host` | `127.0.0.1` | yes | `127.0.0.1`, `::1` or `localhost`; `lan` (this machine's private IPv4 address) or one of its IPv4 addresses also opens `chi web` to the network, with an access token, asked of this machine's 127.0.0.1 too (its `Local:` link carries a one-time code; `chi web --new-token` replaces the token). Anything else binds `127.0.0.1` with a warning. See [CLI: chi web on your phone](cli.md#chi-web-on-your-phone). |
|
|
2331
2331
|
| `web.markdown` | `false` | yes | Render answers as Markdown in `chi web`. |
|
|
2332
|
-
| `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint
|
|
2332
|
+
| `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint as each finished turn's block summary and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load. See [CLI: the working set](cli.md#the-working-set). |
|
|
2333
2333
|
| `web.annotate_presets` | `Agreed\|Could you please elaborate?` | yes | Quick replies next to Annotate in `chi web`, `\|`-separated (a YAML list works too); `""` in the file or on the CLI leaves only Annotate (an empty env value means the default). See [CLI](cli.md#web-annotate-presets). |
|
|
2334
2334
|
| `web.editor` | `vscode` | yes | What a file ref in `chi web` (a file tool row's title, a diff line) opens, for a viewer on this machine: a preset (`vscode`, `vscode-insiders`, `cursor`, `zed`) or a URL template with `{path}` and an optional `{line}`; `none` turns refs off. See [Opening files from chi web](#opening-files-from-chi-web). |
|
|
2335
2335
|
| `web.start_dir` | the folder `chi web` runs in | | The folder `chi web` starts in (`chi web --start-dir DIR` for one run): its page (the printed link, a bare `/`, the LAN link and QR code, the Home Screen app) opens on that folder's project, outside a repo on every session, and a new session from the all view starts there; the header's project picker switches projects. `~` and a relative path (resolved from your home folder) work; one that isn't a folder stops `chi web` from starting. `chi web` never changes its own folder. See [CLI](cli.md). |
|
|
@@ -42,7 +42,7 @@ Behavior:
|
|
|
42
42
|
again or stop it (`TaskWait::MESSAGE_WAITING_NOTE`), and `waited: Ns`; the task keeps running and stays watched
|
|
43
43
|
(below). A task that finished meanwhile gives its finished result, as after a Stop. A `chi -p` turn has no probe. The same probe ends a blocking `delegate_result` (and `delegate`'s wait) early; see [Sessions: Delegating](../sessions.md).
|
|
44
44
|
- `task_create` accepts `env` as a JSON object string for deterministic overrides such as `PATH`; use an absolute interpreter path when that is simpler. Ruby/Bundler isolation variables remain protected.
|
|
45
|
-
- `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB
|
|
45
|
+
- `execute` and `task_create` both spawn with chi's own Ruby/Bundler setup scrubbed (`TaskRuntime.sanitized_spawn_env`): `RUBYOPT` loses its `-r…bundler/setup` flags (others stay) and `RUBYLIB` and what `bundle exec` sets (`BundlerEnv::VARS`: `BUNDLE_GEMFILE`, `BUNDLE_LOCKFILE`, `BUNDLE_BIN_PATH`, `BUNDLER_VERSION`, `BUNDLER_SETUP`) are unset, so a project's `ruby -e` or `bundle exec` uses its own Gemfile, or none, when chi runs under `bundle exec`. RubyGems requires what `BUNDLER_SETUP` names at boot, whatever `RUBYOPT` says. A project's own settings (`BUNDLE_PATH`, `BUNDLE_WITHOUT`) stay. `task_create` refuses those keys in the model's `env`, its error saying to write `KEY=value cmd` in the command instead; `execute`'s `env` comes from chi's callers (`Builtins`, `!cmd`), not the model, and is set over the scrub. An `execute` handed over at its limit keeps the process it spawned, so the task runs with the same env.
|
|
46
46
|
- `execute` and `task_create` export chi's own markers into the command, set over the model's `env` (which can't set them: `Error: env key is reserved`): `SAMAGOTCHI_PARENT_SESSION` (the session's id, `chi` without one; a chi started there answers as a parent agent), `SAMAGOTCHI_SESSION_MODEL` (the resolved ref the session runs on, read at each call so it follows `/model`; unset when unknown) `SAMAGOTCHI_SESSION_THINKING` (the session's own thinking level; unset when it has none) and `SAMAGOTCHI_SESSION_REASONING_HISTORY` (its own reasoning_history level, the same way). `chi self` reads the last three for its model, thinking level and reasoning history. The four are `ChildEnv::SESSION_MARKERS` (`TaskRuntime::MARKER_ENV_KEYS`), which a spawned worker unsets (`ChildEnv.unset`, merged into its spawn env). A session's worker never inherits them from whoever started it (a `chi send` run by another session's `execute`): its other commands (a user's `!cmd`, an MCP server, a context command) have none.
|
|
47
47
|
- They also pass on a set `XDG_STATE_HOME` / `XDG_CONFIG_HOME` made absolute (`Builtins.xdg_absolute`, via `Paths`), so a chi the command runs in another `cwd` (`chi send`) reads this chi's state and config. These aren't reserved: an XDG dir the model sets in `task_create`'s `env` stays its own.
|
|
48
48
|
- Every runner starts from the session's project env, `ProjectEnv.tools_env`: the overlay a spawned worker computed once at boot (`SessionManager.apply_project_env` → `ProjectEnv.compute` → `ProjectEnv.install!`; its folder's `mise env`, and the chi shim's dir first on PATH so `chi` is the chi home's), less `ProjectEnv::TOOLS_DROP` (`RUBYOPT`, `RUBYLIB`, Bundler's: the scrub above sets those). Outside a worker (`ProjectEnv::NONE`) it is `{}`. The sites: `execute` and `task_create` (`Tools::Builtins#project_env`, under the markers and the model's `env`; a participant's turn gets `{}`), the user's `!cmd` (`SessionCommands#shell`), and through `ProjectEnv.spawn_env` (the scrub above, then `tools_env`: what `execute` spawns with, for the runners that don't go through it) a context source's command (`ContextFetch#env_for`) and an MCP server, under the server's own keys (`bundles/mcp/plugin.rb` `server_env`, from mcp 0.7.4; no participant rule: the server is the session's). chi's own process env is never changed, and the project env never enters the prompt. User docs: [Project env](../sessions.md#project-env); the launch side: [How chi launches itself](../architecture.md#how-chi-launches-itself).
|
|
@@ -0,0 +1,96 @@
|
|
|
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 from provenance, so chi web needs no plugins) lists
|
|
14
|
+
who draws what: `GET /api/block_renderers`, `:owner`. `GET /api/block_renderers/:bundle/:file` serves a file, `:owner`:
|
|
15
|
+
the URL's names go through `BlockRenderers.script_path` (membership in a listed renderer's `load` before any path is
|
|
16
|
+
made), then `Provenance.sha_matches?`; a changed file is 409 `renderer_changed`. ETag + `no-cache`, as static files.
|
|
17
|
+
3. `block_host.js` (`createBlockHost`, wired in `app.js`; `participant.js` isn't): one `MutationObserver` per turn root,
|
|
18
|
+
filtered to `TEXTS` (`text_roots.js`), so cards, the asking slot, question replies and delegate reports never draw, and
|
|
19
|
+
every path that adds a block to a root is seen the same way. One `IntersectionObserver` per scroller, with an explicit `root`
|
|
20
|
+
(with `root: null`, `rootMargin` doesn't pass an `overflow: auto` in between). Closed `<details>` fire on open. **A
|
|
21
|
+
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
|
|
22
|
+
to another scroller (the stage's `.ts-scroll` hands a long answer to `#history`) is watched there, or it never draws.
|
|
23
|
+
4. `block_frame.js` (`createBlockFrame`): one hidden frame per language, made on first need, one render at a time, 15 s per
|
|
24
|
+
call. A timeout, a dead frame or a session switch drops it. Handshake `loaded` → `code {files}` → `ready | failed`,
|
|
25
|
+
then `render {id, source, theme}` → `done {id, svg} | error {id, message}`.
|
|
26
|
+
5. On success a `<figure class="block-drawn">` goes right before the `<pre>`, which stays in the DOM, `hidden`, so whatever
|
|
27
|
+
reads text (copy, a claim's walk, fold thresholds) sees what it did. **The DOM is the state** (`block_source.js`): a Revise
|
|
28
|
+
clone arrives with both. On failure the `<pre>` stays, marked `data-block-failed`, with one muted line: an attribute CSS
|
|
29
|
+
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.
|
|
30
|
+
The toggle has an icon and an `aria-label`, no text (`copy.js`'s rule). A jump into the source shows it (`revealInHistory`).
|
|
31
|
+
6. Cache: a `Map` on `lang + theme + source`, about 200 entries, holding the `data:` URL; a later `bubble.innerHTML` wipes the
|
|
32
|
+
figure and the observer redraws in the same pass. No `crypto.subtle`: it's undefined on a plain-http LAN address. A system
|
|
33
|
+
theme change redraws through the figure (a hidden `<pre>` never intersects).
|
|
34
|
+
7. The view: a viewer at the bottom stays there (`scroll.js keepFollowing`); a block drawn above a reader's view keeps
|
|
35
|
+
what they read in place (checked in Chromium and WebKit).
|
|
36
|
+
8. **A maximum picture, in the page, for any renderer** (`block_host.js oversize`): 50,000 px a side and 100M px. Mermaid's
|
|
37
|
+
secure keys don't cover diagram sizes, so a model's `%%{init:{"sequence":{"height":50000000}}}%%` otherwise gives an image
|
|
38
|
+
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;
|
|
39
|
+
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
|
|
40
|
+
from `DOMParser`, never a regex over model-influenced XML (exponents, a comment holding `<svg`, a `>` in an attribute, entities and
|
|
41
|
+
prefixes all fooled one), and a vast one isn't decoded. Then the decoded size is checked: over the maximum, or a width or
|
|
42
|
+
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
|
|
43
|
+
`max-height` and natural `max-width` say the same number, a backstop whatever the parsing gets wrong. The decode is bounded too
|
|
44
|
+
(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.
|
|
45
|
+
|
|
46
|
+
## Security model
|
|
47
|
+
|
|
48
|
+
The bundle's JS is trusted (installing a bundle already means trusting it, [plugins.md](../plugins.md#trust)). **Its input
|
|
49
|
+
is model-written**, so a renderer bug or an injected diagram must not reach a page that controls sessions.
|
|
50
|
+
|
|
51
|
+
| Layer | What it stops |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `<iframe sandbox="allow-scripts">`, never `allow-same-origin` | reading the parent; reading gated responses |
|
|
54
|
+
| 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 |
|
|
55
|
+
| 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 |
|
|
56
|
+
| `delete window.RTCPeerConnection` + `webkitRTCPeerConnection` in the bootstrap, before any bundle code | the plain WebRTC call only |
|
|
57
|
+
| Output only as `<img src="data:…">` | scripts and remote images inside the SVG don't run or load |
|
|
58
|
+
|
|
59
|
+
**Hard rules.**
|
|
60
|
+
|
|
61
|
+
1. Never `allow-same-origin`. Never `innerHTML` the frame's output. SVG goes only into a `data:` URL for an `<img>`.
|
|
62
|
+
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
|
|
63
|
+
navigate to it, where its script reads a gated route with chi's origin and cookie. Opened as a document, a `data:` SVG
|
|
64
|
+
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
|
|
65
|
+
test cookie to another port), so what holds is that the answer stays unreadable, not that no cookie is ever sent.
|
|
66
|
+
3. The page accepts messages only where `e.source === frame.contentWindow`, of known types, and treats them as data
|
|
67
|
+
(`frameMessage`). No other window `message` listener exists on the page.
|
|
68
|
+
4. Code enters the frame by postMessage from the page's own authenticated fetch (the frame's CSP closes its network but for
|
|
69
|
+
WebRTC, the residual risk below, and it has no cookie),
|
|
70
|
+
then as `script.text = code`. `eval` doesn't work: a library's `"use strict"` keeps its global local. A syntax error doesn't
|
|
71
|
+
throw: it reaches the frame's error event, as a failed load. `fetchRendererFile` requires `res.ok` and a JS content type, so a
|
|
72
|
+
proxy's sign-in HTML is never passed in as code.
|
|
73
|
+
|
|
74
|
+
**Residual risk, accepted for v1.** Code running in the frame can send what the frame holds over WebRTC: the diagram sources it
|
|
75
|
+
has rendered. A nested `srcdoc` frame gets a fresh `RTCPeerConnection` and still sends packets, in both browsers, and no CSP
|
|
76
|
+
stops WebRTC. It can't reach the page, cookies or sessions, and it needs a renderer compromised through its input. To limit
|
|
77
|
+
what it holds, the frame is dropped on a session switch.
|
|
78
|
+
|
|
79
|
+
**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
|
|
80
|
+
renderer's runtime. So a source over 64 KB stays source, and before each render a `sessionStorage` mark (`chi_block_rendering:`,
|
|
81
|
+
per block) is written and cleared after; a block whose mark is still there on load stays source with its muted line. A timeout
|
|
82
|
+
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
|
|
83
|
+
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
|
|
84
|
+
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
|
|
85
|
+
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
|
|
86
|
+
rewritten or cloned meanwhile has its blocks wait for that render (`coming`), and each draws into whichever is connected.
|
|
87
|
+
No real hang has been produced; this is a guard.
|
|
88
|
+
|
|
89
|
+
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
|
|
90
|
+
15 s and the frame is dropped. Not a concern for a working renderer, and the page stays usable meanwhile.
|
|
91
|
+
|
|
92
|
+
## Not done
|
|
93
|
+
|
|
94
|
+
Firefox isn't tested: if its CSP blocks srcdoc under `frame-src 'none'`, the frame never answers and blocks stay source.
|
|
95
|
+
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
|
|
96
|
+
loop would stall the page.
|