@loicngr/kobo 2.1.26 → 2.1.28

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 (126) hide show
  1. package/AGENTS.md +6 -3
  2. package/CHANGELOG.md +11 -0
  3. package/CONFIGURATION.md +97 -49
  4. package/SECURITY.md +33 -6
  5. package/dist/mcp-server/kobo-tasks-server.js +36 -1021
  6. package/dist/mcp-server/managed-mcp-dispatcher.js +960 -0
  7. package/dist/mcp-server/workspace-dialogue-client.js +26 -1
  8. package/dist/server/index.js +52 -19
  9. package/dist/server/middleware/host-check-middleware.js +8 -5
  10. package/dist/server/middleware/network-auth-middleware.js +51 -29
  11. package/dist/server/middleware/security-headers-middleware.js +19 -0
  12. package/dist/server/middleware/traffic-limits-middleware.js +74 -0
  13. package/dist/server/routes/auth.js +73 -0
  14. package/dist/server/routes/internal-mcp.js +137 -0
  15. package/dist/server/routes/mcp-credentials.js +30 -0
  16. package/dist/server/routes/mcp.js +156 -76
  17. package/dist/server/routes/workspaces.js +19 -3
  18. package/dist/server/services/agent/orchestrator.js +27 -17
  19. package/dist/server/services/external-mcp-credentials-service.js +110 -0
  20. package/dist/server/services/forge/github/provider.js +15 -3
  21. package/dist/server/services/forge/types.js +8 -0
  22. package/dist/server/services/internal-mcp-capability-service.js +98 -0
  23. package/dist/server/services/mcp-connection-info-service.js +1 -1
  24. package/dist/server/services/memory-mcp-service.js +9 -7
  25. package/dist/server/services/memory-service.js +15 -0
  26. package/dist/server/services/network-access-service.js +43 -73
  27. package/dist/server/services/network-session-service.js +182 -0
  28. package/dist/server/services/settings-service.js +29 -7
  29. package/dist/server/services/terminal-websocket-service.js +5 -0
  30. package/dist/server/services/websocket-service.js +5 -0
  31. package/dist/server/utils/paths.js +2 -1
  32. package/dist/server/utils/startup-display.js +1 -2
  33. package/dist/shared/models.js +6 -0
  34. package/docker-compose.vps.yml +6 -0
  35. package/docs/deployment-vps.md +24 -12
  36. package/package.json +1 -1
  37. package/src/client/dist/pwa/assets/ActivityFeed-D58VS9o2.js +2 -0
  38. package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-DOr41E2Y.js → AutoLoopFinalReviewSettings-CK3r_fVB.js} +1 -1
  39. package/src/client/dist/pwa/assets/{ChangelogPage-DOCgZMpn.js → ChangelogPage-CAAUEn5X.js} +1 -1
  40. package/src/client/dist/pwa/assets/ComparisonPanel-BCb7vB5E.js +2 -0
  41. package/src/client/dist/pwa/assets/CreatePage-DasH1Uk4.js +2 -0
  42. package/src/client/dist/pwa/assets/{DashboardPage-dBpJaQSb.js → DashboardPage-XE52EABP.js} +1 -1
  43. package/src/client/dist/pwa/assets/DiffViewer-CncKlXne.js +8 -0
  44. package/src/client/dist/pwa/assets/{DocumentsPanel-AqLx0MzB.js → DocumentsPanel-L4pbQfz1.js} +1 -1
  45. package/src/client/dist/pwa/assets/{DrawerToggleButton-BLWaFgh0.js → DrawerToggleButton-DiOJm5he.js} +1 -1
  46. package/src/client/dist/pwa/assets/GitPanel-WJlzy2VJ.js +3 -0
  47. package/src/client/dist/pwa/assets/{GitPanel-CRaLJzjc.css → GitPanel-yHvU38dp.css} +1 -1
  48. package/src/client/dist/pwa/assets/{HealthPage-DeryVLyz.js → HealthPage-hJY5MurK.js} +1 -1
  49. package/src/client/dist/pwa/assets/MainLayout-DgWElK7w.js +2 -0
  50. package/src/client/dist/pwa/assets/{MemoryPanel-B34DENtz.js → MemoryPanel-DtEvh_b7.js} +1 -1
  51. package/src/client/dist/pwa/assets/{MemoryProposalList-BpN_OU8J.js → MemoryProposalList-kgFCm9zz.js} +1 -1
  52. package/src/client/dist/pwa/assets/{QBtnDropdown-BPcK_rVs.js → QBtnDropdown-CWPIugs8.js} +1 -1
  53. package/src/client/dist/pwa/assets/{QExpansionItem-CpwQ8L_F.js → QExpansionItem-D-EGL2b3.js} +1 -1
  54. package/src/client/dist/pwa/assets/{QScrollArea-BkNSVl7s.js → QScrollArea-BMrqK-xd.js} +1 -1
  55. package/src/client/dist/pwa/assets/QSelect-DFWyDWCE.js +1 -0
  56. package/src/client/dist/pwa/assets/{QTooltip-Dp8fitHV.js → QTooltip-Bk8dLUtU.js} +1 -1
  57. package/src/client/dist/pwa/assets/{SchedulePanel-Ck8LKDCa.js → SchedulePanel-CuxmxCKi.js} +1 -1
  58. package/src/client/dist/pwa/assets/{SearchPage-BAUZg1zq.js → SearchPage-DKnQPUqY.js} +1 -1
  59. package/src/client/dist/pwa/assets/{SettingsPage-Dcq1Jeoo.css → SettingsPage-B4rmd2WX.css} +1 -1
  60. package/src/client/dist/pwa/assets/SettingsPage-CO3vIpPd.js +9 -0
  61. package/src/client/dist/pwa/assets/{SplitWorkspacePage-CH8OHmtn.js → SplitWorkspacePage-WhQyX8fn.js} +1 -1
  62. package/src/client/dist/pwa/assets/{TerminalPanel-CBmZ0IzQ.js → TerminalPanel-BY0lPq6k.js} +10 -10
  63. package/src/client/dist/pwa/assets/{TerminalPanel-Dz9EE-Ph.css → TerminalPanel-BcbwuFVR.css} +1 -1
  64. package/src/client/dist/pwa/assets/{ToolCallItem-CLtGTV6w.js → ToolCallItem-DYubi-J0.js} +3 -3
  65. package/src/client/dist/pwa/assets/{TouchPan-9SH2dx6m.js → TouchPan-DDotyYLN.js} +1 -1
  66. package/src/client/dist/pwa/assets/{TourReplayButton-Dw4sGf9B.js → TourReplayButton-kAnEMcy2.js} +1 -1
  67. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-wd_sObJa.js → WorkspaceAttentionLabels-DiYt3rP3.js} +1 -1
  68. package/src/client/dist/pwa/assets/WorkspacePage-K-1ce50d.js +4 -0
  69. package/src/client/dist/pwa/assets/{browser-CC4W8r76.js → browser-DVYLH8vl.js} +1 -1
  70. package/src/client/dist/pwa/assets/{build-path-tree-BBuyB-my.js → build-path-tree-DEuOdIIk.js} +1 -1
  71. package/src/client/dist/pwa/assets/custom-sounds-Ggw6V9iF.js +1 -0
  72. package/src/client/dist/pwa/assets/de-Bk3zQrin.js +17 -0
  73. package/src/client/dist/pwa/assets/{editor.api-BwfxvQ88.js → editor.api-BC38eXAS.js} +1 -1
  74. package/src/client/dist/pwa/assets/{efforts-B3I7w5bs.js → efforts-TGacUPcJ.js} +1 -1
  75. package/src/client/dist/pwa/assets/es-f3xuAWA9.js +17 -0
  76. package/src/client/dist/pwa/assets/{expand-template-Ci9UjnxZ.js → expand-template-DM8gq3Mo.js} +1 -1
  77. package/src/client/dist/pwa/assets/{format-CJIzi3Vf.js → format-OwxXBnDJ.js} +1 -1
  78. package/src/client/dist/pwa/assets/{formatters-DQ8cP7nO.js → formatters-YgaCZcwJ.js} +1 -1
  79. package/src/client/dist/pwa/assets/fr-B6c21SG3.js +17 -0
  80. package/src/client/dist/pwa/assets/{freemarker2-DbpVO-cL.js → freemarker2-l7R42fI2.js} +1 -1
  81. package/src/client/dist/pwa/assets/{handlebars-BHve0PpA.js → handlebars-DFmPrgjH.js} +1 -1
  82. package/src/client/dist/pwa/assets/{html-BhUkdFPz.js → html-CERIAlPM.js} +1 -1
  83. package/src/client/dist/pwa/assets/i18n-WaE4cZK9.js +1 -0
  84. package/src/client/dist/pwa/assets/index-CpWBGKdM.js +20 -0
  85. package/src/client/dist/pwa/assets/it-DUstDpC2.js +17 -0
  86. package/src/client/dist/pwa/assets/{javascript-BLAorPa3.js → javascript-BBYncplF.js} +1 -1
  87. package/src/client/dist/pwa/assets/{liquid-CJGF-h-f.js → liquid-C1HqYyYU.js} +1 -1
  88. package/src/client/dist/pwa/assets/{mdx-Cm0oNz_l.js → mdx-E74dsP22.js} +1 -1
  89. package/src/client/dist/pwa/assets/{monaco-contributions-nBg6wf05.js → monaco-contributions-DHvm-kp5.js} +1 -1
  90. package/src/client/dist/pwa/assets/{monaco.contribution-hr9s26Ud.js → monaco.contribution-DYdn9CwR.js} +2 -2
  91. package/src/client/dist/pwa/assets/network-auth-DTZUKbny.js +1 -0
  92. package/src/client/dist/pwa/assets/{notify-theme-EcbxuH_8.js → notify-theme-FuTL7HuS.js} +1 -1
  93. package/src/client/dist/pwa/assets/{permissionModes-DLWiBfdb.js → permissionModes-D-Nexnoo.js} +1 -1
  94. package/src/client/dist/pwa/assets/{project-color-vDuTaTJn.js → project-color-_snXWYUp.js} +1 -1
  95. package/src/client/dist/pwa/assets/{python-BEjbzFAQ.js → python-DNAAGBcX.js} +1 -1
  96. package/src/client/dist/pwa/assets/{razor-BJPT7Wxx.js → razor-CB3g5p_T.js} +1 -1
  97. package/src/client/dist/pwa/assets/{typescript-DPqM46Ef.js → typescript-0kpz8bqP.js} +1 -1
  98. package/src/client/dist/pwa/assets/{use-search-index-status-BwwrB410.js → use-search-index-status-D38yFOD_.js} +1 -1
  99. package/src/client/dist/pwa/assets/{use-tours-B0KtCS9w.js → use-tours-BWkHp34X.js} +2 -2
  100. package/src/client/dist/pwa/assets/{use-worktree-restore-tzd2FESU.js → use-worktree-restore-DUjZr44N.js} +1 -1
  101. package/src/client/dist/pwa/assets/{workspace-templates-CubzsZUU.js → workspace-templates-D-lPJQ4G.js} +1 -1
  102. package/src/client/dist/pwa/assets/{xml-C6SVzw5_.js → xml-BbOuqdUe.js} +1 -1
  103. package/src/client/dist/pwa/assets/{yaml-OpCqnv4g.js → yaml-gasFQtfk.js} +1 -1
  104. package/src/client/dist/pwa/index.html +1 -1
  105. package/src/client/dist/pwa/sw.js +1 -1
  106. package/src/mcp-server/README.md +13 -19
  107. package/src/mcp-server/kobo-tasks-server.ts +42 -1146
  108. package/src/mcp-server/managed-mcp-dispatcher.ts +1071 -0
  109. package/src/mcp-server/workspace-dialogue-client.ts +26 -1
  110. package/src/client/dist/pwa/assets/ActivityFeed-DfYrVNEh.js +0 -2
  111. package/src/client/dist/pwa/assets/ComparisonPanel-b5nTBjK6.js +0 -2
  112. package/src/client/dist/pwa/assets/CreatePage-B7VPHEqm.js +0 -2
  113. package/src/client/dist/pwa/assets/DiffViewer-BLQz2Qyx.js +0 -8
  114. package/src/client/dist/pwa/assets/GitPanel-Ba7ltCsM.js +0 -3
  115. package/src/client/dist/pwa/assets/MainLayout-B2XvjdWi.js +0 -2
  116. package/src/client/dist/pwa/assets/QSelect-DrLpbvcr.js +0 -1
  117. package/src/client/dist/pwa/assets/SettingsPage-Cj6kSVT5.js +0 -9
  118. package/src/client/dist/pwa/assets/WorkspacePage-D4ODPOG-.js +0 -4
  119. package/src/client/dist/pwa/assets/custom-sounds-BbPNRzAs.js +0 -1
  120. package/src/client/dist/pwa/assets/de-D9VzPDGF.js +0 -17
  121. package/src/client/dist/pwa/assets/es-3R4MJuTt.js +0 -17
  122. package/src/client/dist/pwa/assets/fr-DXwDVnJ4.js +0 -17
  123. package/src/client/dist/pwa/assets/i18n-CGLQAADf.js +0 -1
  124. package/src/client/dist/pwa/assets/index-CNcXBpzz.js +0 -20
  125. package/src/client/dist/pwa/assets/it-T9AjnG42.js +0 -17
  126. package/src/client/dist/pwa/assets/network-auth-C850jGfI.js +0 -1
