samagotchi 0.119.0 → 0.121.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -1
  3. data/bin/chi +6 -1
  4. data/docs/architecture.md +35 -5
  5. data/docs/cli.md +33 -8
  6. data/docs/configuration.md +74 -20
  7. data/docs/internals/participants.md +111 -12
  8. data/docs/internals/prompt-caching.md +7 -6
  9. data/docs/internals/remote-sessions.md +6 -1
  10. data/docs/internals/web-push.md +4 -2
  11. data/docs/plugins.md +24 -8
  12. data/docs/sessions.md +4 -3
  13. data/docs/testing.md +13 -2
  14. data/lib/samagotchi/answer_command.rb +2 -0
  15. data/lib/samagotchi/bridge.rb +56 -13
  16. data/lib/samagotchi/bridge_client/event_stream.rb +34 -3
  17. data/lib/samagotchi/bridge_client.rb +58 -19
  18. data/lib/samagotchi/bridge_secret.rb +35 -0
  19. data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
  20. data/lib/samagotchi/bundles/claude-peer/scripts/pair +1 -1
  21. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  22. data/lib/samagotchi/bundles/mcp/manifest.yml +3 -3
  23. data/lib/samagotchi/bundles/mcp/plugin.rb +5 -1
  24. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  25. data/lib/samagotchi/config.rb +3 -0
  26. data/lib/samagotchi/engine.rb +1 -0
  27. data/lib/samagotchi/kernel_loop.rb +5 -0
  28. data/lib/samagotchi/participant_messages.rb +5 -0
  29. data/lib/samagotchi/participants.rb +16 -6
  30. data/lib/samagotchi/plugin/api.rb +6 -3
  31. data/lib/samagotchi/review_command.rb +3 -1
  32. data/lib/samagotchi/run_options.rb +3 -1
  33. data/lib/samagotchi/session_access/contract.rb +4 -1
  34. data/lib/samagotchi/session_access/local.rb +15 -4
  35. data/lib/samagotchi/session_access/remote.rb +43 -7
  36. data/lib/samagotchi/session_access.rb +11 -1
  37. data/lib/samagotchi/session_manager.rb +1 -1
  38. data/lib/samagotchi/sessions_command.rb +1 -2
  39. data/lib/samagotchi/share_command.rb +29 -8
  40. data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -0
  41. data/lib/samagotchi/terminal_ui/local_link.rb +18 -3
  42. data/lib/samagotchi/terminal_ui/question_slot.rb +19 -8
  43. data/lib/samagotchi/tool_declarations.rb +1 -1
  44. data/lib/samagotchi/tool_runner.rb +37 -1
  45. data/lib/samagotchi/toolkit.rb +50 -26
  46. data/lib/samagotchi/toolkit_binding.rb +32 -0
  47. data/lib/samagotchi/toolkit_command.rb +23 -0
  48. data/lib/samagotchi/tools/deferred_tools.rb +10 -2
  49. data/lib/samagotchi/tools/delegate_relay.rb +4 -0
  50. data/lib/samagotchi/tools/registry.rb +20 -6
  51. data/lib/samagotchi/tools/tool_index.rb +103 -20
  52. data/lib/samagotchi/version.rb +1 -1
  53. data/lib/samagotchi/web/app.rb +117 -43
  54. data/lib/samagotchi/web/grants.rb +127 -16
  55. data/lib/samagotchi/web/link_codes.rb +24 -15
  56. data/lib/samagotchi/web/participant_access.rb +69 -13
  57. data/lib/samagotchi/web/participant_exposure.rb +44 -0
  58. data/lib/samagotchi/web/participant_review.rb +71 -9
  59. data/lib/samagotchi/web/participants_admin.rb +12 -6
  60. data/lib/samagotchi/web/proxy_gate.rb +18 -3
  61. data/lib/samagotchi/web/public/annotate_bar.css +18 -0
  62. data/lib/samagotchi/web/public/annotate_bar.js +10 -3
  63. data/lib/samagotchi/web/public/app.js +66 -155
  64. data/lib/samagotchi/web/public/composer.css +73 -0
  65. data/lib/samagotchi/web/public/composer_grip.js +21 -15
  66. data/lib/samagotchi/web/public/composer_mount.js +196 -0
  67. data/lib/samagotchi/web/public/forms.css +12 -0
  68. data/lib/samagotchi/web/public/index.html +14 -88
  69. data/lib/samagotchi/web/public/level_chip.js +2 -2
  70. data/lib/samagotchi/web/public/llm_context_chip.js +4 -4
  71. data/lib/samagotchi/web/public/notifications.js +58 -8
  72. data/lib/samagotchi/web/public/participant.html +46 -10
  73. data/lib/samagotchi/web/public/participant.js +101 -18
  74. data/lib/samagotchi/web/public/participant_annotate.js +75 -0
  75. data/lib/samagotchi/web/public/participant_model.js +53 -1
  76. data/lib/samagotchi/web/public/participants_panel.js +37 -13
  77. data/lib/samagotchi/web/public/question_card.js +15 -1
  78. data/lib/samagotchi/web/public/question_cards.js +11 -2
  79. data/lib/samagotchi/web/public/review_history.js +60 -0
  80. data/lib/samagotchi/web/public/scope.js +12 -8
  81. data/lib/samagotchi/web/public/stage_frame.js +9 -4
  82. data/lib/samagotchi/web/public/stage_view.js +4 -4
  83. data/lib/samagotchi/web/public/toolkit_chip.js +3 -0
  84. data/lib/samagotchi/web/push/notifier.rb +17 -3
  85. data/lib/samagotchi/web/running.rb +7 -4
  86. data/lib/samagotchi/web/server.rb +34 -11
  87. data/lib/samagotchi/web/session_create.rb +7 -7
  88. data/lib/samagotchi/web/start_dir.rb +26 -0
  89. data/lib/samagotchi/web/tunnel.rb +10 -1
  90. data/lib/samagotchi/worker_sidecar.rb +22 -11
  91. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67772cbed2c590109f257a85de25cc009250b6fb3a78c1bf4e1b2194561846b7
4
- data.tar.gz: ac023d3b8f28c1196b234e528eae9faf9947181ee6f44111c7f00723eb92b196
3
+ metadata.gz: 78051f4f253f400d0b791fe2694f0dc3d07a19e7d1268ef30557ff3a780d8d64
4
+ data.tar.gz: c0bf5fbba8c530df5bf56a3947addddc0a9952a39f2077ddd5f59129683ba8f6
5
5
  SHA512:
