samagotchi 0.136.0 → 0.137.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 +42 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +15 -6
- data/docs/guardrails.md +12 -5
- data/docs/internals/engine.md +4 -4
- data/docs/internals/web-frontend.md +20 -10
- data/docs/sessions.md +1 -1
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge.rb +4 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos.rb +3 -2
- data/lib/samagotchi/engine.rb +6 -1
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/chi_dirs.rb +2 -2
- data/lib/samagotchi/guardrails/outside.rb +5 -5
- data/lib/samagotchi/guardrails/pair_boundary.rb +11 -4
- data/lib/samagotchi/guardrails/pair_command.rb +3 -19
- data/lib/samagotchi/guardrails/protected_paths.rb +5 -2
- data/lib/samagotchi/guardrails/review_paths.rb +32 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +0 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +6 -3
- data/lib/samagotchi/web/participant_review.rb +2 -1
- data/lib/samagotchi/web/public/answer_fold.js +44 -15
- data/lib/samagotchi/web/public/app.js +12 -1
- data/lib/samagotchi/web/public/chip_popover.js +20 -42
- data/lib/samagotchi/web/public/context_chips.js +16 -6
- data/lib/samagotchi/web/public/copy.js +8 -5
- data/lib/samagotchi/web/public/details_motion.js +74 -0
- data/lib/samagotchi/web/public/families_view.js +15 -6
- data/lib/samagotchi/web/public/index.html +67 -49
- data/lib/samagotchi/web/public/info_bar.js +1 -1
- data/lib/samagotchi/web/public/lists_view.js +1 -0
- data/lib/samagotchi/web/public/participants_panel.js +1 -1
- data/lib/samagotchi/web/public/popover_modal.js +57 -0
- data/lib/samagotchi/web/public/revise_stack.js +76 -6
- data/lib/samagotchi/web/public/revise_switch.js +55 -0
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/send_turn.js +4 -2
- data/lib/samagotchi/web/public/settings_chip.js +2 -2
- data/lib/samagotchi/web/public/stale_params.js +26 -0
- data/lib/samagotchi/web/public/start_settings.js +1 -1
- data/lib/samagotchi/web/public/start_toolkit.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +10 -5
- data/lib/samagotchi/web/public/working_set_rail.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +2 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bac134a85c720d669fc13d4a63f67aaced78c3078b1335a24e32639e9cce367
|
|
4
|
+
data.tar.gz: 0e7a25de11d0a1b1701671998639bc337d84262f8a26039d4f04f8d3ef6fc587
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b52a815fbe6ee0f013935933c156eee85ce4ca2dd15971cc096606dc4a57eba46756c096ecfcaaa7ab2c8b9919e80a34c2d5ee76a2053fa526195eb387a4e476
|
|
7
|
+
data.tar.gz: c1149439f9e4ce5d22368f8d634d1d2913b4e418cbfadff5f7bf61b0ca6469e3b45ea64e42240b5e9d806f5f83e5e5aa9a9810a3be45deca33a37a50a901bf26
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ 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.137.0] - 2026-10-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Web: a Revise stack's top answer has a ‹ N/M › switcher that shows an earlier version in its place (view only;
|
|
15
|
+
Copy copies the version shown; a reload shows the latest).
|
|
16
|
+
- Web: a long prompt folds like a long answer (same threshold, open by default; this browser remembers the fold).
|
|
17
|
+
- Web: every popover behaves like the ⚙ panel: a modal dialog (Tab stays in it, Esc gives focus back to its chip),
|
|
18
|
+
and on a phone a tap no longer leaves a hover tint.
|
|
19
|
+
- Web: a delegate report's code blocks get copy buttons, like an answer's. On a phone or touch screen a code
|
|
20
|
+
block's copy button sits in a strip above the first line, not over it.
|
|
21
|
+
- Web: a phone's session bar shows ⚙ on first view, right after the status.
|
|
22
|
+
- Web: the open ⚙ panel's running-turn note follows a turn starting or ending.
|
|
23
|
+
- Web: a markdown delegate report in the running turn's stage reads as one plain row.
|
|
24
|
+
- A Verify or Revise sent during a running turn always queues behind it (it no longer cuts the model's thinking
|
|
25
|
+
for a line that waits anyway); the page says it queues.
|
|
26
|
+
- Guardrails: a write to the chi shim asks, and only you can allow it, like home.json. A pair turn's plugin tool
|
|
27
|
+
call that names no command or path asks the owner.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Web: other viewers of a Verify turn no longer see a Revise offer on its answer until they reload.
|
|
32
|
+
- Web: a stale `?view=` (the removed turn view's) leaves the address bar.
|
|
33
|
+
- Desktop: the helper checks the chi home the helper's chi actually reads.
|
|
34
|
+
|
|
35
|
+
Update with `chi update`, then restart `chi web`.
|
|
36
|
+
|
|
37
|
+
## [0.136.1] - 2026-10-30
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Web: collapsibles keep their shape. A steps block, a thinking block, the recap and a long answer's fold are the
|
|
42
|
+
same box open or closed (the bar, the border, the width); a click changes the height, animated unless the system
|
|
43
|
+
prefers reduced motion. A closed answer fold is its box's one line, "▸ first line · N lines"; open, the row is the
|
|
44
|
+
box's top edge, "▾ N lines", and no longer repeats the answer's first line above the answer (a delegate report's
|
|
45
|
+
likewise, in the report's tint).
|
|
46
|
+
|
|
47
|
+
Update with `chi update`, then restart `chi web`. `chi home doctor` says what else to do.
|
|
48
|
+
|
|
10
49
|
## [0.136.0] - 2026-10-30
|
|
11
50
|
|
|
12
51
|
### Changed
|
|
@@ -5218,7 +5257,9 @@ and long-lived sessions.
|
|
|
5218
5257
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
5219
5258
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
5220
5259
|
|
|
5221
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
5260
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.137.0...HEAD
|
|
5261
|
+
[0.137.0]: https://github.com/dm1try/samagotchi/compare/v0.136.1...v0.137.0
|
|
5262
|
+
[0.136.1]: https://github.com/dm1try/samagotchi/compare/v0.136.0...v0.136.1
|
|
5222
5263
|
[0.136.0]: https://github.com/dm1try/samagotchi/compare/v0.135.0...v0.136.0
|
|
5223
5264
|
[0.135.0]: https://github.com/dm1try/samagotchi/compare/v0.134.0...v0.135.0
|
|
5224
5265
|
[0.134.0]: https://github.com/dm1try/samagotchi/compare/v0.133.0...v0.134.0
|
data/docs/architecture.md
CHANGED
|
@@ -258,10 +258,10 @@ higher-level events so UIs get clean turn boundaries without inferring them:
|
|
|
258
258
|
- `:turn_started` — `{ session_id:, prompt:, turn_id:, cut_after: }` (`cut_after`: the seconds a cut waits for this
|
|
259
259
|
model's thinking, `0` = never; plus `continue: true` for a continue turn, `resumed: true` for a step-limit Continue,
|
|
260
260
|
`images:` with the prompt's image refs)
|
|
261
|
-
- `:turn_completed` — `{ result:, turn_summary:, display_pending: }` (`result`: the final
|
|
261
|
+
- `:turn_completed` — `{ turn_id:, result:, turn_summary:, display_pending: }` (`result`: the final
|
|
262
262
|
`LLM::ModelResult`; Engine event (clients get it without `result`); `display_pending`: `after_turn` hooks may still
|
|
263
263
|
present the answer)
|
|
264
|
-
- `:turn_canceled` — `{ cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
|
|
264
|
+
- `:turn_canceled` — `{ turn_id:, cancellation_reason:, cancelled_by:, duration_ms: }` (`cancelled_by`: the bundle or hook label that
|
|
265
265
|
stopped it, for `cancellation_reason: :hook`; else nil)
|
|
266
266
|
|
|
267
267
|
These are the Engine's raw events. The worker's SSE projection omits `call` from
|
data/docs/cli.md
CHANGED
|
@@ -1023,15 +1023,20 @@ hides it for that browser (`chi_markdown_tip_dismissed` in localStorage).
|
|
|
1023
1023
|
Every prompt and answer has a copy button (on hover; always shown, dimmed, on a
|
|
1024
1024
|
touch screen), and so does each code block of a rendered answer. An answer
|
|
1025
1025
|
copies its Markdown source, not the rendered text; a code block copies just
|
|
1026
|
-
its code; a prompt copies the text as you typed it.
|
|
1026
|
+
its code; a prompt copies the text as you typed it. On a phone or a touch
|
|
1027
|
+
screen a code block's button sits in a strip above its first line, clear of
|
|
1028
|
+
the code.
|
|
1027
1029
|
|
|
1028
1030
|
A long answer (over 25 lines or 2,000 characters of its text) can be folded
|
|
1029
|
-
by hand: a
|
|
1030
|
-
|
|
1031
|
+
by hand: a "▾ N lines" row is its top edge once its turn has ended. Click
|
|
1032
|
+
it and the answer closes to one line in its own box,
|
|
1033
|
+
"▸ its first line · N lines" (the
|
|
1031
1034
|
first line of what a hook presented in its place, else its first heading,
|
|
1032
1035
|
else its first line); click again to unfold. A delegate report's reply
|
|
1033
|
-
folds the same way
|
|
1034
|
-
|
|
1036
|
+
folds the same way, and so does a long prompt you sent (over the same
|
|
1037
|
+
threshold; its folded row shows its first line as typed). A delegate
|
|
1038
|
+
report itself starts folded. Folding is this browser's choice, kept per session in
|
|
1039
|
+
localStorage (`chi_answer_fold:<session id>`, the last 200 folded answers and prompts;
|
|
1035
1040
|
the keys of sessions gone from the list are dropped when the list loads);
|
|
1036
1041
|
other viewers and the participant page are not affected. A jump into a
|
|
1037
1042
|
folded answer (a claim link, say) unfolds it for that page load.
|
|
@@ -1049,7 +1054,11 @@ steps and its answer, all behind it (a warn card, a verdict card and the
|
|
|
1049
1054
|
recap stay on view). Click the stub to unfold that version in place (kept per
|
|
1050
1055
|
browser, with the folded answers); a time divider between two folded stubs
|
|
1051
1056
|
moves into the later stub. The newest version has a chip, "v3 · 2 earlier ▾",
|
|
1052
|
-
listing every version by its prompt; pick one to jump to its stub.
|
|
1057
|
+
listing every version by its prompt; pick one to jump to its stub. Before
|
|
1058
|
+
that chip, a "‹ 3/3 ›" switcher reads an earlier version's answer in the
|
|
1059
|
+
newest one's place (its left edge dashed while it shows; Copy copies the
|
|
1060
|
+
version on view); › back to the newest. Switching only changes what this
|
|
1061
|
+
page shows, and a reload shows the newest again. Revise
|
|
1053
1062
|
again on the newest version adds to the stack. Stacks are session data (the
|
|
1054
1063
|
stamp is on the Revise prompt, see [sessions.md](sessions.md), Revise
|
|
1055
1064
|
stacks): every owner's page shows them, live and after a reload; the
|
data/docs/guardrails.md
CHANGED
|
@@ -4,7 +4,7 @@ Every tool call the model makes passes one check before it runs, in both
|
|
|
4
4
|
loops (native and chat hosts). The verdict is **allow**, **ask** or
|
|
5
5
|
**deny**; the strictest vote wins, and a deny can't be undone by a later
|
|
6
6
|
voter. Of two asks the first one names the rule, except a rule only the
|
|
7
|
-
user may allow (`chi-config`, `chi-home`, `chi-hooks`, `shell-touches-chi`,
|
|
7
|
+
user may allow (`chi-config`, `chi-home`, `chi-shim`, `chi-hooks`, `shell-touches-chi`,
|
|
8
8
|
`chi-context-cmd`, `chi-broadcast`): it takes over, and only the scopes both asks offer
|
|
9
9
|
are offered.
|
|
10
10
|
|
|
@@ -221,7 +221,7 @@ rules off: `guardrails: {disable: [write-outside-repo, git-outside-repo]}`.
|
|
|
221
221
|
Whatever the setting, a parent can't allow a call on chi's own files: a
|
|
222
222
|
`write`, `edit` or `execute` on the config dir (config.yml, installed bundles'
|
|
223
223
|
rules), the hooks dir or the approval store, by its rule (`chi-config`, `chi-home`,
|
|
224
|
-
`chi-hooks`, `shell-touches-chi`), its paths or its command. "Allow once" on a
|
|
224
|
+
`chi-shim`, `chi-hooks`, `shell-touches-chi`), its paths or its command. "Allow once" on a
|
|
225
225
|
config.yml rewrite would allow everything from then on, so those questions are
|
|
226
226
|
the user's alone (`user_only`): `chi answer` and the worker refuse the allow
|
|
227
227
|
with `only the user can allow it`. The same holds for the guardrails bundle's
|
|
@@ -285,7 +285,7 @@ Built in, for `write`, `edit` and `memory_write` (symlinks resolved; see
|
|
|
285
285
|
- deny: the approval store's dir, installed bundles (`memories/.bundles/`),
|
|
286
286
|
and [attached context](context.md) sources (`<state dir>/context/`: a
|
|
287
287
|
source's command runs later, outside the gate; `chi context` changes them);
|
|
288
|
-
- ask (once or for the session): `config.yml`, `home.json` (the [chi home](configuration.md#the-chi-home): the Ruby every chi launch boots) and the plain hooks dir.
|
|
288
|
+
- ask (once or for the session): `config.yml`, `home.json` (the [chi home](configuration.md#the-chi-home): the Ruby every chi launch boots), the chi shim (`$XDG_STATE_HOME/samagotchi/bin/chi`, the `chi` every tool runs: `chi-shim`) and the plain hooks dir.
|
|
289
289
|
|
|
290
290
|
`execute` can still reach them; the guardrails bundle asks about shell
|
|
291
291
|
commands that name them.
|
|
@@ -497,7 +497,10 @@ message:
|
|
|
497
497
|
`ruby -run -e httpd`, `php -S`, a static server, a script's `listen(`/
|
|
498
498
|
`HTTPServer`/`createServer`): the owner's browser sends its cookies (chi
|
|
499
499
|
web's on 127.0.0.1 or the LAN address, kept for every port) to the page
|
|
500
|
-
it opens (`PairBoundary::OPENER`, `PairBoundary::LISTENER`)
|
|
500
|
+
it opens (`PairBoundary::OPENER`, `PairBoundary::LISTENER`). The list
|
|
501
|
+
names generic servers only: a project's dev server (`npm run dev`, `npx
|
|
502
|
+
vite`, `bundle exec rails server`) runs and listens until the execute's
|
|
503
|
+
limit kills it (above);
|
|
501
504
|
- a program whose name the shell expands: a `$var`, a `$(…)` or a glob in a
|
|
502
505
|
command's name (`bin/ch? …`, `e=ch; ${e}i …`, `$CMD x`), whatever it would
|
|
503
506
|
turn out to be (`PairBoundary::VERB`);
|
|
@@ -552,6 +555,10 @@ share`, `/bin/SH -c`, `NOHUP`: a disk that folds case, macOS's default, runs
|
|
|
552
555
|
them as git, chi, sh, nohup; `ShellLex.program_name`), and so is chi or git
|
|
553
556
|
config in a script's text.
|
|
554
557
|
|
|
558
|
+
It asks the owner, fresh and once, before a plugin or MCP tool's call whose
|
|
559
|
+
`targets:` name no command or path (`PairBoundary::UNTARGETED`: a browser
|
|
560
|
+
MCP's navigate, say), since nothing here can tell what it reaches.
|
|
561
|
+
|
|
555
562
|
And it **asks the owner** before a `write` or `edit` that may land outside the
|
|
556
563
|
session's project root, and before a command that may write there, every time:
|
|
557
564
|
the ask is fresh (`Verdict#fresh_ask`: no stored approval answers it, and no
|
|
@@ -913,7 +920,7 @@ every surface answers for the same paths):
|
|
|
913
920
|
| Primitive | Where | Used by |
|
|
914
921
|
|---|---|---|
|
|
915
922
|
| expand (`..` the kernel's way), real, lexical_real, reals | `PathResolve` (`ProtectedPaths` keeps the old names) | every surface |
|
|
916
|
-
| in a dir: `under?` (case-folded on a folding disk; `fold: false` exact), `strictly_under?`, `within?` (root resolved too) | `PathResolve` | `under?`: ShellPaths, ReviewPaths, PairCommand, ChildBoundary, SelfProtect's words; `Outside.
|
|
923
|
+
| in a dir: `under?` (case-folded on a folding disk; `fold: false` exact), `strictly_under?`, `within?` (root resolved too) | `PathResolve` | `under?`: ShellPaths, ReviewPaths, PairCommand, ChildBoundary, SelfProtect's words; `Outside.under_folding_existing?` and RmTargets' `strictly_under?` fold only when the dir exists (a missing session root keeps the case it was typed in); `within?`: ProtectedPaths, ScratchWrites, ParentApprovals, SelfProtect's paths, ChiDirs.review (`ProtectedPaths.within?` is the same predicate) |
|
|
917
924
|
| tmp dirs and "not one the session is in" | `PathResolve.tmp_roots(session:)` (exact case) | Outside (`Outside.tmp_roots` is the list it and Targets read), RmTargets; `touches_chi?` takes all of them |
|
|
918
925
|
| a shell word's `~`, `$HOME`, XDG dirs, `$TMPDIR` | `PathResolve.expand_word` | ShellPaths::Walk, ShellGitDirs; PairCommand has its own `~`/`$HOME` (`expand_tilde`) |
|
|
919
926
|
| a program word's name: basename, case-folded on every disk (`GIT`, `/bin/SH`, `ſh`) | `ShellLex.program_name` / `.program?` | PairCommand, ImplicitWriters, SelfProtect's file runners, ShellGitDirs, SharedGit |
|
data/docs/internals/engine.md
CHANGED
|
@@ -79,7 +79,7 @@ Engine#run_turn(session, prompt, on_event:, max_iterations:, cancel_controller:,
|
|
|
79
79
|
│ # {cancellation_reason, cancelled_by, duration_ms} is emitted; before the replace,
|
|
80
80
|
│ # stamp_opening puts a Verify turn's `verify` and a completed Revise turn's `revises`
|
|
81
81
|
│ # (re-targeted to its stack's top) on the opening message, metadata never sent, and
|
|
82
|
-
│ # :turn_completed
|
|
82
|
+
│ # every end event carries `turn_id`, :turn_completed `verify` / `verdict` / `revises` when there are
|
|
83
83
|
│ # then the :after_turn hook (status, messages, present: the AnswerDisplay presenter,
|
|
84
84
|
│ # refs: the AnswerRefs adder)
|
|
85
85
|
│ # store_answer_display (one replace of the answer: `display` and/or `display_refs`;
|
|
@@ -548,9 +548,9 @@ Turn events (the main ones; `Events` in `events.rb` keeps the shared sets):
|
|
|
548
548
|
| `:working_set_updated` | after `:generation_started` (the running turn's conversation, the turn's strategy) and once in `end_turn` before the end event (the kept messages), only when the set differs from the last one emitted for the session and is not empty, except one empty event when the last one had items (a rollback emptied the set): a tool-free turn emits none (`Engine#emit_working_set`); between turns `Engine#announce_working_set` announces it under the next turn's strategy (after a `/working-set` op, held behind the command's `command_ran`) | `{items: [{kind, key, state (live\|stale\|forgotten\|dropped), tokens, ops, count, first_id, pinned, has_drop}], at, adds?: [{kind, key, label}]}` (`WorkingSet`, internals/working-set.md; `adds` while the user's adds are pending) |
|
|
549
549
|
| `:hook_notice` | a hook's or plugin's notify during a turn | `{hook, text, level}` (announced with `between_turns: true` outside a turn) |
|
|
550
550
|
| `:guardrail_warning` | a guardrails line during a turn | `{message}` |
|
|
551
|
-
| `:turn_completed` | normal end | Engine event `{result, turn_summary, display_pending}`, plus `verdict` (a Verify turn's recorded verdict) and `revises` (a Revise turn's stamp, `{turn_id}`) when there are; the Bridge's SSE frame omits `result` (answer remains in `turn_summary.output`) |
|
|
552
|
-
| `:turn_canceled` | Stop, hook stop, Ctrl-C | `{cancellation_reason, cancelled_by?, duration_ms}` |
|
|
553
|
-
| `:turn_failed` | an error | `{error_class, message, duration_ms, error_kind?, retryable?, host?, summary?}` |
|
|
551
|
+
| `:turn_completed` | normal end | Engine event `{turn_id, result, turn_summary, display_pending}`, plus `verify: true` (a Verify turn, verdict or not: the page's `data-verify`), `verdict` (a Verify turn's recorded verdict) and `revises` (a Revise turn's stamp, `{turn_id}`) when there are; the Bridge's SSE frame omits `result` (answer remains in `turn_summary.output`) |
|
|
552
|
+
| `:turn_canceled` | Stop, hook stop, Ctrl-C | `{turn_id, cancellation_reason, cancelled_by?, duration_ms}` |
|
|
553
|
+
| `:turn_failed` | an error | `{turn_id, error_class, message, duration_ms, error_kind?, retryable?, host?, summary?}` |
|
|
554
554
|
| `:answer_display` | after the after_turn hooks (observers only, not the sink) | `{display, refs?}` (display a string, or nil; refs the count of the answer's refs, only when a hook attached some) |
|
|
555
555
|
|
|
556
556
|
A turn's origin is a `TurnOrigin` (`lib/samagotchi/turn_origin.rb`, a `Data.define` of `client_id`, `enqueued_id`
|
|
@@ -10,6 +10,10 @@ only library. Pure modules (`*_model.js`, `session_state.js`, `sessions_list.js`
|
|
|
10
10
|
and have `node --test` specs under `spec/web/public/` (happy-dom for the DOM ones); the pages as a whole are covered
|
|
11
11
|
by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec.
|
|
12
12
|
|
|
13
|
+
**`app.js` has a size cap**: `npm run lint` fails past `max-lines` in `eslint.config.js` (counted as `wc -l` counts,
|
|
14
|
+
a little over its size today). A new feature goes into a module of its own, wired from `app.js` in a few lines; a
|
|
15
|
+
refactor that shrinks `app.js` ratchets the cap down with it. Never raise it: extract instead.
|
|
16
|
+
|
|
13
17
|
**New exports go in new modules.** The static JS is served at unversioned URLs. Today every load revalidates
|
|
14
18
|
(app.rb `serve_static` sends `Cache-Control: no-cache` with a content ETag, and `sw.js` has no fetch handler), but an
|
|
15
19
|
open tab still holds the modules it loaded, and a proxy that ignores `no-cache` can serve an old one: a new export
|
|
@@ -17,9 +21,9 @@ on an existing module, imported by a new version of another, then finds the old
|
|
|
17
21
|
page. A factory dep a new version stops passing is the same hazard the other way round: the store's redraws took
|
|
18
22
|
`updateInfoBar` and the chip controls out of `stream_handlers.js`'s deps (and `updateInfoBar`/`updateComposerMode`
|
|
19
23
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
20
|
-
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`,
|
|
24
|
+
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
21
25
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
22
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js` and `
|
|
26
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js` and `popover_modal.js` are new modules for that reason.
|
|
23
27
|
|
|
24
28
|
## Two pages, two models
|
|
25
29
|
|
|
@@ -46,7 +50,7 @@ change.
|
|
|
46
50
|
| **`store.current`** (`session_state.js`: `emptySessionState` / `sessionStateFrom`; per field: `session_fields.js FIELDS`) | the open session's fields, about 40 (info bar, chips, verdicts, timing, worker, stream cursor) | `store.update` / `setStatus` from app.js, `stream_handlers.js`, `send_turn.js` (`firstPreview`, `owner`), `session_actions.js` (`status`); `forgetSession`'s `replaceCurrent`. Two writers in place, on purpose (D8 in `FIELDS`): live_timing.js's `activeTurn`, verdict_fresh.js's running list | the store runs the field's redraws (`FIELDS[key].redraws`) |
|
|
47
51
|
| **`store.sessions`** (the hub's projection) | every card | `store.update({sessions})` from app.js: `openSessionEvents` (`applySessionEvent`), `refresh` (the list fallback), `addCreatedSession`, `applyView` (re-sorted), `replaceSelectedCard` (`setLiveStatus` through `setStatus`, `readTurnEnd`, `setUsedMemories`), the stream's close (`withoutWorker`); `session_actions.js` (`setSessions`) | the store: `lists` and `infoBar`. **Always a new array**; an entry is replaced, never changed in place. `followSelectedSession` copies the open one's owner and worker (`fieldsFromCard`) and, when not streaming, its status (`setStatus(…, "hub")`) into `store.current` |
|
|
48
52
|
| **Factory closures**: the `create*` factories (popovers, panels, views) | each component's own state: open-for, busy, error, its own fetched list | the factory | an `onChange` hook back into app.js, or a getter the factory reads at draw time |
|
|
49
|
-
| **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), kept command bubbles, which texts were shown, unnamed badges, folded answers (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
|
|
53
|
+
| **The history DOM** | the conversation as drawn: which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
|
|
50
54
|
| **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
|
|
51
55
|
| **SSE** | see Streams | the server | owner page: events go into the DOM directly; participant page: an event marks the data stale |
|
|
52
56
|
|
|
@@ -104,10 +108,11 @@ a store write. A value written unchanged redraws nothing (a chunk with the same
|
|
|
104
108
|
An input the store doesn't hold calls `store.touch(name)` where it changes: `infoBar` for the clock (liveTurn's
|
|
105
109
|
`onTick`), the versions (`noticeVersions`), `restartingIds`, the stream (`startStream`, `workerGone`), the working
|
|
106
110
|
set's and the participants' `onChange`, the continue and review in-flight sets
|
|
107
|
-
(`session_actions.js`); `composer` and `infoBar` for `liveTurn.running` (`setTurnRunning
|
|
108
|
-
`renderSessionView` (a whole view drawn). Left as they are,
|
|
109
|
-
`updateComposerMode()` (the bar doesn't read it),
|
|
110
|
-
`
|
|
111
|
+
(`session_actions.js`); `composer` and `infoBar` for `liveTurn.running` (`setTurnRunning`, with `settingsPanel`:
|
|
112
|
+
the open ⚙ panel's running-turn note) and at the end of `renderSessionView` (a whole view drawn). Left as they are,
|
|
113
|
+
on purpose: the `narrowScreen` listener calls `updateComposerMode()` (the bar doesn't read it),
|
|
114
|
+
`applyStripHidden`/`applyView` call `updateSessionTitle()`, and `closeStream`/`stream.restart` set
|
|
115
|
+
`streamControl = null` without a redraw (a resync or a workerGone follows).
|
|
111
116
|
|
|
112
117
|
A function that writes more than once batches its synchronous part, so each redraw runs once: `select` (before its
|
|
113
118
|
`await resync()`), `clearSelection`, the hub's `snapshot` and `session` events, `workerGone`, `setTurnRunning`,
|
|
@@ -197,8 +202,13 @@ Three, for one job. Pick by what the piece is:
|
|
|
197
202
|
|
|
198
203
|
Listeners: every factory is a page-lifetime singleton, so `addEventListener` without a remove is fine; a per-render
|
|
199
204
|
listener lands on a fresh node. A popover goes through `chip_popover.js createChipPopover` (its own outside-click,
|
|
200
|
-
Esc, placement and resize); `context_chips.js
|
|
201
|
-
|
|
205
|
+
Esc, placement and resize); `context_chips.js` and the lists' families/chains popovers (`families_view.js
|
|
206
|
+
createFamilyPop`) still place their own. Every popover behaves the same (`popover_modal.js`): a modal dialog
|
|
207
|
+
(`modalDialog`: `aria-modal`, Tab wraps inside it), the opener's `aria-expanded` (a lit-drawn chip writes
|
|
208
|
+
`aria-expanded="false"` statically; the shell flips it), Esc closes it and focus goes back to the opener
|
|
209
|
+
(`focusBack`), and its `:hover` tints (the opener's too) sit in `@media(hover:hover)`, so a tap on a phone leaves
|
|
210
|
+
none behind. `model_picker.js`, `command_list.js` (listboxes: the focus stays in the input), `ref_menu.js` (a menu)
|
|
211
|
+
and `update_sheet.js` (a native modal `<dialog>`) aren't popovers in that sense.
|
|
202
212
|
|
|
203
213
|
## Stale answers
|
|
204
214
|
|
|
@@ -256,7 +266,7 @@ reach another device or the server.
|
|
|
256
266
|
| `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
|
|
257
267
|
| `chi_update_available_shown` | app.js | update notices already shown |
|
|
258
268
|
| `chi_notify`, `chi_push_device` | `notifications.js` (an injected storage) | the bell's state, this device's push id |
|
|
259
|
-
| `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
|
|
269
|
+
| `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers and long prompts (`<turn id>:prompt`) folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
|
|
260
270
|
| `chi_review_history:<grant>`, `chi_review_history_session:<session>` | `review_history.js` (participant page) | a participant's composer history |
|
|
261
271
|
|
|
262
272
|
sessionStorage: `chi_client_id` (`turn_events.js`: this tab's client id, kept over a reload mid-turn) and
|
data/docs/sessions.md
CHANGED
|
@@ -6,7 +6,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
6
6
|
|
|
7
7
|
**Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
|
|
8
8
|
|
|
9
|
-
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`Engine#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`: the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data
|
|
9
|
+
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`Engine#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
|
|
10
10
|
|
|
11
11
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
12
12
|
|
data/docs/testing.md
CHANGED
|
@@ -22,7 +22,8 @@ with two rules only, `no-undef` (a name used but never imported or declared: a d
|
|
|
22
22
|
style pass: add a rule only for a bug class it would have caught. It runs in about 2 s, in CI's `npm test` job and in
|
|
23
23
|
`rake release:check`; `rake lint` stays RuboCop alone (CI's rubocop job has no node). A name that is also a browser
|
|
24
24
|
global (`name`, `status`, `open`…) passes even when its import is gone: give an export such a name only with care.
|
|
25
|
-
It also
|
|
25
|
+
It also caps `app.js`'s size (`max-lines`; see docs/internals/web-frontend.md).
|
|
26
|
+
It also checks `spec/e2e/` for two things: `no-unused-vars`, and the Playwright twin of `Samagotchi/SpecSleep`: no `page.waitForTimeout`.
|
|
26
27
|
Wait for the state (a locator's `expect`, `data-phase`, an animation's end through `waitForFunction`); time the page
|
|
27
28
|
keeps (a pointer's dwell, a hold) runs on Playwright's `page.clock` (`install`, then `pauseAt` and `runFor`: the
|
|
28
29
|
stage's hand-off dwell in stage.spec.js); a pause that is the behaviour outside the page (a slow post held in a
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -1108,6 +1108,10 @@ module Samagotchi
|
|
|
1108
1108
|
pending_limit = fetched(parsed, "pending_limit")
|
|
1109
1109
|
pending_limit = nil unless pending_limit.is_a?(Integer) && pending_limit.positive?
|
|
1110
1110
|
end
|
|
1111
|
+
# A Verify or Revise turn is a turn of its own too (the worker holds
|
|
1112
|
+
# it for the turn's end, SessionInbox.turn_end_data?): never a cut,
|
|
1113
|
+
# and the ack and :turn_enqueued say it queues behind a running turn.
|
|
1114
|
+
delivery = Delivery::QUEUE unless anchors.empty?
|
|
1111
1115
|
|
|
1112
1116
|
# A session command sent as a message (chi send -m "/model x", chi -p,
|
|
1113
1117
|
# a UI whose command list lags) runs as the command, as typed in a
|
|
@@ -535,7 +535,8 @@ module Samagotchi
|
|
|
535
535
|
# Why a launch argv (strings) can't run chi, or nil: its ruby isn't an
|
|
536
536
|
# absolute path, an absolute path in it is gone, or it boots the chi
|
|
537
537
|
# home (home.json's ruby) and the home can't boot chi
|
|
538
|
-
# (ChiHome.problem).
|
|
538
|
+
# (ChiHome.problem). home.json is the one the helper's chi reads
|
|
539
|
+
# (#launch_config_home), not this process's.
|
|
539
540
|
def argv_problem(argv)
|
|
540
541
|
return "the launch file's ruby #{argv[0]} isn't an absolute path: run chi desktop install --force" unless argv[0].start_with?("/")
|
|
541
542
|
|
|
@@ -546,7 +547,7 @@ module Samagotchi
|
|
|
546
547
|
return nil unless home_boot?(argv)
|
|
547
548
|
|
|
548
549
|
home = begin
|
|
549
|
-
ChiHome.read
|
|
550
|
+
ChiHome.read(path: ChiHome.path(env: { "XDG_CONFIG_HOME" => launch_config_home }))
|
|
550
551
|
rescue ChiHome::Invalid
|
|
551
552
|
nil
|
|
552
553
|
end
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -1627,11 +1627,16 @@ module Samagotchi
|
|
|
1627
1627
|
# event]. +save:+ also saves the session (a failed turn: a worker exits
|
|
1628
1628
|
# after it). The disk writes come after the step: every emitter waits
|
|
1629
1629
|
# for the event lock. +exhausted:+ the turn ran out of iterations
|
|
1630
|
-
# (its limit goes in last_turn).
|
|
1630
|
+
# (its limit goes in last_turn). The end event names its turn
|
|
1631
|
+
# (`turn_id:`, as :turn_started does) and, for a Verify turn,
|
|
1632
|
+
# `verify: true` (its opening's stamp: a page watching it live offers
|
|
1633
|
+
# no Revise on its answer, verdict or not, as a reload does).
|
|
1631
1634
|
def end_turn(turn, outcome, save: false, exhausted: false)
|
|
1632
1635
|
seconds = turn.elapsed
|
|
1633
1636
|
synchronize_events do
|
|
1634
1637
|
kept, event = yield(seconds)
|
|
1638
|
+
event[:turn_id] = turn.id
|
|
1639
|
+
event[:verify] = true if turn.verify
|
|
1635
1640
|
revises = stamp_opening(kept, turn, outcome)
|
|
1636
1641
|
replace_session_messages(turn.session, kept) if kept
|
|
1637
1642
|
verdict = record_verdict(turn, outcome)
|
|
@@ -266,13 +266,15 @@ module Samagotchi
|
|
|
266
266
|
require_relative "memory_bundle/provenance"
|
|
267
267
|
require_relative "context_sources"
|
|
268
268
|
require_relative "chi_home"
|
|
269
|
+
require_relative "chi_shim"
|
|
269
270
|
Guardrails::ProtectedPaths.new(
|
|
270
271
|
store_dir: File.dirname(@approvals.path),
|
|
271
272
|
bundles_dir: MemoryBundle::Provenance.bundles_dir,
|
|
272
273
|
config_path: Samagotchi::ConfigFile.global_path,
|
|
273
274
|
hooks_dir: Hooks::Loader.hooks_dir,
|
|
274
275
|
context_dir: ContextSources.root,
|
|
275
|
-
home_path: ChiHome.path
|
|
276
|
+
home_path: ChiHome.path,
|
|
277
|
+
shim_path: ChiShim.path
|
|
276
278
|
)
|
|
277
279
|
end
|
|
278
280
|
end
|
|
@@ -50,11 +50,11 @@ module Samagotchi
|
|
|
50
50
|
.map { |dir| PathResolve.real(dir.to_s.chomp("/")) }.reject(&:empty?).uniq
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
# chi's state dir and config dir
|
|
53
|
+
# chi's state dir and config dir (as .protected finds it), and the hooks
|
|
54
54
|
# dir when config.yml puts it outside the config dir (hooks run as the
|
|
55
55
|
# owner: a participant reads none of them).
|
|
56
56
|
def review
|
|
57
|
-
config =
|
|
57
|
+
config = ConfigFile.config_dir
|
|
58
58
|
hooks = begin Hooks::Loader.hooks_dir rescue nil end
|
|
59
59
|
hooks = nil if hooks.to_s.empty? || PathResolve.reals(hooks).any? { |real| PathResolve.within?(real, config) }
|
|
60
60
|
[Paths.state_dir, config, hooks].compact
|
|
@@ -28,9 +28,9 @@ module Samagotchi
|
|
|
28
28
|
session = PathResolve.real(root)
|
|
29
29
|
tmp = nil
|
|
30
30
|
reals.any? do |real|
|
|
31
|
-
next false if
|
|
31
|
+
next false if under_folding_existing?(real, session)
|
|
32
32
|
next false if MemoryBundle::IndexSync.memory_scope(path)
|
|
33
|
-
next false if (tmp ||= session_tmp_roots(session)).any? { |dir|
|
|
33
|
+
next false if (tmp ||= session_tmp_roots(session)).any? { |dir| under_folding_existing?(real, dir) }
|
|
34
34
|
|
|
35
35
|
true
|
|
36
36
|
end
|
|
@@ -45,7 +45,7 @@ module Samagotchi
|
|
|
45
45
|
def self.inside?(path, root:)
|
|
46
46
|
reals = resolve(path) or return false
|
|
47
47
|
session = PathResolve.real(root)
|
|
48
|
-
reals.all? { |real|
|
|
48
|
+
reals.all? { |real| under_folding_existing?(real, session) }
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Whether a shell command may write +path+ as a tmp file (the child
|
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
reals = resolve(path) or return false
|
|
58
58
|
session = PathResolve.real(root)
|
|
59
59
|
tmp = session_tmp_roots(session)
|
|
60
|
-
reals.all? { |real| tmp.any? { |dir|
|
|
60
|
+
reals.all? { |real| tmp.any? { |dir| under_folding_existing?(real, dir) } }
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# +path+ resolved (PathResolve.reals: the lexical answer and where
|
|
@@ -91,7 +91,7 @@ module Samagotchi
|
|
|
91
91
|
# on a disk that folds case when +root+ exists (resolved, it has its
|
|
92
92
|
# on-disk case), by their exact text when it doesn't (a missing root
|
|
93
93
|
# keeps the case it was typed in).
|
|
94
|
-
def self.
|
|
94
|
+
def self.under_folding_existing?(path, root) = PathResolve.under?(path, root, fold: File.exist?(root))
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
end
|
|
@@ -100,6 +100,10 @@ module Samagotchi
|
|
|
100
100
|
NET = "a participant's turn runs a command chi can't read for sure (git or chi behind a program it doesn't " \
|
|
101
101
|
"know, or a $(…) kept in a value the shell expands later), so the owner approves it"
|
|
102
102
|
VERB = "a participant's turn runs only a program named plainly: no $, $(…) or glob in a command's name"
|
|
103
|
+
# A plugin or MCP tool whose targets: name no command or path (a
|
|
104
|
+
# browser MCP's navigate, say): nothing here can tell what it reaches.
|
|
105
|
+
UNTARGETED = "a participant's turn calls a plugin tool chi can't see the targets of (no command or path), " \
|
|
106
|
+
"so the owner approves it"
|
|
103
107
|
UNREADABLE = "a participant's turn runs only commands chi can read: split this command, or write the file " \
|
|
104
108
|
"with the write tool"
|
|
105
109
|
# A pair turn's execute runs without the environment variables whose
|
|
@@ -135,7 +139,7 @@ module Samagotchi
|
|
|
135
139
|
ParentApprovals::CHI_TEXT,
|
|
136
140
|
%r{\.local/state/samagotchi|web-grants|grants\.json|web-token},
|
|
137
141
|
%r{(?:\A|[\s/'"=])\.(?:ssh|gnupg|aws|kube|docker)(?![\w.-])},
|
|
138
|
-
%r{(?:\A|[\s/'"=])(?:\.env(?:\.[\w.-]*)?|\.envrc
|
|
142
|
+
%r{(?:\A|[\s/'"=])(?:\.env(?:\.[\w.-]*)?|\.envrc|#{MisePaths::LOCAL_FILE}|#{MisePaths::LOCAL_DIR_FILE}|[._]netrc|\.npmrc|\.pypirc|
|
|
139
143
|
\.git-credentials)(?![\w-])}x,
|
|
140
144
|
/\bid_(?:rsa|dsa|ecdsa|ed25519)\b|credentials|\.(?:pem|p12|pfx|tfstate)\b/
|
|
141
145
|
)
|
|
@@ -199,9 +203,12 @@ module Samagotchi
|
|
|
199
203
|
# (LINK), one that runs mise or direnv (ENV_LOADER), a write to mise's
|
|
200
204
|
# config (MISE_CONFIG), a command chi can't read for sure (NET:
|
|
201
205
|
# PairCommand#net_ask?), a write that may land outside the project (OUTSIDE_WRITE, or
|
|
202
|
-
# NO_PROJECT without one)
|
|
206
|
+
# NO_PROJECT without one), a plugin tool's call that names no command
|
|
207
|
+
# or path (UNTARGETED: Targets#args is set for plugin tools only); nil
|
|
208
|
+
# when nothing asks.
|
|
203
209
|
def ask_reason(verdict)
|
|
204
210
|
targets = verdict.targets
|
|
211
|
+
return UNTARGETED if targets&.args && !targets.command && targets.paths.compact.empty?
|
|
205
212
|
return LINK if targets&.command && command(targets, nil).links?
|
|
206
213
|
return ENV_LOADER if targets&.command && command(targets, nil).loads_env?
|
|
207
214
|
return MISE_CONFIG if mise_config_write?(verdict)
|
|
@@ -211,7 +218,7 @@ module Samagotchi
|
|
|
211
218
|
project_root ? OUTSIDE_WRITE : NO_PROJECT
|
|
212
219
|
end
|
|
213
220
|
|
|
214
|
-
# A write or edit of a mise config file (
|
|
221
|
+
# A write or edit of a mise config file (MisePaths.config?), or a
|
|
215
222
|
# command that writes or names one (PairCommand#mise_config_write?).
|
|
216
223
|
def mise_config_write?(verdict)
|
|
217
224
|
targets = verdict.targets
|
|
@@ -219,7 +226,7 @@ module Samagotchi
|
|
|
219
226
|
return command(targets, nil).mise_config_write? if targets.command
|
|
220
227
|
return false unless WRITE_TOOLS.include?(verdict.call&.dig(:name).to_s)
|
|
221
228
|
|
|
222
|
-
targets.paths.to_a.compact.any? { |path|
|
|
229
|
+
targets.paths.to_a.compact.any? { |path| MisePaths.config?(File.expand_path(path, targets.cwd || Dir.pwd)) }
|
|
223
230
|
end
|
|
224
231
|
|
|
225
232
|
# A write or edit whose path may land outside the project root, or a
|
|
@@ -273,22 +273,6 @@ module Samagotchi
|
|
|
273
273
|
Outside.inside?(path, root: root) || Outside.tmp_write?(path, root: root)
|
|
274
274
|
end
|
|
275
275
|
|
|
276
|
-
# mise's config files (mise.toml, .mise.toml, mise.<env>.toml, .config/mise.toml,
|
|
277
|
-
# anything under .config/mise/, mise/ or .mise/ config*.toml, and the
|
|
278
|
-
# .tool-versions it reads too): mise runs their [env] at the next worker
|
|
279
|
-
# start (ProjectEnv), and installs the tools they pin.
|
|
280
|
-
MISE_CONFIG_PATH = %r{(?:\A|/)(?:\.?mise(?:\.[^/]+)?\.toml|\.config/mise\.toml|\.config/mise/.+|
|
|
281
|
-
\.?mise/config[^/]*\.toml|\.tool-versions)\z}x
|
|
282
|
-
MISE_CONFIG_TEXT = %r{(?<![\w-])(?:\.?mise(?:\.[\w.-]+)?\.toml|\.config/mise|\.?mise/config[\w.-]*\.toml|
|
|
283
|
-
\.tool-versions(?![\w.-]))}x
|
|
284
|
-
|
|
285
|
-
# Whether +path+ is one of mise's config files (MISE_CONFIG_PATH), as
|
|
286
|
-
# given or as it resolves.
|
|
287
|
-
def self.mise_config?(path)
|
|
288
|
-
path.to_s.match?(MISE_CONFIG_PATH) ||
|
|
289
|
-
(path.to_s.start_with?("/") && PathResolve.reals(path).any? { |real| real.match?(MISE_CONFIG_PATH) })
|
|
290
|
-
end
|
|
291
|
-
|
|
292
276
|
# Whether +path+ is in a .git/hooks folder, as given or as it resolves
|
|
293
277
|
# (ShellPaths::HOOKS_PART): git runs what is there.
|
|
294
278
|
def self.git_hook?(path)
|
|
@@ -624,7 +608,7 @@ module Samagotchi
|
|
|
624
608
|
end
|
|
625
609
|
|
|
626
610
|
# Whether it writes (or, not only reading, names) a mise config file
|
|
627
|
-
# (.
|
|
611
|
+
# (MisePaths.config?), a script it hands on included: the owner approves
|
|
628
612
|
# it. True for one it can't read.
|
|
629
613
|
def mise_config_write?
|
|
630
614
|
refusal
|
|
@@ -780,7 +764,7 @@ module Samagotchi
|
|
|
780
764
|
reason = if SHELLS.include?(program) || program == "eval"
|
|
781
765
|
nested_refusal(script)
|
|
782
766
|
elsif PairCommand.interpreter?(program)
|
|
783
|
-
@mise_config = true if script.match?(
|
|
767
|
+
@mise_config = true if script.match?(MisePaths::CONFIG_TEXT)
|
|
784
768
|
script_text_refusal(script)
|
|
785
769
|
end
|
|
786
770
|
return reason if reason
|
|
@@ -1184,7 +1168,7 @@ module Samagotchi
|
|
|
1184
1168
|
resolved.each do |word, path|
|
|
1185
1169
|
return PairBoundary::GIT_HOOKS if !@read_only && PairCommand.git_hook?(path || word)
|
|
1186
1170
|
|
|
1187
|
-
@mise_config = true if !@read_only &&
|
|
1171
|
+
@mise_config = true if !@read_only && MisePaths.config?(path || word)
|
|
1188
1172
|
|
|
1189
1173
|
if path
|
|
1190
1174
|
return PairBoundary::SECRET if @paths.secret_refusal([path])
|
|
@@ -15,7 +15,8 @@ module Samagotchi
|
|
|
15
15
|
# command runs later, outside the gate; chi context writes
|
|
16
16
|
# it); nothing legitimate writes there through the file tools.
|
|
17
17
|
# ask (once / session): config.yml, home.json (the chi home: the
|
|
18
|
-
# Ruby every chi launch boots)
|
|
18
|
+
# Ruby every chi launch boots), the chi shim (ChiShim: the
|
|
19
|
+
# `chi` every tool runs) and the plain hooks dir. The
|
|
19
20
|
# system bundle's config protocol edits config.yml with
|
|
20
21
|
# write/edit, so this keeps the user in the loop without
|
|
21
22
|
# breaking it.
|
|
@@ -31,7 +32,8 @@ module Samagotchi
|
|
|
31
32
|
# @param hooks_dir [String] the plain hooks dir
|
|
32
33
|
# @param context_dir [String, nil] <state dir>/context (ContextSources.root)
|
|
33
34
|
# @param home_path [String, nil] home.json (ChiHome.path)
|
|
34
|
-
|
|
35
|
+
# @param shim_path [String, nil] the chi shim (ChiShim.path)
|
|
36
|
+
def initialize(store_dir:, bundles_dir:, config_path:, hooks_dir:, context_dir: nil, home_path: nil, shim_path: nil)
|
|
35
37
|
@deny = [
|
|
36
38
|
["guardrail-store", store_dir, "the approval store belongs to the user"],
|
|
37
39
|
["installed-bundles", bundles_dir, "installed bundles (hooks, rules, manifests) are changed by chi bundle install"],
|
|
@@ -40,6 +42,7 @@ module Samagotchi
|
|
|
40
42
|
@ask = [
|
|
41
43
|
["chi-config", config_path, "changes chi's config.yml"],
|
|
42
44
|
["chi-home", home_path, "changes the Ruby every chi launch boots"],
|
|
45
|
+
["chi-shim", shim_path, "changes the chi every tool's chi runs"],
|
|
43
46
|
["chi-hooks", hooks_dir, "changes chi's hooks"]
|
|
44
47
|
].select { |_, root, _| root }
|
|
45
48
|
end
|