package/AGENTS.md CHANGED
@@ -6,7 +6,9 @@ Guidance for AI coding agents (Claude Code, Cursor, etc.) working on this reposi
6
6
 
7
7
  **Kōbō** (工房, Japanese for "workshop") orchestrates Claude Code and OpenAI Codex agents across isolated git worktrees. Each "workspace" is a self-contained mission with its own worktree, branch, agent sessions, optional dev server, optional Notion/Sentry context, and a dedicated MCP tools server. A Vue 3 UI lets the human track progress, read live agent output, review Git changes, and manage the lifecycle. A workspace can switch engines through a handoff to a fresh session while preserving its worktree and history.
8
8
 
9
- Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled, which binds all interfaces and gates non-loopback API/WS requests behind a shared token. Behind-a-reverse-proxy mode also requires the token for loopback API/WS requests; `/api/health` stays exempt. Host/Origin checks apply separately. No multi-tenant concerns.
9
+ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the opt-in "network access" setting is enabled. Operator HTTP access uses the network token or a 12-hour HttpOnly browser session; browsers use 60-second single-use path-bound tickets for WebSocket upgrades, never the operator token in the URL or localStorage. Behind-a-reverse-proxy mode also gates loopback operator requests. Rotation, network disable and proxy-mode changes revoke sessions, tickets and active WebSockets. `/api/health` stays exempt. Host/Origin checks apply separately; public proxy origin must be declared explicitly. No multi-tenant concerns.
10
+
11
+ External `/api/mcp` and global stdio require dedicated hashed credentials with tool/workspace grants, expiry and revocation, even on loopback. Manage them through operator-only `/api/mcp-credentials`; global stdio uses `KOBO_MCP_TOKEN`. Managed workspace stdio uses per-launch `KOBO_MCP_SESSION_TOKEN` through OS-loopback-only `/api/internal-mcp`, never the operator token or direct SQLite writes. The backend enforces Plan/read-only permissions and revokes capabilities when stopping begins. Keep report/progress exceptions narrowly scoped. See [SECURITY.md](./SECURITY.md).
10
12
 