6
- metadata.gz: e818ad092946ec8ca8d1765079824a0cd7f540734fc9ad52b1a28d2ad70f12be7ff21324f348eda1461474d2924e99f97fa5f45fe25e11e16cc65c7e2a7f63af
7
- data.tar.gz: 2c852f4b48dcee1bd4c742eb3acdbe34ee802a53407b562b09398b13be34a9de3bcbdeebb058781691c07a7c696797ea660133e84155ccb2247ab6e9e5f72596
6
+ metadata.gz: c438bac307263d1a56cd9a97b56a2c3ab7175d9be0944c4e327fb92a57c9fdd9ffb82bdac8ec7048ac780a4a9e5f3545a3186dfcb3fec7842e687e2d47099307
7
+ data.tar.gz: 2738c2cc9f4f81f34c3c88f02568066dced57578a88ac6d1e301ca62508a3e343179a97a7d6ef9d6778f463a00648d2aa0f0b1aded820e8bce7442b2601a0d8a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,103 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.121.0] - 2026-10-26
11
+
12
+ ### Added
13
+
14
+ - Toolkit kits pick the MCP servers their sessions reach: `groups: [chrome]` in `toolkit.kits` (names or globs; no
15
+ key = the base's; the built-ins reach every server, `review` none). `/toolkit` switches them from the next turn
16
+ (asking first: the cached prompt changes). A call into a server the kit doesn't reach is refused without starting
17
+ it, naming a kit that reaches it; its deferred stub says why, `/mcp` marks such servers, and the web's toolkit chip
18
+ lists a restricted kit's servers. Kits without `groups:` send the same request as before.
19
+
20
+ ### Changed
21
+
22
+ - `ask_user_question`'s description asks the model, when it recommends one option, to put it first and end its
23
+ label with " (Recommended)" (eval 0005: Flash Next and DeepSeek v4.1-flash mark it 5/5 on a decision question,
24
+ 0/5 before; an answer comes back with the suffix, so `chi answer --option` takes that exact label or the number).
25
+ - The recommended option shows as such: chi web's question card marks its row in the accent colour with a small
26
+ "recommended" tag (the label shown, and the answered line, without the suffix; nothing is pre-checked), and the
27
+ terminal's question paints its row bold. Approval and step-limit cards are unchanged.
28
+ - chi web's select boxes have one shape in every dialog: the Invite panel's person/agent and Expires now match the
29
+ text fields beside them (flat box, same height and corners, one thin caret), as the Thinking and LLM context
30
+ dialogs' selects already did, in place of the browser's own pill.
31
+
32
+ ### Fixed
33
+
34
+ - claude-peer (0.4.4): `pair --inbox-socket PATH` says the remote socket is forwarded to this Claude's socket, not
35
+ from it (`ssh -R`).
36
+ - An ended participant link forgets its comment history even when its first review read is refused.
37
+ - The participant review status stays idle when a live snapshot has no current turn and its status is stale.
38
+ - The participant usage row no longer says "0 queued" when no worker is running.
39
+ - The participant usage row counts link time down and shows its end while the page stays open.
40
+
41
+ Update with `chi update` (the mcp bundle 0.7.2, claude-peer 0.4.4, the dev profile 0.7.3).
42
+
43
+ ## [0.120.0] - 2026-10-26
44
+
45
+ ### Added
46
+
47
+ - `chi web --start-dir DIR` and `web.start_dir`: the folder chi web starts in, in place of the one you run it in
48
+ (`~` and a relative path work; one that isn't a folder stops it, exit 1). Its page opens on that folder's project,
49
+ and a new session from the all view starts there. A chi web already running in another folder keeps it and the
50
+ second one says so: restart it to change it.
51
+ - Annotate on the participant page: selecting text in the Plan (the review's files) or the thread offers your
52
+ `web.annotate_presets` and quotes the selection into the comment box (`From <path>:` / `From <name>'s comment:`),
53
+ as on your own page.
54
+ - The participant page's comment box is the owner's composer: it grows with the text, its grip resizes or folds it
55
+ (Annotate and typing open it again), it stays one row on a phone, and ↑/↓ recall the participant's own earlier
56
+ comments from that browser (kept per review link, never your prompt history). No `/` list, delivery choices or
57
+ images there yet; every comment is still a queued review turn. Your participant view has no comment box.
58
+ - The participant page shows the participant their own usage in one line under the comment box: what their
59
+ comments have cost you of their cap (a running turn's too), their queued comments of their queue limit, when their
60
+ link ends, and the session's model. Never another participant's spend, yours or the session's. It sits on a
61
+ per-link exposure level (`own`, the only one so far), so a later tier can show more.
62
+ - Review links bound to one account: `chi share ID --name NAME --identity EMAIL` (and the Invite dialog's email field,
63
+ behind an auth proxy that names the signed-in user: `web.public_url` with `web.proxy.identity_header`, or
64
+ `chi web --tunnel tailscale`) makes a link that opens only through that proxy, signed in as EMAIL. A forwarded
65
+ link opens for nobody else, and on the LAN or this machine for nobody. `--identity` without such a proxy is refused
66
+ (the link could never open), and `chi share` warns when a link could be bound and isn't. `chi share ls` shows
67
+ `alex@corp · bound`. Through `chi --remote`, `--identity` needs that chi web to be this version or newer (an older
68
+ one would ignore it and make a link anyone could open).
69
+ - chi web trusts proxy headers (`X-Forwarded-*`, the identity header, the public Host) only from this machine
70
+ (a loopback peer): the proxy must run where chi web runs. With `web.host: lan` and `web.public_url` together, a LAN
71
+ device sending those headers is a LAN request, never one through the proxy. An identity header that isn't ASCII is
72
+ refused.
73
+ - A bound link opened by another account, or not through the proxy, is refused with its own words (401
74
+ `token_bound`: "this review link is for another account …", on the page too), and the refusal goes into the
75
+ session's participants audit naming who tried. Its one-time code stays usable: the person it was sent to can still
76
+ open it. Behind a proxy the audit's new-peer lines are per signed-in identity (every peer there is 127.0.0.1).
77
+
78
+ ### Changed
79
+
80
+ - chi web's bare page (`/`: the LAN link and its QR code, the Home Screen app) opens on the project chi web starts in,
81
+ as its printed link already did; outside a repo it shows every session as before. Every project's sessions are now
82
+ `/?scope=all` (the header's `all`), and `chi web --scope=all` (or a `chi web` run outside a repo) prints that link.
83
+ - A pushed notification opens its session in that session's own project (the all view for a session outside a repo),
84
+ not the project the app shows. The Home Screen app's notifications and badge still cover every project: on a
85
+ project's page it tidies them from every session's events, where before a project's page (`?dir=`) left them as
86
+ they were.
87
+ - The participants' grants store (`web-grants/grants.json`) carries a version: 2 once a grant is bound to an identity.
88
+ chi keeps a grant's fields it doesn't know when it rewrites the store, and never writes over a store a newer chi
89
+ wrote (its links still work; invite, cap and revoke say to update chi). A chi from before this one doesn't check.
90
+
91
+ ### Fixed
92
+
93
+ - chi web's stage: on a phone its height follows the whole bottom dock (an info bar that wraps late after a reload
94
+ no longer leaves it too tall), and the keyboard opening during a smooth scroll no longer leaves a question card
95
+ out of sight.
96
+
97
+ ### Security
98
+
99
+ - A session's worker answers only chi's own clients on its machine: its Bridge makes a random secret, written to the
100
+ session's `bridge.json` (readable by you only), and refuses every request that doesn't name it (403
101
+ `bridge_secret`). A proxy or tunnel that forwards every local port, or another account on the machine, reaches the
102
+ port but no longer a session. **After updating chi, restart chi web, re-attach terminals** (`chi --attach ID`) **and
103
+ restart older session workers** (`chi sessions restart ID...`): a chi web, attached terminal or session worker from
104
+ before the update sends no secret, and a worker the new chi starts (a delegate child too) refuses it with a message
105
+ saying so. Workers started before the update ask for no secret and answer both until they restart.
106
+
10
107
  ## [0.119.0] - 2026-10-26
11
108
 
12
109
  ### Added
@@ -4454,7 +4551,9 @@ and long-lived sessions.
4454
4551
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4455
4552
  hotkey panel that send selected text or the clipboard to your sessions.
4456
4553
 
4457
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.119.0...HEAD
4554
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.121.0...HEAD
4555
+ [0.121.0]: https://github.com/dm1try/samagotchi/compare/v0.120.0...v0.121.0
4556
+ [0.120.0]: https://github.com/dm1try/samagotchi/compare/v0.119.0...v0.120.0
4458
4557
  [0.119.0]: https://github.com/dm1try/samagotchi/compare/v0.118.0...v0.119.0
4459
4558
  [0.118.0]: https://github.com/dm1try/samagotchi/compare/v0.117.0...v0.118.0
4460
4559
  [0.117.0]: https://github.com/dm1try/samagotchi/compare/v0.116.0...v0.117.0
data/bin/chi CHANGED
@@ -134,6 +134,10 @@ if options[:tunnel] && !web
134
134
  warn "Error: --tunnel goes with chi web (chi web --tunnel tailscale)"
135
135
  exit 2
136
136
  end
137
+ if options[:start_dir] && !web
138
+ warn "Error: --start-dir goes with chi web (chi web --start-dir DIR)"
139
+ exit 2
140
+ end
137
141
  if options[:sets] && web
138
142
  warn "Error: --set is a new session's (chi --set, chi send --new --set), not chi web's"
139
143
  exit 2
@@ -225,7 +229,8 @@ if options[:web]
225
229
  markdown: markdown, view: view, new_token: options[:new_token], tunnel: options[:tunnel],
226
230
  working_set: Samagotchi::Config.get("web.working_set"),
227
231
  annotate_presets: Samagotchi::Config.get("web.annotate_presets"),
228
- editor: Samagotchi::Config.get("web.editor"))
232
+ editor: Samagotchi::Config.get("web.editor"),
233
+ start_dir: options[:start_dir] || Samagotchi::Config.get("web.start_dir"))
229
234
  end
230
235
 
231
236
  # --resume and --attach take a unique id prefix too, like git.
data/docs/architecture.md CHANGED
@@ -152,7 +152,7 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
152
152
  | Core | `Samagotchi::Engine` | System prompt, memory injection, tool declarations, session lifecycle, the model↔tool loop (`run_turn`). No terminal coupling. |
153
153
  | UI | `Samagotchi::TerminalUI` | Interactive REPL (Reline), rendering (ANSI, spinner, status line), REPL commands. Delegates all core work to an `Engine`. |
154
154
  | Model loops and adapters | `KernelLoop`, `LLM::ChatLoop`, `Samagotchi::Client`, `LLM::OpenAIChat`, `LLM::AnthropicMessages`, `LLM::HTTP` | The model↔tool loops and the HTTP adapters they talk through (see "Model loops and adapters"). |
155
- | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, no auth, localhost-only). |
155
+ | Bridge (SSE/HTTP) | `Samagotchi::Bridge`, `SessionManager` | The **single live client transport**: an SSE read stream + HTTP POST turn/cancel/answer surface that attaches to a worker's existing `Engine` via `Engine#subscribe`. Every session worker starts it (bound `127.0.0.1`, localhost-only), and it answers only a request that names its secret (`X-Chi-Bridge`, from the worker's 0600 `bridge.json`). |
156
156
  | Web (Rack) | `Samagotchi::Web::App`, `SessionManager` | Single-port `127.0.0.1:4567` control plane via `rack`+`webrick` (serve `index.html` + `/api/*`). With `web.host: lan` (or one of this machine's IPv4 addresses) the server also listens on that address, where every request but this machine's needs the access token (`App#token_gate`: the `chi_token` cookie or `Authorization: Bearer`; [CLI](cli.md#chi-web-on-your-phone)). Experimental: behind an auth proxy (`web.public_url`, `Web::ProxyGate`) every request needs the token, this machine's too, and one that came through the proxy also the configured identity header for the owner ([configuration.md](configuration.md#behind-an-auth-proxy)). A request goes through, in order: the Host check (`local_host_header?`: loopback names, the LAN address, the public host), the cross-site check (`cross_site?`: Origin this server's or the public origin, no `Sec-Fetch-Site` cross-site/same-site on a POST or `/api`), the frame check (`framed?`: a frame/embed/object load from another origin, 403 `framed`; every HTML answer carries `X-Frame-Options: DENY` and `frame-ancestors 'none'`), the token gate (`token_gate` → `principal_for`, `proxied_principal` for a proxied request; the `?token=` / `POST /api/token` trade sets the cookie, `__Host-chi_token` through the proxy; with `web.public_url` a direct request takes Bearer only, and chi's token-less `/api/info` probe gets `probe_info`), then the route's permission (`Principal#permits?`). It talks to each worker's Bridge (`App#relay`, `bridge_get`; `/stream` proxies it) and reads the session files; no `Engine` of its own. Markdown (`web.markdown`) is rendered here only (`Web::MarkdownRenderer`): the answers it serves, card bodies, and `POST /api/render` for a text the page already shows. `App` keeps the gate, `ROUTES` and the helpers the handlers share; its handlers live in mixins it includes, each a `Web::` module with private methods: `SessionAccessRoutes` (`web/session_access_routes.rb`), `ParticipantAccess` (`web/participant_access.rb`), `ParticipantsAdmin` (`web/participants_admin.rb`), `ParticipantReview` (`web/participant_review.rb`), `MessageDisplay` (`web/message_display.rb`), `SessionShow` (`web/session_show.rb`), `SessionCreate` (`web/session_create.rb`), `SessionControl` (`web/session_control.rb`), `PushRoutes` (`web/push_routes.rb`) and `ProxyGate` (`web/proxy_gate.rb`). Exact paths in `GATE_FREE_PATHS` are served before the token gate (the fragment trade's script, and the Home Screen app's manifest, service worker and icons). `bin/chi web` entrypoint. |
157
157
  | Sessions | `Samagotchi::Session`, `SessionManager` | File-based `~/.local/state/samagotchi/sessions/<uuid>.json` + sidecar `input/`/`output/`; retention (14d/500) + ordering (`updated_at desc`). |
158
158
 
@@ -390,8 +390,27 @@ process that already owns the `Engine`); every worker starts it, and it exposes:
390
390
  worker only through `chi web`. A request carrying any trace a proxy adds (`ProxyTrace`: `X-Forwarded-*`,
391
391
  `Forwarded`, `X-Real-IP`, `X-Auth-Request-*`, `Cf-*`…, underscore twins too) is `403 proxied`
392
392
  (`Bridge#proxied_request?`): an auth proxy that forwards every local port and adds such a header doesn't reach a
393
- worker. A heuristic: a forward that adds none still does, until Bridges have a secret (shared-sessions P6).
394
- `bridge.json` is 0600.
393
+ worker.
394
+ - **The secret.** Every request, on every route (the stream, the reads such as `state` and `stats`, the writes, and
395
+ an unknown path), names the Bridge's secret in `X-Chi-Bridge`; one without it, or with another one, is
396
+ `403 bridge_secret` with one message (`BridgeSecret::REFUSED`: restart chi web, re-attach terminals and restart
397
+ older session workers), after the browser and proxy checks and before any route (`Bridge#secret_ok?`,
398
+ `OpenSSL.secure_compare`). No route is left open: what the reads show is the session itself. Only a request that
399
+ got past it counts as client activity, so a refused one (an older chi web's polls) never holds the idle exit. Each Bridge makes its own (`SecureRandom.hex(32)`, 64 hex
400
+ characters) when it starts and writes it to `bridge.json`, which is 0600 from the moment it exists
401
+ (`AtomicFile` creates the temporary file with that mode, then renames it), so only this machine's user can read
402
+ it: a forward that adds no proxy header, or another local account, reaches the port but not a session. Clients get
403
+ it only from the sidecar: `BridgeClient.discover`/`.wait_for`/`.for_sidecar` take the port and the secret from the
404
+ same read (chi web makes its client from the sidecar for every request, the attached terminal again on a
405
+ `worker_changed`), so a respawned worker's new one is used and nothing passes a port by hand. A worker from before
406
+ the secret writes none and asks for none; a chi web or attached terminal from before it sends none, so a worker
407
+ the newer chi started refuses it until it is restarted. A client tells that refusal from a route's own 403
408
+ (`BridgeClient::Response#secret_refused?`) and shows the one message: chi web's relays answer 502 `bridge_secret`,
409
+ `chi answer` and a parent's approval relay say it instead of "refused", and an event stream ends with reason
410
+ `bridge_secret` once rediscovery finds no newer worker (the attached terminal's closing line), never retrying in
411
+ silence; chi web's stream proxy passes no refusal on as frames. The secret is never logged (a refusal logs
412
+ `secret_refused` with the path, at most once a minute), never in an event, a reply, `/api/info` or a session
413
+ file, and `WorkerSidecar`, `BridgeClient` and `Bridge` leave it out of `#inspect`.
395
414
 
396
415
  The metrics in `/state`, `/snapshot`, `/stats` and the SSE `snapshot`/`reset` frames
397
416
  (`SessionMetrics#snapshot`) carry the session's totals and only the recent timing records: the
@@ -472,7 +491,7 @@ Every route goes through `Bridge#dispatch`: an id other than the bridge's own se
472
491
  `handler_failed`) rather than dropping the connection.
473
492
 
474
493
  The per-session port is OS-assigned (bound to `0`) and published to a `bridge.json` sidecar
475
- for client discovery. `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
494
+ for client discovery, with the Bridge's secret (see above). `chi web`'s `GET /api/sessions/:id/stream` proxies this bridge
476
495
  (503 `not_live` when the worker is not running; full history of any session is served by
477
496
  `GET /api/sessions/:id/output`), as a byte pipe for the owner: `App#proxy_stream_body` gives `ProxyStreamBody` the
478
497
  request's frame filter (`App#stream_filter`: nil for the owner; for a participant, and the owner's participant view
@@ -493,7 +512,18 @@ in RubyGems' specification dirs, or a checkout's `version.rb` on disk; looked at
493
512
  probe, every 10 s), and a change is a `chi` frame `{version, installed}`. A newer one than the