11
13
  ## Tech stack
12
14
 
@@ -103,7 +105,8 @@ src/
103
105
  │ ├── pages/ # WorkspacePage, CreatePage, SettingsPage
104
106
  │ └── router/
105
107
  ├── mcp-server/ # standalone MCP server spawned per workspace
106
- │ ├── kobo-tasks-server.ts # entrypoint, registers tools
108
+ │ ├── kobo-tasks-server.ts # stdio bridge; forwards discovery/calls to authenticated backend
109
+ │ ├── managed-mcp-dispatcher.ts # workspace tool definitions/dispatch, executed in backend
107
110
  │ └── kobo-tasks-handlers.ts # pure handlers (list_tasks, mark_task_done)
108
111
  ├── shared/ # model catalogues, prompts, dialogue contracts, shared utilities
109
112
  └── __tests__/ # backend Vitest suites; client tests live under client/src/__tests__/
@@ -292,7 +295,7 @@ also bypasses the auto-loop next-iteration UI path; normal backend immediate
292
295
  message delivery pauses a ready loop. Do not flush existing queues on toggle,
293
296
  remove compaction/permission/handoff guards, or queue rejected forced sends.
294
297
 
295
- `POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind the existing Host/Origin and network-token gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
298
+ `POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind Host/Origin, network enablement and dedicated MCP credential gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
296
299
 