494
513
  server runs is said in chi web's terminal and on the page: restart chi web (its workers already
495
514
  run the newest chi, [Sessions](sessions.md)). `GET /api/info` carries `installed` too, and the
496
- feature `newest_workers`.
515
+ feature `newest_workers`; next to `cwd` (the folder chi web runs in) it names `start_dir`, the folder it
516
+ starts in (`web.start_dir`, `--start-dir`; `Web::StartDir`), which a second `chi web` given another one
517
+ mentions.
518
+
519
+ The page's scope is in its URL, and the server picks the default: `?dir=<folder>` is that folder's
520
+ project (the list, the events stream and a new chat follow it), `?scope=all` every session, and a bare
521
+ `/` the start folder's project when that folder is in a repo (`App#index_data_attributes` writes
522
+ `data-scope-dir`, which the page reads when the URL has no `?dir`), else every session. The page never
523
+ redirects, so the `?token=` trade's 303, a link code's `#code=` and the Home Screen app's `start_url` `/`
524
+ all land on the start project; a push opens its session's own project (`/?dir=<root>#/s/<id>`, or
525
+ `/?scope=all#/s/<id>` outside a repo). `?dir=` beats `?scope=all`. A session created
526
+ without a folder starts in the start folder.
497
527
  Files stay the source of truth and workers don't know the hub. Its watcher is a 1 s tick that
498
528
  stats the sessions dir (every session writer goes tmp + rename, which bumps the dir's mtime) and
499
529
  each `<id>/` folder (recap.json, bridge.json, the stopped and archived markers), re-parsing only the files whose mtime or size
data/docs/cli.md CHANGED
@@ -11,8 +11,9 @@
11
11
  - `chi scratch [options]` — a one-time session in the plain in-process REPL, in this folder, that leaves nothing behind (see [Scratch sessions](#scratch-sessions))
12
12
  - `chi --attach <session-id>` — attach the terminal to a session's worker (e.g. one started from the Web UI), waking one if it has exited
13
13
  - A session id can be shortened to any unique prefix (like git): `chi --attach 2ea8`. `--resume`, `--attach`, `sessions stop`, `sessions archive` and `sessions delete` take one; an ambiguous prefix lists the sessions it matches.
14
- - `chi web [--port 4567] [--open] [--scope=all]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
14
+ - `chi web [--port 4567] [--open] [--scope=all] [--start-dir DIR]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: `/?scope=all`, every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
15
15
  - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
16
+ - `chi web --start-dir DIR` (config `web.start_dir`) — the folder chi web starts in, in place of the one you run it in (`~` and a relative path work; chi web never changes its own folder): its page opens on that folder's project (the printed link, a bare `/`, the LAN link and its QR code, the Home Screen app), and a new session from the all view starts there; `/?scope=all` (the header's `all`) is every project. One that isn't a folder exits 1; `--start-dir` without `web` exits 2. A chi web already running in another folder keeps it (the second one says so: restart it to change it)
16
17
  - `chi web --tunnel tailscale` — the Web UI over https on your tailnet, for your phone anywhere: Tailscale Serve (never Funnel) on this machine's `<machine>.<tailnet>.ts.net` name for this run, the owner your Tailscale login, turned off on exit (see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet)); `--tunnel` without `web` exits 2
17
18
  - `chi web push ls [--format json]`, `chi web push rm ID`, `chi web push test [ID]` — the phones chi web pushes to (see [Phone notifications](configuration.md#phone-notifications-web-push)): list them (short id, label, origin, added, last delivery, done turns, last error; `stale` for one made on another public URL than chi web's now: never sent to), remove one, or send a test notification to ID or to every device not stale. chi web need not run; nothing printed holds a device's push address or keys. A usage error exits 2; a store others can read, a corrupt one, an unknown device or a failed test push exit 1. This machine only: `chi --remote X web push` exits 2
18
19
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
@@ -26,7 +27,7 @@
26
27
  - `chi answer ID --question QID (--option N|LABEL)... [--text T] [--timeout S] [--format json]` (or `--dismiss`) — answer the question a session waits on (the one `chi send --wait` exited 3 with), then wait and print what comes next as `chi send --wait` does: the reply (0), the next question (3), still running after `--timeout` (4). `--option` is 1-based or the label, repeated on a multi-select question; `--text` is free text, or a Deny's reason; `--dismiss` leaves it unanswered and the model finishes its reply. A question no longer open (the web answered first) isn't answered again: it waits for that turn's reply. An option the question doesn't offer exits 2; a worker that is gone exits 1 (`send the task again: chi send --wait -m "…" ID`). What a parent may pick follows `guardrails.parent_answers`, or the session's own `parent_answers` when stricter (`soft`, the default: a deny, a plain answer, Continue and "Allow once"; `standard`: no allow; `hard`: no Continue either), never a wider scope or an allow on chi's own config; a refused pick exits 1 with what to answer instead (a convention, not a security boundary; see [Guardrails](guardrails.md#answers-from-a-parent-agent))
27
28
  - `chi remote add NAME URL [--token-file F | --token-stdin]`, `chi remote ls`, `chi remote rm NAME` — the other machines whose chi web the session commands reach with `--remote NAME` (see [Remote sessions](#remote-sessions))
28
29
  - `chi review FILE… [--dir DIR] [--model M]`, `chi review --from ID [FILE…]` — start a review session to invite participants to: for files of a project, or as a review of a working session that starts from its recap, not its transcript (see [Review sessions](#review-sessions))
29
- - `chi share ID --name NAME [--agent] [--expires 3d] [--cap 2]`, `chi share ls [ID]`, `chi share cap ID P DOLLARS`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
30
+ - `chi share ID --name NAME [--identity EMAIL] [--agent] [--expires 3d] [--cap 2]`, `chi share ls [ID]`, `chi share cap ID P DOLLARS`, `chi share revoke ID [P] | --all` — invite a participant to review a session through chi web on the LAN (a link of their own, 7 days, a $2 spending cap), list them, change one's cap, revoke them (see [Participants](#participants))
30
31
  - `chi --remote NAME send|answer|note|context|sessions|share|review …` (or `CHI_REMOTE=NAME`) — those commands on that machine's sessions, through its chi web (see [Remote sessions](#remote-sessions))
31
32
  - `chi --remote NAME --attach ID` (`--resume ID`, `--dir DIR [-p TEXT]`) — the attached TUI on a session of that machine (see [Remote sessions](#remote-sessions))
32
33
  - `chi memory ls [--scope project|system] [--format text|json]` — the memories of both scopes (this folder's project first), one row each: scope, name, size, date, `from <bundle>`, the index description and its model overlays; `chi memory rm NAME [--scope project|system]` removes one as `memory_write` with `remove: true` does: its file and model overlays go to the bundle trash (the line names the dir; `chi bundle trash` lists and empties it) and its index line is dropped. NAME in both scopes without `--scope` exits 2 naming both; a missing NAME or a bundle's memory (`chi bundle uninstall` removes it) exits 1 (see [Memory: Listing and removing from the shell](memory.md#listing-and-removing-from-the-shell))
@@ -238,7 +239,12 @@ done
238
239
  hands over to the new chi, which does the rest and prints the table. Old
239
240
  versions stay installed: running workers and an old `chi web` still load
240
241
  files from them, so a `gem cleanup` while they run breaks them (a
241
- `LoadError`, missing web assets); restart them first. Offline, the row says
242
+ `LoadError`, missing web assets); restart them first. A chi web, an
243
+ attached terminal or a session worker started before the update may not
244
+ reach a worker the new chi starts (403 `bridge_secret`: its Bridge asks for
245
+ a secret they don't send; an older worker's delegate child is one): restart
246
+ chi web, re-attach, and restart the older workers (`chi sessions restart
247
+ ID...`, as the `workers` row says). Offline, the row says
242
248
  "couldn't check" and the rest still runs; a failed install fails the row
243
249
  and the rest runs on the current version. Under Bundler (`bundle exec`)
244
250
  the row says `bundle update samagotchi` instead.
@@ -1164,6 +1170,12 @@ On a touch screen (a phone, a tablet) the bar sits just above the composer
1164
1170
  instead of next to the selection, where the system's own Copy menu and the
1165
1171
  selection handles would cover it; several pills wrap to a second row.
1166
1172
 
1173
+ A review's participants get the same on their page (a `chi share` / `chi
1174
+ review` link): selecting text in the Plan or the thread quotes it into
1175
+ their comment box, labelled `From docs/plan.md:` or `From Sam's comment:`,
1176
+ with the same presets. Not in the owner's participant view, and no plugin
1177
+ pills there (a review turn refuses `/` lines).
1178
+
1167
1179
  The list is `web.annotate_presets`, `|`-separated (at most five; a preset
1168
1180
  can't contain `|`; a YAML list works too):
1169
1181
 
@@ -1200,7 +1212,10 @@ Plain http: the link and your traffic can be read by anyone on this Wi-Fi.
1200
1212
 
1201
1213
  Scan the QR code with the phone's camera. The page trades the token in the
1202
1214
  link for a cookie (kept 400 days) and drops it from the address, so a
1203
- bookmark or a home-screen icon keeps working across restarts. A home-screen
1215
+ bookmark or a home-screen icon keeps working across restarts. It opens on
1216
+ the project chi web starts in (`--start-dir DIR` or `web.start_dir` picks
1217
+ another; outside a repo it shows every session); the header's `all` shows
1218
+ every project's. A home-screen
1204
1219
  web app on iOS has cookies of its own: if it opens on "needs chi web's
1205
1220
  access token", paste the token there (the part of the link after
1206
1221
  `token=`), or open the link once in it.
@@ -1350,8 +1365,9 @@ chi --remote work sessions list
1350
1365
  ```
1351
1366
 
1352
1367
  - Pick a local port other than 14567 when this machine runs its own chi web.
1353
- - Forward only chi web's port, never a worker's: a session's Bridge is
1354
- unauthenticated.
1368
+ - Forward only chi web's port, never a worker's: a session's Bridge answers
1369
+ only chi's own clients on its machine (they read its secret from the
1370
+ session's folder), so a forwarded worker port is of no use.
1355
1371
  - Without `web.public_url` there, a request that arrives through the tunnel
1356
1372
  looks local and needs no token, so any process here reaching the forwarded
1357
1373
  port acts as you there. With it set, every request needs the token, which
@@ -1403,8 +1419,9 @@ chi share revoke --all # every link to every session
1403
1419
  - **The participant page.** Their link opens a page of its own (never yours): the session's title and status, who
1404
1420
  else takes part, and the conversation as a participant sees it: every prompt and reply with who wrote it, each
1405
1421
  step's tools as one line (`read docs/plan.md`: the tool and a project file's path, never arguments or output), and
1406
- the reply streaming in while a turn runs. A "Comment or ask" box sends their comment; their queued comments show
1407
- with **Take back**; "waiting for the owner" shows while you answer a question or an approval of the turn; a refused
1422
+ the reply streaming in while a turn runs. A "Comment or ask" box sends their comment (your composer's box, with its
1423
+ grip to resize or fold it and ↑/↓ over their own earlier comments from that browser; no `/` list, no images yet);
1424
+ their queued comments show with **Take back**; "waiting for the owner" shows while you answer a question or an approval of the turn; a refused
1408
1425
  comment (a `!` or `/` line, too many waiting, their cap reached) says why. On a phone it is the thread first with
1409
1426
  the box pinned, tool lines folded. They never see tool output or arguments, thinking, approvals or questions,
1410
1427
  memories, context notes, plugin and delegate input, cards, command output, the working set, cost, timing or host
@@ -1433,6 +1450,14 @@ chi share revoke --all # every link to every session
1433
1450
  `https://<public url>/#code=…`; with `chi web --tunnel tailscale` too (its `ts.net` name, which web.json records
1434
1451
  while that chi web answers on its port: a chi web on another port takes web.json over, and the link falls back to
1435
1452
  127.0.0.1). A tunnel link opens only on a device on your tailnet; the grant still decides.
1453
+ - **A link for one account.** Behind an auth proxy that names the signed-in user (`web.public_url` with
1454
+ `web.proxy.identity_header`, or `chi web --tunnel tailscale`), `--identity EMAIL` binds the link to that account:
1455
+ it opens only through the proxy, signed in as EMAIL. A forwarded link opened by anyone else says "This review link
1456
+ is for another account …", and its one-time code still works for EMAIL. `chi share` says `· bound to EMAIL`, `chi
1457
+ share ls` `alex@corp · bound`, and the panel's Invite has an email field there. Without such a proxy `--identity`
1458
+ is refused (the link could never open); with one, an unbound invite gets a warning line. Through `chi --remote`
1459
+ it needs that chi web to have bound links (this version or newer), else it is refused before anything is issued.
1460
+ See [configuration.md](configuration.md#inviting-colleagues).
1436
1461
  - The owner's own token and the `chi web --new-token` rotation are separate: rotating it ends no participant's link.
1437
1462
  - **In the attached terminal.** A participant's message reads `Alex ▸ review> is step 3 right?`, the name in that
1438
1463
  participant's own colour (queued, running, and the last exchange when you join); yours stay as they were. A line
@@ -1400,6 +1400,8 @@ toolkit:
1400
1400
  kits: # your own, on a base
1401
1401
  audit: { base: lean, add: [web_fetch, delegate], remove: [memory_write] }
1402
1402
  terse: { base: lean, prompt: "- Be concise in your responses" }
1403
+ web: { base: lean, groups: [chrome] }
1404
+ coding: { base: lean, groups: [] }
1403
1405
  ```
1404
1406
 
1405
1407
  | toolkit | declares |
@@ -1412,8 +1414,21 @@ toolkit:
1412
1414
  | `review` | `read`, `memory_read` (not `forget_outputs`, even under the `forget` layer): what a [participant's](internals/participants.md#turns) turn may call, whatever the session declares (the review boundary, [guardrails](guardrails.md#a-participants-review-boundary)). Reserved: `toolkit.kits.review` is refused ("review is reserved for participants' turns"), and a bundle can't declare it. A session may run on it too, as any kit |
1413
1415
 
1414
1416
  - **Plugin tools** are declared in every toolkit unless its `remove:` names them, and so is `forget_outputs` under
1415
- the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:` and `prompt:` (any other
1416
- key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit` ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil)).
1417
+ the `forget` layer. A `kits:` entry takes `base:` (default `full`), `add:`, `remove:`, `prompt:` and `groups:`
1418
+ (any other key, a typo or a `tools:` list, is ignored with a warning); it can't reuse a built-in's name
1419
+ (`review`'s least of all: it is the participants' boundary). A bundle declares its own with `chi.toolkit`
1420
+ ([plugins](plugins.md#chitoolkitname-base-add--remove--prompt-nil-groups-nil)).
1421
+ - **Tool-index groups.** `groups:` selects source groups by name or glob (`chrome`, `db*`); for the MCP bundle
1422
+ each group is a server. Selectors are sorted and deduplicated and match the groups declared when plugins load.
1423
+ An omitted key inherits the base's selectors; `full` and `lean` (and `child`, which is on `lean`) reach all
1424
+ groups, `review` (the participants' kit) reaches none, `groups: ["*"]` matches every group, and `groups: []`
1425
+ reaches none. A kit whose base doesn't resolve (an unknown name, a cycle) falls back to `full`, which reaches
1426
+ every group.
1427
+ A selector that matches no group in that load-time snapshot warns once. `find_tools` and `call_tool` are
1428
+ declared only when the toolkit reaches a source group or has a reachable built-in. Calls naming an unreached
1429
+ group are refused before guardrails and do not start its server. The first-run init wait is unchanged: it can
1430
+ still block the first turn even when its server's group is unreached; `start: eager` and stale-cache refreshes
1431
+ also still start. `/mcp` marks servers outside the current toolkit.
1417
1432
  - **A kit's prompt text.** `prompt:` (text, several lines allowed) goes into the system prompt of the sessions on
1418
1433
  that kit, verbatim, with no header: after AGENTS.md and the appended files, before the memory indexes. A kit
1419
1434
  without one inherits its base's (`audit: {base: terse, add: [web_fetch]}` stays concise); `prompt: ""` clears
@@ -1422,12 +1437,13 @@ toolkit:
1422
1437
  fits the kit's work (the coordinator bundle's `coding`: be concise): one that isn't text or is over 2000
1423
1438
  characters is left out with a warning, and long text belongs in `append_system_prompt:`. `/toolkit` shows it
1424
1439
  (`prompt: …`), `/stats` its kit and size (`kit prompt:`), the web's tools chip in its tooltip.
1425
- - **Saved at creation, never re-resolved from config.** A session stores its toolkit as named
1440
+ - **Saved at creation, definitions resolve at worker start.** A session stores its toolkit as named
1426
1441
  (`"toolkit": {"name": "lean", "add": [], "remove": []}`) when it is created: `toolkit.default`, `toolkit.child`
1427
1442
  for a delegate child (or the kit its `delegate` call names), or `chi --toolkit NAME` / `--tool` / `--no-tool` ([cli](cli.md)). Changing `toolkit.default`
1428
1443
  doesn't move existing sessions, not even when their worker is woken again; a session from before toolkits is on
1429
- `full`. A continue and a fork keep the toolkit. `/toolkit` shows and changes it. The prompt text is part of the
1430
- kit, not of the session file: it follows the name.
1444
+ `full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
1445
+ text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
1446
+ worker start.
1431
1447
  - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1432
1448
  `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
1433
1449
  the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
@@ -1435,10 +1451,10 @@ toolkit:
1435
1451
  tool in a toolkit's `remove:`, a `--no-tool` or a `/toolkit remove` is **removed**: hard, also after an add, and
1436
1452
  call_tool can't reach it (`child` removes `memory_write`, `delegate` and `delegate_result`, so a child on it
1437
1453
  starts no children). To keep a built-in away from the model, name it under `remove:`. `find_tools` and `call_tool`
1438
- are declared when a toolkit has a reachable built-in (or an MCP server is configured); `full` with no MCP server
1454
+ are declared when a toolkit has a reachable built-in or reaches a source group; `full` with no reachable source
1439
1455
  declares neither, and removing them there (`--no-tool call_tool`) leaves nothing out: no prompt line, no chip.
1440
1456
  - **On `api: anthropic`, reachable tools are callable by name.** Every tool `call_tool` reaches (the reachable
1441
- built-ins, the MCP servers' tools as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1457
+ built-ins, tools from reached MCP groups as `mcp_<server>_<tool>`) also goes in the request's tools deferred
1442
1458
  (`defer_loading`: Messages keeps them out of the model's view until something loads them, at a flat ~100 tokens).
1443
1459
  A direct call of one (`list_sessions`, `mcp_fs_read`) runs, under the same guardrails, approvals and rows as
1444
1460
  through `call_tool` (an MCP tool's call is `call_tool`'s). A `find_tools` hit **loads** its matches (up to 5):
@@ -1455,10 +1471,13 @@ toolkit:
1455
1471
  the route: `Error: delegate isn't declared in this session (toolkit lean); call it with call_tool(tool:
1456
1472
  "chi/delegate", args: {…}) (find_tools gives its inputSchema).` A removed tool's: `Error: memory_write isn't
1457
1473
  loaded in this session (toolkit child) and can't be loaded from here; only the user can (/toolkit add
1458
- memory_write).` `/toolkit` lists both kinds, and the web's toolkit chip says them in its tooltip.
1474
+ memory_write).` An MCP tool in an unreached group gets a toolkit-specific refusal before guardrails; a deferred
1475
+ stub for it points to the toolkit instead of suggesting a search. `/toolkit` lists group selectors and the
1476
+ change; the web's toolkit chip says its source-group scope in the tooltip.
1459
1477
  - **Applied at a turn's start.** A change (`/toolkit`, a plugin's `ctx.use_toolkit`) is saved at once and the next
1460
- turn declares it, with its kit's prompt text; once the session has a model reply, `/toolkit` asks first
1461
- ("Changes that restart the prompt cache"), also for a kit with the same tools and another prompt text.
1478
+ turn declares it, with its kit's prompt text and group filter; once the session has a model reply, `/toolkit`
1479
+ asks first ("Changes that restart the prompt cache"), also when only the group selectors change and the
1480
+ declared tools stay the same.
1462
1481
  - **A name that doesn't resolve** (a typo, a bundle that was uninstalled) declares every tool for that process,
1463
1482
  with one `toolkit>` warning at the first turn; the saved name stays, so the bundle's return brings it back.
1464
1483
  - **Versions.** A toolkit's definition (its tools and its prompt text) can change with chi or a bundle, which
@@ -1608,7 +1627,11 @@ web:
1608
1627
 
1609
1628
  Experimental. `chi web` binds 127.0.0.1 only; to use it from elsewhere over https, put it behind a proxy that does TLS
1610
1629
  and, usually, a sign-in (oauth2-proxy, nginx `auth_request`, Caddy `forward_auth`, Cloudflare Access, Tailscale
1611
- Serve; a work pod forwarding `https://<port>.<pod>.<domain>` through oauth2-proxy) and tell chi web its public URL:
1630
+ Serve; a work pod forwarding `https://<port>.<pod>.<domain>` through oauth2-proxy) and tell chi web its public URL.
1631
+ The proxy must run **on the same machine** as chi web: only a request from a loopback peer (127.0.0.0/8, ::1) counts
1632
+ as one through the proxy, with its identity header. A proxy on another host, reaching chi web on its LAN address
1633
+ (`web.host: lan`), isn't supported: its requests are LAN requests (the LAN token; a link bound to an account refused),
1634
+ since any LAN device could send the same headers.
1612
1635
 
1613
1636
  ```yaml
1614
1637
  web:
@@ -1649,23 +1672,21 @@ What changes once `web.public_url` is set:
1649
1672
  to the proxy, but it outlives the sign-in's redirects: the sign-in page sits at that URL (its scripts can read it)
1650
1673
  and it stays in the browser's history, which is why it holds a code that works once. The page trades the code for
1651
1674
  the cookie (`POST /api/token`) and drops it from the URL; a page that already has a session asks first, since the
1652
- trade replaces it. A code presented with the wrong identity is used up and gives nothing, so whoever already holds
1653
- a link can spend it (`chi web` again, or a new invite, makes another). The 401 page's paste box posts the token in
1675
+ trade replaces it. An owner's code (or an unbound participant's) presented with the wrong identity is used up and
1676
+ gives nothing, so whoever already holds a link can spend it (`chi web` again, or a new invite, makes another); a
1677
+ link bound to one account ([Inviting colleagues](#inviting-colleagues)) is refused to anyone else and keeps its code. The 401 page's paste box posts the token in
1654
1678
  the request's body, never in the URL.
1655
1679
  - When the proxy's sign-in or chi's token ends, the page says so: it reloads once (the proxy's sign-in brings you
1656
1680
  back), and within a minute of that shows "Signed out … Reload" instead of a list that quietly stopped updating. Its
1657
1681
  API calls ask for JSON, so oauth2-proxy answers an ended sign-in with 401 instead of a redirect.
1658
- - A session's Bridge (its worker's own port, unauthenticated) refuses a request that carries a proxy header such as
1659
- `X-Forwarded-For` or `Forwarded` (403 `proxied`). That is a heuristic, not a lock: a forward that adds no such header
1660
- (nginx's `proxy_pass` with its defaults, a plain TCP forward) still reaches a worker, as the owner.
1682
+ - A session's Bridge (its worker's own port) answers only a request that names its secret, which only chi's own
1683
+ clients on this machine read (from the worker's 0600 `bridge.json`): a proxy or tunnel that forwards every local
1684
+ port reaches the port but gets 403. It also refuses a request that carries a proxy header such as
1685
+ `X-Forwarded-For` or `Forwarded` (403 `proxied`).
1661
1686
  - `chi --remote` to a chi web behind a sign-in proxy isn't supported yet.
1662
1687
  SSH to that machine works instead: forward its port and add it as a remote
1663
1688
  ([Through an SSH tunnel](cli.md#through-an-ssh-tunnel)).
1664
1689
 
1665
- > **Warning.** Until Bridges get a secret of their own (planned), don't run chi web behind a proxy or tunnel that can
1666
- > reach any local port, such as a pod forwarding every `https://<port>.<pod>…`, unless it adds `X-Forwarded-For` or
1667
- > `Forwarded` to every request; and never one that also lets other people through its sign-in.
1668
-
1669
1690
  Which header to trust: only one the proxy **sets** on every signed-in request **and replaces** when a client sends
1670
1691
  it. Check before setting `identity_header`: run an echo on a free port behind the same proxy (`python3 -c` with
1671
1692
  `http.server`, or any header-echo server), open it signed in, and send your own copies (`curl -H 'X-Forwarded-Email:
@@ -1685,6 +1706,33 @@ one.
1685
1706
 
1686
1707
  Keep the state dir (`web-token`, the grants) out of any folder others can read, such as a team's shared folder.
1687
1708
 
1709
+ ### Inviting colleagues
1710
+
1711
+ Behind a proxy whose sign-in lets colleagues in (a work pod behind Okta and oauth2-proxy, a tailnet you share), give
1712
+ each reviewer a link bound to their account:
1713
+
1714
+ ```sh
1715
+ chi share 3f9a1c2e --name Alex --identity alex@corp.example
1716
+ # p_7c01d2aa Alex (human, reviewer) · session 3f9a1c2e · expires … · cap $2.00 · bound to alex@corp.example
1717
+ # https://4567.pod.example.com/#code=chc_…&s=3f9a1c2e-…
1718
+ ```
1719
+
1720
+ - It needs `web.proxy.identity_header` (or `chi web --tunnel tailscale`): the link opens only through the proxy,
1721
+ signed in as that email (any case). Opened by someone else (forwarded, pasted in the wrong chat), on the LAN or on
1722
+ this machine, it says "This review link is for another account …", and its one-time code stays: Alex can still open
1723
+ it. The refusal is in the session's participants audit with the identity that tried. Without an identity header
1724
+ `--identity` is refused: the link could never open.
1725
+ - Run only chi 0.109 or newer on the same state dir: an older chi rewriting the grants store drops the binding and
1726
+ makes every bound link open for anyone the proxy lets in.
1727
+ - An unbound link there opens for anyone the proxy lets in who has it; `chi share` warns when you make one. The web's
1728
+ Invite has an email field behind such a proxy (the name defaults to the email's first part).
1729
+ - The identity is what the trusted header carries: the work email through Okta, the Tailscale login (`alex@github`)
1730
+ through Tailscale Serve. Bind to the one your proxy sends. A tagged Tailscale device has no user login, so a bound
1731
+ link refuses it.
1732
+ - The header is the proxy's word only for requests from this machine (the proxy is local; a LAN device's copy is
1733
+ ignored): any program on this machine that reaches chi web's port directly can send it too. On a machine or pod only you use, that is you already; on a host
1734
+ other users share, a bound link's identity isn't a boundary against them.
1735
+
1688
1736
  ### Tailscale Serve (your tailnet)
1689
1737
 
1690
1738
  chi web on your phone away from the home Wi-Fi: [Tailscale](https://tailscale.com) Serve gives this Mac an https
@@ -1798,6 +1846,9 @@ What it needs:
1798
1846
 
1799
1847
  Turning it on, on the phone: open the `Proxied:` link in Safari → Share → **Add to Home Screen** → open the **Chi**
1800
1848
  app → tap the **bell** at the top → turn on **This device** in the sheet → Allow. A test notification arrives at once.
1849
+ The app opens on the project chi web starts in (`web.start_dir`; the header's `all` shows every project), but its
1850
+ notifications are every project's: opening it closes the ones whose question or card is no longer open and sets its
1851
+ badge for every project, whichever project the page shows.
1801
1852
  In a browser tab the bell stays the tab's own notifications (while the tab is in the background), as before.
1802
1853
 
1803
1854
  **The devices** (the bell's sheet in the app; a bottom sheet on the phone): this device first, with its switch (off
@@ -1809,6 +1860,8 @@ are never sent to; `chi web push rm ID` removes them. From the Mac: `chi web pus
1809
1860
  shows a recent delivery (Apple keeps accepting pushes for it): if one stops getting notifications, switch it off and on
1810
1861
  there; the new subscription replaces the dead one.
1811
1862
 
1863
+ - **What a tap opens**: the session, in its own project's view (the all view for a session in no repo), whichever
1864
+ project the app was showing; an open app window moves there.
1812
1865
  - **What is sent**: the session's name and the reason ("needs approval", "needs an answer", "hit its step limit",
1813
1866
  "needs you", "turn failed", "done in N s"), never the question or the command, as the tab's notification on the
1814
1867
  Mac. The session's name shows on the lock screen and the Watch.
@@ -1970,6 +2023,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
1970
2023
  | `web.working_set` | `true` | yes | The stage view's working set: the files, memories and URLs a turn brought into the model's context, in a rail in the stage, a footprint line under each finished turn and a `set N` chip in place of the mem chip; `false` brings the mem chip back. `?working_set=1` or `?working_set=0` on the page URL overrides it for that page load; the turn view ignores it. See [CLI: the working set](cli.md#the-working-set). |
1971
2024
  | `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). |
1972
2025
  | `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). |
2026
+ | `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; `/?scope=all` is every project. `~` and a relative path work; one that isn't a folder stops `chi web` from starting. `chi web` never changes its own folder. See [CLI](cli.md). |
1973
2027
  | `web.participants.expires` | `7d` | | How long a participant's link lasts unless `chi share --expires` says otherwise: `Nh` or `Nd`, 1h to 30d. See [participants](internals/participants.md). |
1974
2028
  | `web.participants.cap` | `2.0` | | What a participant's turns, and the side calls they cause, may cost you in dollars unless `chi share --cap` says otherwise ($0 to $1000); at the cap their next comment is refused (429 `cap_reached`) and you get a notice. `chi share cap ID P DOLLARS` changes one grant's. |
1975
2029
  | `web.participants.queue` | `3` | | How many of a participant's turns may wait behind their running one (0 to 20); one more is refused (429 `too_many_turns`). |