297
300
  Optional `send_workspace_message.idempotency_key` uses `mcp_message_requests` (migration v43). Reserve before dispatch, preserve fingerprints and terminal outcomes, and never automatically resend an unfinished request after restart. The user event and accepted receipt commit in one SQLite transaction; broadcast follows commit. Dispatch may precede persistence, so interrupted requests are explicitly `delivery_unknown`, not an exactly-once execution promise. The backend alone calls `reconcileMessageRequests` on startup. History retention does not delete the ledger; workspace deletion cascades it.
298
301
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 2.1.28
8
+
9
+ - fix(ci): authenticate packaged mcp smoke tests
10
+ - fix(security): harden authentication and mcp access
11
+ - feat(models): Add Haiku 5.5
12
+
13
+ ## 2.1.27
14
+
15
+ - fix(security): harden authentication and mcp access
16
+ - feat(models): Add Haiku 5.5
17
+
7
18
  ## 2.1.26
8
19
 
9
20
  - feat: switch Claude and Codex accounts with ccswap
package/CONFIGURATION.md CHANGED
@@ -887,11 +887,28 @@ destination worktree and sends file/image references to the agent and chat.
887
887
  Keyed retries include attachment identity and never upload or send twice. See
888
888
  [Sending files and images](src/mcp-server/README.md#sending-files-and-images).
889
889
 
890
- Remote HTTP clients follow the [network access](#network-access) settings and supply the Kōbō token as `Authorization: Bearer <token>` or `X-Kobo-Token`. The endpoint uses the same Host/Origin and reverse-proxy policy as the rest of the backend. No network setting is changed automatically.
890
+ Every external MCP client, including loopback and global stdio clients, must use
891
+ a dedicated MCP key. The operator token and browser session cookie are not accepted
892
+ by `/api/mcp`. Send the MCP key as `Authorization: Bearer <key>`. The endpoint retains the same Host/Origin, network-access and
893
+ reverse-proxy policy. No network setting changes automatically.
894
+
895
+ Keys are independent, stored only as hashes and have explicit tool and workspace
896
+ grants, optional expiration and individual revocation. The default tool grant is
897
+ read-only; writes and destructive tools require an explicit grant. Each key must
898
+ select workspace IDs or deliberately grant all workspaces. An MCP key cannot
899
+ authenticate to operator REST, terminal or credential-management endpoints.
900
+
901
+ An authenticated operator manages credentials through `/api/mcp-credentials`:
902
+ GET lists metadata and available tools; POST accepts `name`, optional `tools`,
903
+ `workspaceIds` or `allWorkspaces: true`, and optional ISO `expiresAt`; DELETE
904
+ `/api/mcp-credentials/<id>` revokes one key. Creation returns the raw `token` once.
905
+ Do not send that token to logs, commit it, or reuse it as the operator credential.
891
906
 
892
907
  See [Connect an external LLM](src/mcp-server/README.md#connect-an-external-llm) for connection examples, tool arguments, cursor polling and authentication details.
893
908
 
894
- The **External LLM access (MCP)** panel within global Network access settings provides copyable URLs and HTTP/stdio configurations for this running installation. Previews use token placeholders; copying a configuration with the real token is a separate action. Set a client name to identify its messages and question answers in workspace history.
909
+ The **External LLM access (MCP)** panel within global Network access settings
910
+ provides HTTP/stdio configuration templates. Supply the dedicated MCP key for the
911
+ intended client. Client names are display labels, not permission identities.
895
912
 
896
913
  External senders can supply `idempotency_key` once per intended message and reuse it on retries. The durable receipt survives backend restarts and event retention. An uncertain delivery is explicitly reported and never automatically sent again; see the MCP guide for response codes and limitations.
897
914
 
@@ -1058,8 +1075,9 @@ To disable network access, toggle it off and restart again.
1058
1075
 
1059
1076
  ### The network access token
1060
1077
 
1061
- When network access is enabled, every non-loopback HTTP request and every
1062
- non-loopback WebSocket connection must supply a shared token.
1078
+ When network access is enabled, remote API and WebSocket access requires
1079
+ operator authentication. The shared operator token opens a browser session;
1080
+ external MCP clients use separate scoped credentials.
1063
1081
 
1064
1082
  - **Auto-generated**: Kōbō generates a random token the first time you enable
1065
1083
  the feature. You never have to type one in.
@@ -1075,11 +1093,12 @@ non-loopback WebSocket connection must supply a shared token.
1075
1093
 
1076
1094
  **Option A: QR code (recommended for phones)**
1077
1095
 
1078
- The Settings panel shows a QR code encoding
1079
- `http://<lan-ip>:<port>/?token=<token>`. Scan it with your phone's camera
1080
- while on the same Wi-Fi network; the browser opens directly to a pre-authenticated
1081
- session. The token is stored in the browser's `localStorage` under
1082
- `kobo:network-token` so subsequent visits don't require re-scanning.
1096
+ The Settings panel shows a QR code using a URL fragment (`#token=<token>`),
1097
+ which browsers do not send in HTTP requests. The client immediately removes the
1098
+ fragment and exchanges the token for a 12-hour HttpOnly, SameSite=Strict session
1099
+ cookie (Secure for a configured HTTPS proxy). The operator token is not retained in localStorage.
1100
+ Previously stored tokens are removed during migration. Use QR links only on
1101
+ trusted devices; do not publish or bookmark a credential-bearing link.
1083
1102
 
1084
1103
  **Option B: manual URL**
1085
1104
 
@@ -1087,22 +1106,27 @@ session. The token is stored in the browser's `localStorage` under
1087
1106
  `http://<lan-ip>:<port>/`).
1088
1107
  2. Open it on the remote device.
1089
1108
  3. A login dialog appears: paste the token from the Settings panel and confirm.
1090
- 4. The token is stored in `localStorage`; subsequent visits auto-authenticate.
1109
+ 4. The HttpOnly session cookie authenticates subsequent visits until expiry or revocation.
1091
1110
 
1092
1111
  **Host machine (localhost)**: connect to `http://localhost:<port>` as usual.
1093
1112
  No token is needed unless **Behind a reverse proxy** is enabled.
1094
1113
 
1095
1114
  ### Token transport
1096
1115
 
1097
- | Connection type | How to supply the token |
1116
+ | Connection type | Authentication |
1098
1117
  |---|---|
1099
- | HTTP requests | `X-Kobo-Token: <token>` header |
1100
- | MCP HTTP requests (`/api/mcp`) | `X-Kobo-Token` or `Authorization: Bearer <token>` |
1101
- | WebSocket connections | `?token=<token>` query parameter on the upgrade URL |
1102
-
1103
- The Kōbō browser client handles both automatically once the token is stored in
1104
- `localStorage`. Third-party scripts or API consumers must supply the header or
1105
- query param manually.
1118
+ | Browser API requests | HttpOnly session cookie after login |
1119
+ | Operator scripts / REST | `X-Kobo-Token: <operator-token>` header |
1120
+ | External MCP HTTP (`/api/mcp`) | Dedicated MCP key as `Authorization: Bearer <key>` |
1121
+ | Browser WebSocket connections | Short-lived, single-use ticket in the upgrade URL, bound to its WS path |
1122
+
1123
+ The browser handles session and ticket exchanges automatically. Tickets expire
1124
+ after 60 seconds and cannot be replayed, reused for a different terminal, or used
1125
+ after their session expires. The permanent operator token is no longer accepted
1126
+ in a WebSocket query string. Rotation, disabling network access or changing proxy
1127
+ mode invalidates browser sessions, pending tickets and existing main/terminal
1128
+ WebSocket connections. Agent processes are not stopped by credential revocation.
1129
+ Server restart also expires browser sessions.
1106
1130
 
1107
1131
  The IP address used for the loopback exemption is taken **from the OS socket**,
1108
1132
  never from forwarded headers (`X-Forwarded-For` etc.), so a remote client cannot
@@ -1154,19 +1178,15 @@ Practical consequences:
1154
1178
  count as the real interface. In development the Quasar dev server is the one
1155
1179
  exception, since it proxies to the backend and so never matches the `Host`;
1156
1180
  the `dev` npm script declares it through `KOBO_DEV_CLIENT_ORIGIN`.
1157
- - **Behind-a-reverse-proxy mode accepts any host by default**, because only the
1158
- operator knows the expected domain. Declare it with
1159
- `KOBO_NETWORK_ACCESS_PROXY_HOST` (see below) to close that; the bundled
1160
- `docker-compose.example.yml` and `docker-compose.local-traefik.yml` do. That
1161
- mode also disables the loopback exemption, so every `/api/*` request has to
1162
- carry the token — but the SPA itself is served outside `/api/*` and behind no
1163
- token, so without a declared hostname a rebinding page can be served the real
1164
- interface and ask the user to paste their token into it.
1181
+ - **Behind-a-reverse-proxy mode requires a declared public hostname.** Set
1182
+ `KOBO_NETWORK_ACCESS_PROXY_HOST`; missing or invalid configuration rejects
1183
+ public requests. Internal loopback clients remain reachable but still require
1184
+ authentication. The bundled proxy Compose files configure this explicitly.
1165
1185
 
1166
1186
  #### `KOBO_NETWORK_ACCESS_PROXY_HOST`
1167
1187
 
1168
- Set it to the domain your proxy serves Kōbō under, and the host check is
1169
- enforced in behind-a-reverse-proxy mode instead of being skipped:
1188
+ Set it to the public origin your proxy serves Kōbō under. This is required in
1189
+ behind-a-reverse-proxy mode:
1170
1190
 
1171
1191
  ```yaml
1172
1192
  environment:
@@ -1175,9 +1195,33 @@ environment:
1175
1195
  - KOBO_NETWORK_ACCESS_PROXY_HOST=kobo.example.com
1176
1196
  ```
1177
1197
 
1178
- A scheme and a port are accepted and ignored (`https://kobo.example.com:8443`
1179
- reads as `kobo.example.com`). Loopback stays reachable so a local healthcheck
1180
- keeps working. Leaving it unset preserves the previous behaviour.
1198
+ A bare hostname means HTTPS. For a nonstandard port, set the complete origin,
1199
+ e.g. `https://kobo.example.com:8443`. Browser Origin checks compare the scheme,
1200
+ hostname and port exactly. Local HTTP proxy staging must explicitly declare
1201
+ `http://kobo.localhost`. Paths, credentials, query strings and fragments are
1202
+ invalid. Loopback healthchecks and authenticated internal calls remain reachable.
1203
+ Leaving this variable unset rejects public proxy traffic.
1204
+
1205
+ ### Browser response protections
1206
+
1207
+ The server sends `Content-Security-Policy: frame-ancestors 'self'` and
1208
+ `X-Frame-Options: SAMEORIGIN`, preserving Kōbō's split panes while blocking
1209
+ embedding by another site. It also sends `X-Content-Type-Options: nosniff`,
1210
+ `Referrer-Policy: no-referrer` and `Cache-Control: no-store` for API responses.
1211
+ The public HTTPS Compose examples add one-year HSTS at Traefik, without applying
1212
+ it to subdomains or HTTP-only local staging. Kōbō does not trust forwarded headers
1213
+ to decide whether a connection is HTTPS. A broader script/style CSP is not enabled;
1214
+ it needs separate compatibility testing with Quasar and Monaco.
1215
+
1216
+ Application limits apply per OS peer address (forwarded addresses are not trusted):
1217
+ 3,000 API requests per minute and 20 login attempts per minute. Requests exceeding
1218
+ these limits receive HTTP 429. MCP permits 32 concurrent requests, with at most
1219
+ four bodies larger than 1 MiB or of unknown/chunked length; ordinary attachment
1220
+ uploads also have four slots. Authentication runs before body buffering. Main
1221
+ WebSocket messages are capped at 1 MiB, terminal messages at 128 KiB; sockets
1222
+ have per-peer and global caps and inbound message-rate limits. Behind a proxy,
1223
+ clients share the proxy peer's bucket, so configure client-IP limits at the proxy
1224
+ for deployments with many simultaneous clients.
1181
1225
 
1182
1226
  ### Security caveats
1183
1227
 
@@ -1300,7 +1344,7 @@ retrieved once — see [Token / login flow](#token--login-flow).
1300
1344
  | `${HOME}/.claude.json:/root/.claude.json` | RW | Claude Code auth + MCP server registrations. **Must already exist as a file on the host** (run `claude /login` locally first if it doesn't) — Docker silently creates an empty *directory* instead of erroring on a missing bind-mount source, which breaks auth in a confusing way. |
1301
1345
  | `${HOME}/.claude:/root/.claude` | RW | Claude Code's full state directory (skills/plugins cache, session transcripts, memory) — Claude Code writes into this tree during normal operation. |
1302
1346
  | `${HOME}/.codex:/root/.codex` | RW | Codex's full state directory (`auth.json`, `config.toml`). Harmless to mount even if you only use Claude Code. |
1303
- | `/var/run/docker.sock:/var/run/docker.sock` | RW, enabled in `example.yml`, opt-in in `vps.yml` | Lets the dev-server panel start/stop/tail logs for your projects' own `docker compose` stacks. **DANGER**: grants the container root-equivalent control over the host — anyone who can run a command inside `kobo` can spawn a privileged container that mounts the host filesystem. Remove this line if you don't need the dev-server panel. |
1347
+ | `/var/run/docker.sock:/var/run/docker.sock` | RW, opt-in in both `example.yml` and `vps.yml` | Lets the dev-server panel start/stop/tail logs for your projects' own `docker compose` stacks. **DANGER**: grants the container root-equivalent control over the host — anyone who can run a command inside `kobo` can spawn a privileged container that mounts the host filesystem. Leave this mount commented out unless required. |
1304
1348
 
1305
1349
  ### Ports
1306
1350
 
@@ -1316,25 +1360,32 @@ reachable, matching the real production posture.
1316
1360
 
1317
1361
  ### Token / login flow
1318
1362
 
1319
- All four files pre-enable network access via env vars, so there's no interactive Settings-UI step.
1320
- Retrieve the auto-generated token from the container's boot log, then bookmark the token-bearing URL:
1363
+ All four files pre-enable network access via environment variables. Startup logs
1364
+ never contain the token. Retrieve it deliberately from a trusted local terminal:
1321
1365
 
1322
1366
  ```bash
1323
- docker compose -f <compose-file> logs kobo | grep Token
1367
+ docker compose -f <compose-file> exec kobo node -e 'const fs = require("node:fs"); console.log(JSON.parse(fs.readFileSync(process.env.KOBO_HOME + "/settings.json", "utf8")).global.networkAccessToken)'
1324
1368
  ```
1325
1369
 
1326
- Open `http://<host>/?token=<token>` (exact host depends on the file — see the walkthroughs below).
1327
- The token is stored in the browser's `localStorage`, so you only paste it once per browser.
1370
+ Open the plain application URL and paste the token into its login dialog. Do not
1371
+ put the token in a URL, command argument, log collector or shared transcript.
1372
+ For an npm installation, read `networkAccessToken` from your private
1373
+ `$KOBO_HOME/settings.json` (default `~/.config/kobo/settings.json`).
1374
+ Kōbō restricts its home to mode `0700` and settings, temporary settings and
1375
+ settings backups to `0600`, including existing files on startup or the next disk read.
1376
+
1377
+ Existing logs from older releases may still contain a valid token. After upgrading,
1378
+ rotate it from Settings and apply your log-retention policy to retained copies.
1379
+ Kōbō does not delete historical logs automatically.
1328
1380
 
1329
1381
  ### Quick local test — `docker-compose.local.yml`
1330
1382
 
1331
1383
  ```bash
1332
1384
  docker build -t kobo:local . # once
1333
1385
  docker compose -f docker-compose.local.yml up -d
1334
- docker compose -f docker-compose.local.yml logs kobo | grep Token
1335
1386
  ```
1336
1387
 
1337
- Open `http://localhost:3000/?token=<token from above>`.
1388
+ Open `http://localhost:3000/`; retrieve the token using [Token / login flow](#token--login-flow) if prompted.
1338
1389
 
1339
1390
  By default the file bind-mounts `./kobo-projects` (next to it) at `/projects` — put a real git repo
1340
1391
  in there (or set `KOBO_PROJECTS_DIR` to point elsewhere) before creating a workspace. When you get
@@ -1353,10 +1404,9 @@ and lose all Kōbō state).
1353
1404
  ```bash
1354
1405
  docker build -t kobo:local . # once, if not already built
1355
1406
  docker compose -f docker-compose.local-traefik.yml up -d
1356
- docker compose -f docker-compose.local-traefik.yml logs kobo | grep Token
1357
1407
  ```
1358
1408
 
1359
- Open `http://kobo.localhost/?token=<token from above>` — `.localhost` resolves to `127.0.0.1` on
1409
+ Open `http://kobo.localhost/` and sign in using [Token / login flow](#token--login-flow) — `.localhost` resolves to `127.0.0.1` on
1360
1410
  most OSes without touching `/etc/hosts`. This exercises the exact routing/auth/mount setup used by
1361
1411
  `docker-compose.example.yml`, minus a real domain and TLS certificate.
1362
1412
 
@@ -1386,17 +1436,16 @@ Before using it:
1386
1436
  2. Replace `kobo.example.com` with your real domain, and `you@example.com` with a real email (Let's Encrypt account).
1387
1437
  3. Populate a `.env` file next to the compose file with `GH_TOKEN` / `GITLAB_TOKEN` if you use the forge integration.
1388
1438
  4. Point every bind mount (`~/.ssh`, `~/.gitconfig`, `~/.claude.json`, `~/.claude`, `~/.codex`) at real paths on the host — see [Volumes](#volumes) for what each one is for, in particular the "`~/.claude.json` must already exist as a file" gotcha.
1389
- 5. Decide whether you need the Docker-socket passthrough (dev-server panel). If not, delete that volume line — see the DANGER note in [Volumes](#volumes).
1439
+ 5. Leave the Docker socket unmounted unless your project workflows require it; opting in grants host-level control. See [Volumes](#volumes).
1390
1440
  6. Network access + behind-a-reverse-proxy are already pre-enabled via the `KOBO_NETWORK_ACCESS_*` env vars in the file — no manual bootstrap step.
1391
1441
 
1392
- Bring it up, grab the token, bookmark the URL:
1442
+ Start it, then retrieve the token using the login flow above:
1393
1443
 
1394
1444
  ```bash
1395
1445
  docker compose -f docker-compose.example.yml up -d
1396
- docker compose -f docker-compose.example.yml logs kobo | grep Token
1397
1446
  ```
1398
1447
 
1399
- Open `https://kobo.example.com/?token=<token>`. SSH access: `ssh -p 2222 root@<host>` (key-based
1448
+ Open `https://kobo.example.com/` and sign in using [Token / login flow](#token--login-flow). SSH access: `ssh -p 2222 root@<host>` (key-based
1400
1449
  only). See [Ports](#ports) and [Volumes](#volumes) above for the full rationale on the Docker socket
1401
1450
  and SSH tradeoffs before deploying this to a real machine.
1402
1451
 
@@ -1455,9 +1504,8 @@ An old image alone is not a complete rollback after database migrations.
1455
1504
  — check `docker inspect <container> --format '{{json .State.Health}}'`. This should not happen on
1456
1505
  a current image (`/api/health` is exempt from the token gate, see [The image](#the-image)); if you
1457
1506
  see a 401 in the health log on a custom-built image, you're missing that exemption.
1458
- - **`docker compose logs kobo | grep Token` returns nothing** — network access wasn't enabled at
1459
- boot. Confirm `KOBO_NETWORK_ACCESS_ENABLED=true` is actually set in the `environment:` block for
1460
- the `kobo` service.
1507
+ - **The token is absent from startup logs** — this is intentional. Use the local
1508
+ [token retrieval command](#token--login-flow), then the browser login dialog.
1461
1509
 
1462
1510
  ## Agent runtimes
1463
1511
 
@@ -1473,7 +1521,7 @@ claude /login
1473
1521
 
1474
1522
  Kōbō talks to the embedded [`@anthropic-ai/claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-typescript), which reuses the same login. The `claude` CLI is **not** required at runtime; you only need it for `/login`, `mcp add`, and other one-off setup commands. As a fallback you can export `ANTHROPIC_API_KEY` instead.
1475
1523
 
1476
- The model selector is populated from [`src/shared/models.ts`](./src/shared/models.ts), also used by backend validation. This checkout includes Fable 5.1, Mythos 5, Opus 5, Opus 4.8, Sonnet 5 and older variants. Pick the default in **Settings → Agents**. Catalogue entries describe what Kōbō offers; provider access still depends on your account. `auto` leaves model selection to the runtime.
1524
+ The model selector is populated from [`src/shared/models.ts`](./src/shared/models.ts), also used by backend validation. This checkout includes Fable 5.1, Mythos 5.1/5, Opus 5.5, Sonnet 5.5, Haiku 5.5 (`claude-haiku-5-5`) and older variants. Pick the default in **Settings → Agents**. Catalogue entries describe what Kōbō offers; provider access still depends on your account. `auto` leaves model selection to the runtime.
1477
1525
 
1478
1526
  #### Immediate chat messages
1479
1527
 
package/SECURITY.md CHANGED
@@ -17,11 +17,37 @@ the account running Kōbō and the selected engine's permission mode. A Git work
17
17
  isolates a checkout, not processes, credentials or the filesystem. Only open
18
18
  projects and configure executable hooks/integrations you trust.
19
19
 
20
- The default listener is loopback. LAN access uses one shared token; it is not
21
- per-user authorization. Behind a reverse proxy, enable the corresponding setting
22
- so loopback API/WebSocket requests also require that token. Health checks are
23
- exempt. Host and Origin validation remain separate protections. Use HTTPS or a
24
- trusted private network; do not offer this installation as a shared public service.
20
+ The default listener is loopback. Network access uses an operator token; it is not
21
+ per-user authorization. Browsers exchange it for a 12-hour HttpOnly, SameSite=Strict
22
+ cookie (Secure behind HTTPS proxies), never persist it in localStorage, and obtain
23
+ 60-second single-use tickets for each WebSocket path. Rotation, disabling network
24
+ access, or changing proxy mode revokes sessions, pending tickets and active sockets,
25
+ including terminals. Restarting Kōbō also expires sessions. A settings change may
26
+ establish a new session for the browser that performed it using the returned token.
27
+
28
+ Behind a reverse proxy, enable the corresponding setting and declare the exact
29
+ public origin with `KOBO_NETWORK_ACCESS_PROXY_HOST` (bare domains default to HTTPS).
30
+ Health checks are exempt. Host and Origin validation remain separate protections.
31
+ Use HTTPS or a trusted private network; do not offer this installation as a shared
32
+ public service. Configure HSTS at the HTTPS terminator. The supplied public Compose
33
+ examples include it. Socket and request size/rate/concurrency limits are bounded
34
+ in-process; proxy limits should complement them. Client IPs come from OS sockets,
35
+ so callers behind the same proxy share limits.
36
+
37
+ External MCP requires a separately issued Bearer credential, including on loopback.
38
+ Each key has explicit tool and workspace grants, optional expiry, and independent
39
+ revocation; only its hash is stored. The UI defaults to read-only tools, selected
40
+ workspaces and 30-day expiry. Operator cookies/tokens cannot authenticate `/api/mcp`,
41
+ and an MCP key cannot authenticate operator APIs or terminal upgrades. Global stdio
42
+ forwards to that endpoint using `KOBO_MCP_TOKEN`; it does not read the database offline.
43
+
44
+ Managed workspace stdio uses a per-launch `KOBO_MCP_SESSION_TOKEN` through the local
45
+ `/api/internal-mcp` dispatcher. The backend owns authorization and database writes;
46
+ Plan and read-only review launches cannot invoke mutating managed tools even if the
47
+ provider approves a tool. Launch capabilities are revoked when stopping begins.
48
+ Operational progress and narrowly scoped review/handoff reports retain their own
49
+ capabilities. This tool boundary does not sandbox arbitrary code executed by an
50
+ agent or other integrations running as the same OS account.
25
51
 
26
52
  Git workflow preferences express which automatic actions are wanted. They are not
27
53
  a sandbox for arbitrary shell commands or external MCP tools. Claude and Codex
@@ -33,7 +59,8 @@ network token.
33
59
 
34
60
  KOBO_HOME stores conversations, workspaces, backups and settings. Direct integration
35
61
  commands and credentials live in `integrations.json`, written atomically with mode
36
- 0600 on Unix. They are plaintext local secrets: protect the host account and backups.
62
+ 0600 on Unix. Settings and their temporary files/backups are also private (0600),
63
+ and new Kōbō home directories use 0700. They are plaintext local secrets: protect the host account and backups.
37
64
  They are excluded from settings exports and are never returned by the connection
38
65
  status API. A replacement connection must be entered in full. MCP error text and
39
66
  stderr are withheld because third-party processes may echo secrets.