@yadsh/dsh-qa-surface 0.3.0 → 0.4.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 (245) hide show
  1. package/README.md +153 -12
  2. package/lib/account-remotes.js +100 -0
  3. package/lib/account-remotes.js.map +1 -0
  4. package/lib/accounts/credentials.js +27 -0
  5. package/lib/accounts/credentials.js.map +1 -0
  6. package/lib/accounts/errors.js +9 -0
  7. package/lib/accounts/errors.js.map +1 -0
  8. package/lib/accounts/file.js +101 -0
  9. package/lib/accounts/file.js.map +1 -0
  10. package/lib/accounts/store.js +144 -161
  11. package/lib/accounts/store.js.map +1 -1
  12. package/lib/accounts/token.js +61 -0
  13. package/lib/accounts/token.js.map +1 -0
  14. package/lib/attachment-rules.js +111 -0
  15. package/lib/attachment-rules.js.map +1 -0
  16. package/lib/cli.js +122 -9
  17. package/lib/cli.js.map +1 -1
  18. package/lib/client/QaAccountsController.js +32 -2
  19. package/lib/client/QaAccountsController.js.map +1 -1
  20. package/lib/client/QaSessionController.js +47 -65
  21. package/lib/client/QaSessionController.js.map +1 -1
  22. package/lib/client/QaSurface.js +121 -107
  23. package/lib/client/QaSurface.js.map +1 -1
  24. package/lib/client/QaTranscriptAdapter.js +71 -6
  25. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  26. package/lib/client/attachments.js +150 -0
  27. package/lib/client/attachments.js.map +1 -0
  28. package/lib/client/attestation.js +1 -1
  29. package/lib/client/attestation.js.map +1 -1
  30. package/lib/client/base64.js +16 -0
  31. package/lib/client/base64.js.map +1 -0
  32. package/lib/client/chat-files.js +48 -0
  33. package/lib/client/chat-files.js.map +1 -0
  34. package/lib/client/components/Markdown.js +1 -1
  35. package/lib/client/components/Markdown.js.map +1 -1
  36. package/lib/client/components/QaChangelog.js +38 -19
  37. package/lib/client/components/QaChangelog.js.map +1 -1
  38. package/lib/client/components/QaComposer.js +56 -74
  39. package/lib/client/components/QaComposer.js.map +1 -1
  40. package/lib/client/components/QaFileAttachment.js +19 -0
  41. package/lib/client/components/QaFileAttachment.js.map +1 -0
  42. package/lib/client/components/QaFilesPanel.js +45 -0
  43. package/lib/client/components/QaFilesPanel.js.map +1 -0
  44. package/lib/client/components/QaHeader.js +34 -0
  45. package/lib/client/components/QaHeader.js.map +1 -0
  46. package/lib/client/components/QaMessage.js +17 -4
  47. package/lib/client/components/QaMessage.js.map +1 -1
  48. package/lib/client/components/QaModal.js +31 -0
  49. package/lib/client/components/QaModal.js.map +1 -0
  50. package/lib/client/components/QaProfile.js +64 -0
  51. package/lib/client/components/QaProfile.js.map +1 -0
  52. package/lib/client/components/QaRightRail.js +15 -0
  53. package/lib/client/components/QaRightRail.js.map +1 -0
  54. package/lib/client/components/QaSidebar.js +6 -1
  55. package/lib/client/components/QaSidebar.js.map +1 -1
  56. package/lib/client/components/{QaSourcesDrawer.js → QaSourcesPanel.js} +3 -3
  57. package/lib/client/components/QaSourcesPanel.js.map +1 -0
  58. package/lib/client/components/QaTurnRail.js +15 -0
  59. package/lib/client/components/QaTurnRail.js.map +1 -1
  60. package/lib/client/components/QaWidthHandle.js +25 -20
  61. package/lib/client/components/QaWidthHandle.js.map +1 -1
  62. package/lib/client/components/QaWorkGroup.js +19 -20
  63. package/lib/client/components/QaWorkGroup.js.map +1 -1
  64. package/lib/client/components/surface-utils.js +28 -0
  65. package/lib/client/components/surface-utils.js.map +1 -0
  66. package/lib/client/components/thinking-phrases.js +24 -0
  67. package/lib/client/components/thinking-phrases.js.map +1 -0
  68. package/lib/client/create-session.js +7 -32
  69. package/lib/client/create-session.js.map +1 -1
  70. package/lib/client/failure-copy.js +63 -0
  71. package/lib/client/failure-copy.js.map +1 -0
  72. package/lib/client/index.js +38 -7
  73. package/lib/client/index.js.map +1 -1
  74. package/lib/client/prompt-content.js +54 -0
  75. package/lib/client/prompt-content.js.map +1 -0
  76. package/lib/client/settings/card.js +143 -0
  77. package/lib/client/settings/card.js.map +1 -0
  78. package/lib/client/settings/fields.js +145 -0
  79. package/lib/client/settings/fields.js.map +1 -0
  80. package/lib/client/settings/format.js +255 -0
  81. package/lib/client/settings/format.js.map +1 -0
  82. package/lib/client/settings/sections/access.js +25 -0
  83. package/lib/client/settings/sections/access.js.map +1 -0
  84. package/lib/client/settings/sections/accounts.js +47 -0
  85. package/lib/client/settings/sections/accounts.js.map +1 -0
  86. package/lib/client/settings/sections/attachments.js +46 -0
  87. package/lib/client/settings/sections/attachments.js.map +1 -0
  88. package/lib/client/settings/sections/branding.js +28 -0
  89. package/lib/client/settings/sections/branding.js.map +1 -0
  90. package/lib/client/settings/sections/common.js +20 -0
  91. package/lib/client/settings/sections/common.js.map +1 -0
  92. package/lib/client/settings/sections/embedding.js +19 -0
  93. package/lib/client/settings/sections/embedding.js.map +1 -0
  94. package/lib/client/settings/sections/interface.js +60 -0
  95. package/lib/client/settings/sections/interface.js.map +1 -0
  96. package/lib/client/settings/sections/lockdown.js +82 -0
  97. package/lib/client/settings/sections/lockdown.js.map +1 -0
  98. package/lib/client/settings/sections/session.js +53 -0
  99. package/lib/client/settings/sections/session.js.map +1 -0
  100. package/lib/client/settings/sections/sources.js +72 -0
  101. package/lib/client/settings/sections/sources.js.map +1 -0
  102. package/lib/client/settings/sections/status.js +37 -0
  103. package/lib/client/settings/sections/status.js.map +1 -0
  104. package/lib/client/settings/sections.js +23 -0
  105. package/lib/client/settings/sections.js.map +1 -0
  106. package/lib/client/settings/styles.js +57 -0
  107. package/lib/client/settings/styles.js.map +1 -0
  108. package/lib/client/stream-publisher.js +47 -0
  109. package/lib/client/stream-publisher.js.map +1 -0
  110. package/lib/client/styles.js +85 -19
  111. package/lib/client/styles.js.map +1 -1
  112. package/lib/client/types.js.map +1 -1
  113. package/lib/client/use-right-rail.js +96 -0
  114. package/lib/client/use-right-rail.js.map +1 -0
  115. package/lib/client/use-session-ui-state.js +26 -19
  116. package/lib/client/use-session-ui-state.js.map +1 -1
  117. package/lib/client.js +4788 -869
  118. package/lib/client.js.map +1 -1
  119. package/lib/config-resolvers/accounts.js +60 -0
  120. package/lib/config-resolvers/accounts.js.map +1 -0
  121. package/lib/config-resolvers/attachments.js +28 -0
  122. package/lib/config-resolvers/attachments.js.map +1 -0
  123. package/lib/config-resolvers/basics.js +59 -0
  124. package/lib/config-resolvers/basics.js.map +1 -0
  125. package/lib/config-resolvers/defaults.js +132 -0
  126. package/lib/config-resolvers/defaults.js.map +1 -0
  127. package/lib/config-resolvers/lockdown.js +79 -0
  128. package/lib/config-resolvers/lockdown.js.map +1 -0
  129. package/lib/config-resolvers/session.js +42 -0
  130. package/lib/config-resolvers/session.js.map +1 -0
  131. package/lib/config-resolvers/shared.js +42 -0
  132. package/lib/config-resolvers/shared.js.map +1 -0
  133. package/lib/config-resolvers/sources.js +81 -0
  134. package/lib/config-resolvers/sources.js.map +1 -0
  135. package/lib/config-resolvers/ui.js +20 -0
  136. package/lib/config-resolvers/ui.js.map +1 -0
  137. package/lib/config.js +65 -4
  138. package/lib/config.js.map +1 -1
  139. package/lib/entry-redirect.js +2 -1
  140. package/lib/entry-redirect.js.map +1 -1
  141. package/lib/index.js +123 -74
  142. package/lib/index.js.map +1 -1
  143. package/lib/profile.js +214 -0
  144. package/lib/profile.js.map +1 -0
  145. package/lib/prompt-notes.js +231 -0
  146. package/lib/prompt-notes.js.map +1 -0
  147. package/lib/resolve-config.js +35 -410
  148. package/lib/resolve-config.js.map +1 -1
  149. package/lib/secure-session.js +82 -16
  150. package/lib/secure-session.js.map +1 -1
  151. package/lib/shared/session-key.js +18 -0
  152. package/lib/shared/session-key.js.map +1 -0
  153. package/lib/shared/settings.js +10 -0
  154. package/lib/shared/settings.js.map +1 -0
  155. package/lib/thinking-phrases.js +68 -0
  156. package/lib/thinking-phrases.js.map +1 -0
  157. package/lib/typert.host.js +162 -17
  158. package/lib/typert.remote-client.d.ts +5 -1
  159. package/lib/typert.remote-client.js +132 -13
  160. package/lib/types/account-remotes.d.ts +42 -0
  161. package/lib/types/accounts/credentials.d.ts +14 -0
  162. package/lib/types/accounts/errors.d.ts +11 -0
  163. package/lib/types/accounts/file.d.ts +82 -0
  164. package/lib/types/accounts/store.d.ts +66 -43
  165. package/lib/types/accounts/token.d.ts +31 -0
  166. package/lib/types/attachment-rules.d.ts +44 -0
  167. package/lib/types/cli.d.ts +2 -2
  168. package/lib/types/client/QaAccountsController.d.ts +8 -1
  169. package/lib/types/client/QaSessionController.d.ts +16 -16
  170. package/lib/types/client/QaSurface.d.ts +7 -1
  171. package/lib/types/client/attachments.d.ts +54 -0
  172. package/lib/types/client/base64.d.ts +10 -0
  173. package/lib/types/client/chat-files.d.ts +30 -0
  174. package/lib/types/client/components/QaChangelog.d.ts +1 -1
  175. package/lib/types/client/components/QaComposer.d.ts +8 -4
  176. package/lib/types/client/components/QaFileAttachment.d.ts +14 -0
  177. package/lib/types/client/components/QaFilesPanel.d.ts +15 -0
  178. package/lib/types/client/components/QaHeader.d.ts +41 -0
  179. package/lib/types/client/components/QaMessage.d.ts +3 -1
  180. package/lib/types/client/components/QaModal.d.ts +26 -0
  181. package/lib/types/client/components/QaProfile.d.ts +25 -0
  182. package/lib/types/client/components/QaRightRail.d.ts +23 -0
  183. package/lib/types/client/components/QaSidebar.d.ts +14 -0
  184. package/lib/types/client/components/{QaSourcesDrawer.d.ts → QaSourcesPanel.d.ts} +7 -4
  185. package/lib/types/client/components/QaTurnRail.d.ts +6 -0
  186. package/lib/types/client/components/QaWidthHandle.d.ts +14 -7
  187. package/lib/types/client/components/QaWorkGroup.d.ts +5 -3
  188. package/lib/types/client/components/surface-utils.d.ts +11 -0
  189. package/lib/types/client/components/thinking-phrases.d.ts +9 -0
  190. package/lib/types/client/create-session.d.ts +6 -9
  191. package/lib/types/client/failure-copy.d.ts +19 -0
  192. package/lib/types/client/prompt-content.d.ts +30 -0
  193. package/lib/types/client/settings/card.d.ts +23 -0
  194. package/lib/types/client/settings/fields.d.ts +139 -0
  195. package/lib/types/client/settings/format.d.ts +108 -0
  196. package/lib/types/client/settings/sections/access.d.ts +9 -0
  197. package/lib/types/client/settings/sections/accounts.d.ts +9 -0
  198. package/lib/types/client/settings/sections/attachments.d.ts +15 -0
  199. package/lib/types/client/settings/sections/branding.d.ts +8 -0
  200. package/lib/types/client/settings/sections/common.d.ts +28 -0
  201. package/lib/types/client/settings/sections/embedding.d.ts +8 -0
  202. package/lib/types/client/settings/sections/interface.d.ts +9 -0
  203. package/lib/types/client/settings/sections/lockdown.d.ts +9 -0
  204. package/lib/types/client/settings/sections/session.d.ts +9 -0
  205. package/lib/types/client/settings/sections/sources.d.ts +9 -0
  206. package/lib/types/client/settings/sections/status.d.ts +22 -0
  207. package/lib/types/client/settings/sections.d.ts +25 -0
  208. package/lib/types/client/settings/styles.d.ts +8 -0
  209. package/lib/types/client/stream-publisher.d.ts +28 -0
  210. package/lib/types/client/types.d.ts +29 -1
  211. package/lib/types/client/use-right-rail.d.ts +56 -0
  212. package/lib/types/client/use-session-ui-state.d.ts +13 -7
  213. package/lib/types/config-resolvers/accounts.d.ts +15 -0
  214. package/lib/types/config-resolvers/attachments.d.ts +6 -0
  215. package/lib/types/config-resolvers/basics.d.ts +8 -0
  216. package/lib/types/config-resolvers/defaults.d.ts +4 -0
  217. package/lib/types/config-resolvers/lockdown.d.ts +10 -0
  218. package/lib/types/config-resolvers/session.d.ts +6 -0
  219. package/lib/types/config-resolvers/shared.d.ts +16 -0
  220. package/lib/types/config-resolvers/sources.d.ts +6 -0
  221. package/lib/types/config-resolvers/ui.d.ts +6 -0
  222. package/lib/types/index.d.ts +20 -13
  223. package/lib/types/profile.d.ts +76 -0
  224. package/lib/types/prompt-notes.d.ts +110 -0
  225. package/lib/types/resolve-config.d.ts +8 -3
  226. package/lib/types/secure-session.d.ts +26 -1
  227. package/lib/types/shared/session-key.d.ts +23 -0
  228. package/lib/types/shared/settings.d.ts +10 -0
  229. package/lib/types/thinking-phrases.d.ts +17 -0
  230. package/lib/types/types.d.ts +123 -5
  231. package/lib/types/user-workspace.d.ts +38 -0
  232. package/lib/user-workspace.js +231 -0
  233. package/lib/user-workspace.js.map +1 -0
  234. package/package.json +14 -7
  235. package/CHANGELOG.md +0 -176
  236. package/SPEC-dsh-qa-surface.md +0 -2425
  237. package/docs/ARCHITECTURE.md +0 -66
  238. package/docs/COMPATIBILITY.md +0 -14
  239. package/docs/CONFIGURATION.md +0 -199
  240. package/docs/EMBEDDING.md +0 -13
  241. package/docs/SOURCES-MIGRATION.md +0 -28
  242. package/docs/SOURCES-PROVENANCE-AUDIT.md +0 -88
  243. package/docs/SPEC-dsh-qa-surface-accounts.md +0 -219
  244. package/docs/SPEC-dsh-qa-surface-sources-provenance.md +0 -1692
  245. package/lib/client/components/QaSourcesDrawer.js.map +0 -1
package/README.md CHANGED
@@ -16,7 +16,8 @@ Session and Agent Loop.
16
16
  - optionally shows a minimal per-browser chat-history sidebar
17
17
  (`ui.showSessionList`) whose switching re-runs policy attestation;
18
18
  - blocks unsupported approvals/questions instead of auto-approving them;
19
- - pins locked sessions to `read-only` + `approval=never` before Send is enabled;
19
+ - pins locked sessions to the configured `read-only` or isolated
20
+ `workspace-write` policy plus `approval=never` before Send is enabled;
20
21
  - applies a Host-side tool allow-list plus a monotonic execution guard;
21
22
  - optionally gates the surface behind email + password accounts
22
23
  (`accounts.enabled`) with server-side session ownership, a first-login
@@ -26,6 +27,10 @@ Session and Agent Loop.
26
27
  - optionally redirects non-loopback hostnames from the harness root into the
27
28
  QA route (`entry.redirectNonLoopback`), keeping the operator's localhost
28
29
  harness UI untouched;
30
+ - ships an operator settings card (Settings → Plugins → plugin configuration →
31
+ «Помощник QA») that edits the `qa-surface` namespace in place — route,
32
+ branding, session, interface, lockdown, accounts, sources, embedding — and
33
+ reports the configuration the running Host resolved;
29
34
  - uses the existing same-origin DSH connection and trust boundary.
30
35
 
31
36
  It does not add another HTTP server, provider proxy, permissive CORS rule, or
@@ -64,6 +69,17 @@ Configuration is registered under the Host settings namespace `qa-surface`.
64
69
  Composition values form the base layer; normal DSH user settings can override
65
70
  them when the deployment provides writable settings.
66
71
 
72
+ The browser half carries a settings card for that namespace: **Settings →
73
+ Plugins → plugin configuration → «Помощник QA»**. It writes the user layer of
74
+ `qa-surface` — so every change is revertible through the card's own reset — and
75
+ shows the configuration the running Host resolved next to it. Combinations the
76
+ Host refuses are either written together in one mutation (a provider with its
77
+ model, per-user workspaces with the `workspace-write` sandbox) or disabled with
78
+ the reason stated. The card renders only where the settings namespace is
79
+ readable, which the DSH gateway pins to loopback; a browser served over the LAN
80
+ reads the same configuration read-only through `qaSurface/describe` on the QA
81
+ page itself.
82
+
67
83
  ```yaml
68
84
  config:
69
85
  enabled: true
@@ -96,16 +112,35 @@ config:
96
112
  showToolActivity: false
97
113
  showReasoning: false
98
114
  renderMarkdown: true
99
- maxContentWidth: 900
115
+ minContentWidth: 650
100
116
  showSessionList: false
101
117
  accounts:
102
118
  enabled: false
103
119
  allowRegistration: true
104
120
  sessionTtlDays: 30
105
121
  showOtherUsersChats: false
122
+ perUserWorkspace: false
123
+ profile:
124
+ enabled: true
125
+ inject: true
126
+ identities: [] # e.g. [{ key: jira, label: Jira }]
127
+ instructionsMaxLength: 2000
106
128
  suggestedQuestions:
107
129
  - Как запросить доступ?
108
130
  - Где лежит инструкция?
131
+ # Phrases the running indicator cycles through (optional). An empty list
132
+ # restores the built-in ones.
133
+ thinkingPhrases:
134
+ - Уточняю по регламенту…
135
+ - Сверяюсь с инструкцией…
136
+ # What a visitor may attach (optional). Files are staged on the Host and
137
+ # read from the stored copy, so keep `read` in lockdown.toolPolicy.allow.
138
+ attachments:
139
+ textFiles: true
140
+ pastedTextLines: 200
141
+ maxFileBytes: 10485760
142
+ maxPending: 8
143
+ extensions: [md, txt, log, json, yaml, csv, sql]
109
144
  interaction:
110
145
  approvals: blocked
111
146
  questions: unsupported
@@ -166,6 +201,44 @@ uses the Host's normal default working directory. Put the system prompt, tools,
166
201
  skills, knowledge connections and permission policy in `agentPreset`, not in
167
202
  this UI plugin.
168
203
 
204
+ ### Writable per-user research space
205
+
206
+ Set `accounts.perUserWorkspace: true` only together with accounts, a registered
207
+ `session.workspaceId`, `lockdown.enforceFixedWorkspace: true`, and a
208
+ `workspace-write` + `never` permission preset:
209
+
210
+ ```yaml
211
+ session:
212
+ workspaceId: "<registered-workspace-uuid>"
213
+ accounts:
214
+ enabled: true
215
+ perUserWorkspace: true
216
+ lockdown:
217
+ enabled: true
218
+ enforceFixedWorkspace: true
219
+ sandboxMode: workspace-write
220
+ approvalPolicy: never
221
+ permissionPreset: qa-workspace-write
222
+ toolPolicy:
223
+ mode: allow-list
224
+ allow: [read, read_image, glob, grep, write, edit, web_search, web_fetch]
225
+ ```
226
+
227
+ The Host resolves that Workspace record's path and creates
228
+ `<workspace>/.qa-users/<account UUID>` with private Unix directory mode. It
229
+ passes the child as session `cwd` but deliberately does not register or attach
230
+ it as another DSH Workspace. Chats therefore remain ordinary entries in the
231
+ global DSH session list rather than creating one Workspace row per account.
232
+
233
+ The boundary combines DSH `workspace-write` with a Host tool guard for both
234
+ read and write paths, canonicalizes existing ancestors to reject symlink
235
+ escapes, propagates the root to subagent sessions, rejects shell/process/LSP
236
+ and `dsh_git_*` escape hatches, limits one model-controlled write to 10 MiB,
237
+ and limits an account directory to 256 MiB. `web_fetch` plus `write` is the
238
+ intended bounded research-download path; there is no unrestricted URL-to-disk
239
+ or shell downloader. Account directories are persistent scratch space and are
240
+ not deleted automatically.
241
+
169
242
  Model override is opt-in: `provider` and `model` must be set together. Slash
170
243
  commands are rejected as plain QA input. Reasoning and tool details remain
171
244
  hidden by default. Enabling `ui.showReasoning` and `ui.showToolActivity` adds a
@@ -174,6 +247,42 @@ then collapses to `Worked for ...` before the final answer. Tool capability is
174
247
  still controlled exclusively by `lockdown.toolPolicy.allow`; the display flags
175
248
  do not grant tools.
176
249
 
250
+ ### User profile
251
+
252
+ Clicking the account name in the sidebar footer opens the signed-in user's
253
+ profile: full name, one handle per external system the deployment declares,
254
+ and free-form instructions about how they want answers. The Host hands both
255
+ to the QA agent as a note in the conversation, so "покажи мои задачи" resolves
256
+ to a tracker lookup with the right login instead of a question. Because a QA
257
+ preset can declare its persona the complete system prompt (the shipped
258
+ `qa-research` one does, which discards every plugin prompt section), the note
259
+ travels as injected context on the conversation instead of as prompt text. It
260
+ is written once per profile, and subagents of the chat get their own copy.
261
+ Values are self-declared and the note says so: the agent names the identifier
262
+ it searched by and asks when the results contradict the request. The feature
263
+ needs no switch beyond accounts, though `accounts.profile.enabled` and
264
+ `inject` exist for deployments that want the form without the note, or
265
+ neither:
266
+
267
+ ```yaml
268
+ accounts:
269
+ enabled: true
270
+ profile:
271
+ identities:
272
+ - key: jira
273
+ label: Jira
274
+ - key: gitlab
275
+ label: GitLab
276
+ instructionsMaxLength: 2000
277
+ ```
278
+
279
+ `qa-accounts profile <email>` fills the same fields from an operator shell
280
+ (`--identity jira=i.ivanov`, `--instructions-file`, `--clear-identity`), which
281
+ is how a fresh deployment gets everyone's handles in place before users log
282
+ in. See
283
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md)
284
+ for the limits and the prompt's exact wording.
285
+
177
286
  Session policies:
178
287
 
179
288
  - `browser-persistent` restores the id stored in this browser, replacing stale
@@ -200,8 +309,9 @@ Sources are structured Host-owned provenance, independent of
200
309
  `ui.showToolActivity`. Successful reads/fetches, bounded web-search evidence,
201
310
  Jira/Confluence/knowledge results, and inherited subagent sources are
202
311
  normalized and deduplicated into one turn bundle. That exact bundle feeds the
203
- answer footer and grouped drawer and is persisted as `qa/sources`, so reload
204
- does not rerun tools. Search-only discovery stays hidden by default.
312
+ answer footer and the right rail's sources tab and is persisted as
313
+ `qa/sources`, so reload does not rerun tools. Search-only discovery stays
314
+ hidden by default.
205
315
 
206
316
  Local file cards open a source-scoped, read-only preview after Host-side real
207
317
  path validation against the attested session root. Markdown opens rendered by
@@ -213,8 +323,8 @@ Observable local subagents are inherited recursively. The internal
213
323
  `qa_report_sources` tool covers opaque delegated providers and is admitted as
214
324
  a provenance-only capability even when it is not listed among ordinary QA
215
325
  tools. A provider that neither exposes events nor reports sources marks the
216
- turn provenance incomplete. The QA prompt tells models not to append a manual
217
- `Sources`/`Источники` bibliography.
326
+ turn provenance incomplete. A note in the conversation tells the model not to
327
+ append a manual `Sources`/`Источники` bibliography.
218
328
 
219
329
  Images: the composer accepts PNG/JPEG/WebP/GIF via drag & drop onto the
220
330
  composer, paste, and the picker button, several at once (soft client caps:
@@ -223,6 +333,30 @@ promotes to durable attachments, so they survive reloads; sent images
223
333
  render as clickable thumbnails on the message. Whether the model can see
224
334
  them depends on the deployment's model (vision).
225
335
 
336
+ Attachments: the same picker, drop zone and paste path also take text files
337
+ (`md`, `txt`, `log`, and the other extensions in `attachments.extensions`), and
338
+ pasted text longer than `attachments.pastedTextLines` (default 200) becomes an
339
+ attachment named after its line count instead of filling the input field. A
340
+ file is staged on the Host through the browser upload service and the prompt
341
+ cites the returned receipt, so the durable copy survives reloads; the transcript
342
+ shows it as an extension badge, its name and its size. Unlike an image, a file
343
+ reaches the model as the path of that stored copy rather than as content, so
344
+ `lockdown.toolPolicy.allow` has to keep `read` for an attachment to be usable.
345
+ `attachments.textFiles: false` restricts the composer to images again, while
346
+ `maxFileBytes` and `maxPending` cap one file and the combined number of images
347
+ plus files per message.
348
+
349
+ The right rail is the chat's side panel, mirroring the Harness right Sidebar's
350
+ pattern: a tab strip is the panel's whole top edge, and the strip's close
351
+ control collapses the column. The sources tab carries the grouped list and
352
+ preview the sources drawer used to render — a message footnote opens it pinned
353
+ to that answer's subset, and «Все источники» returns to the whole chat. The
354
+ «Файлы» tab (header button with a live count) lists every attachment this chat
355
+ sent, grouped per message newest first, with the same file handles the
356
+ transcript shows and image thumbnails resolved from the session's asset
357
+ repository; each group jumps back to its message. Below 600px the rail goes
358
+ full-bleed. The agents drawer keeps its own header drawer for now.
359
+
226
360
  Subagents: the deployment may opt the delegation family (`subagent`,
227
361
  `subagent_fork`, `send_message`, `list_agents`, `interrupt_agent`) into the
228
362
  lockdown allow-list; the preset must mount them. Launches then render as
@@ -259,7 +393,9 @@ scope, and local in-contour model processing. Explicit acknowledgement is
259
393
  stored as a versioned browser-local flag; changing the disclosure version
260
394
  shows it again. Other DSH routes retain the stock onboarding entry.
261
395
 
262
- Locked mode requires a deployment permission preset named `qa-read-only`.
396
+ The default locked mode requires a deployment permission preset named
397
+ `qa-read-only`. Per-user writable mode uses a separate preset such as
398
+ `qa-workspace-write`.
263
399
  Extend the existing `@deepseek-ai/dsh-permission-presets` row without changing
264
400
  its process-wide default:
265
401
 
@@ -282,9 +418,14 @@ its process-wide default:
282
418
  approval: never
283
419
  name: QA Read Only
284
420
  description: No filesystem mutations and no permission escalation.
421
+ qa-workspace-write:
422
+ sandbox: workspace-write
423
+ approval: never
424
+ name: QA User Workspace
425
+ description: Writes only inside the attested per-user workspace.
285
426
  ```
286
427
 
287
- The shipped tool allow-list is empty. Add only reviewed read-only tool names
428
+ The shipped tool allow-list is empty. Add only reviewed tool names
288
429
  from the actual deployment. A name that is not registered fails closed. The
289
430
  Host restriction retains exact allow-listed tools from the agent preset's
290
431
  ancestor scope, and the additional execution guard also denies session-scoped
@@ -342,11 +483,11 @@ What a LAN deployment does not change:
342
483
  version in that browser's local storage. The stock DSH notice remains
343
484
  unchanged on operator routes.
344
485
 
345
- See [Configuration](docs/CONFIGURATION.md) for the config-channel details.
486
+ See [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md) for the config-channel details.
346
487
 
347
- See [Architecture](docs/ARCHITECTURE.md),
348
- [Configuration](docs/CONFIGURATION.md), and
349
- [Compatibility](docs/COMPATIBILITY.md).
488
+ See [Architecture](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/ARCHITECTURE.md),
489
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md), and
490
+ [Compatibility](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/COMPATIBILITY.md).
350
491
 
351
492
  ## Develop
352
493
 
@@ -0,0 +1,100 @@
1
+ import { QaAccounts, QaAccountsError, defaultAccountsFilePath, } from "./accounts/store.js";
2
+ const ACCOUNTS_DISABLED_ERROR = "QA accounts are not enabled on this deployment.";
3
+ /** Build the account-remotes context for one `QaSurface` service instance. */
4
+ export function createQaAccountRemotes(options) {
5
+ const { getConfig, logger } = options;
6
+ // Memoized across remotes: rebuilt only when the account-affecting options
7
+ // change; the file is shared across rebuilds.
8
+ let accounts;
9
+ let accountsOptions;
10
+ const resolve = (config) => {
11
+ if (!config.accounts.enabled)
12
+ return undefined;
13
+ const memoKey = JSON.stringify([
14
+ config.accounts.sessionTtlDays,
15
+ config.accounts.allowRegistration,
16
+ config.accounts.profile.instructionsMaxLength,
17
+ config.accounts.profile.identities,
18
+ ]);
19
+ if (accounts === undefined || accountsOptions !== memoKey) {
20
+ accounts = new QaAccounts(defaultAccountsFilePath(), {
21
+ sessionTtlDays: config.accounts.sessionTtlDays,
22
+ allowRegistration: config.accounts.allowRegistration,
23
+ instructionsMaxLength: config.accounts.profile.instructionsMaxLength,
24
+ identityFields: config.accounts.profile.identities,
25
+ });
26
+ accountsOptions = memoKey;
27
+ }
28
+ return accounts;
29
+ };
30
+ /** The accounts store, or the disabled refusal the browser maps to copy. */
31
+ const requireAccounts = () => {
32
+ const store = resolve(getConfig());
33
+ if (store === undefined) {
34
+ throw new Error(ACCOUNTS_DISABLED_ERROR);
35
+ }
36
+ return store;
37
+ };
38
+ const run = (operation, sessionIdForLog) => {
39
+ try {
40
+ return operation();
41
+ }
42
+ catch (error) {
43
+ if (error instanceof QaAccountsError) {
44
+ logger.warn("accounts.rejected", {
45
+ reason: error.reason,
46
+ sessionId: sessionIdForLog,
47
+ });
48
+ throw new Error(`QA accounts refused the request (reason: ${error.reason})`, {
49
+ cause: error,
50
+ });
51
+ }
52
+ throw error;
53
+ }
54
+ };
55
+ return {
56
+ resolve,
57
+ run,
58
+ register: (email, password) => {
59
+ const store = requireAccounts();
60
+ return run(() => store.register(email, password));
61
+ },
62
+ login: (email, password) => {
63
+ const store = requireAccounts();
64
+ return run(() => store.login(email, password));
65
+ },
66
+ whoami: (token) => {
67
+ if (!getConfig().accounts.enabled)
68
+ return { authenticated: false };
69
+ const store = requireAccounts();
70
+ return run(() => store.whoami(token));
71
+ },
72
+ claimSessions: (token, sessionIds) => {
73
+ const store = requireAccounts();
74
+ return run(() => store.claimSessions(token, sessionIds));
75
+ },
76
+ ownedSessions: (token) => {
77
+ if (!getConfig().accounts.enabled)
78
+ return { ids: [] };
79
+ const store = requireAccounts();
80
+ return run(() => ({ ids: store.ownedSessionIds(token) }));
81
+ },
82
+ listOwnership: (token) => {
83
+ if (!getConfig().accounts.enabled)
84
+ return { entries: [] };
85
+ const store = requireAccounts();
86
+ return run(() => ({ entries: store.listOwnership(token) }));
87
+ },
88
+ updateProfile: (token, input) => {
89
+ const config = getConfig();
90
+ const store = requireAccounts();
91
+ return run(() => {
92
+ if (!config.accounts.profile.enabled) {
93
+ throw new QaAccountsError("profile-disabled", "self-service profiles are disabled on this deployment");
94
+ }
95
+ return store.updateOwnProfile(token, input);
96
+ });
97
+ },
98
+ };
99
+ }
100
+ //# sourceMappingURL=account-remotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-remotes.js","sourceRoot":"","sources":["../src/account-remotes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { QaAccountsError } from "./errors.js";
2
+ const MIN_PASSWORD_LENGTH = 8;
3
+ const MAX_PASSWORD_LENGTH = 200;
4
+ const MAX_DISPLAY_NAME_LENGTH = 100;
5
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
6
+ /**
7
+ * The email/password/display-name gate shared by self-registration and the
8
+ * operator's addUser, so both paths reject unusable input with the same
9
+ * errors and messages, in the same order. Returns the normalized address and
10
+ * trimmed display name; the caller still decides uniqueness and role.
11
+ */
12
+ export function validateCredentials(email, password, displayName) {
13
+ const normalized = email.trim().toLowerCase();
14
+ if (!EMAIL_PATTERN.test(normalized) || normalized.length > 254) {
15
+ throw new QaAccountsError("invalid-email", "email is not a usable address");
16
+ }
17
+ if (password.length < MIN_PASSWORD_LENGTH ||
18
+ password.length > MAX_PASSWORD_LENGTH) {
19
+ throw new QaAccountsError("weak-password", `password must be ${MIN_PASSWORD_LENGTH} to ${MAX_PASSWORD_LENGTH} characters`);
20
+ }
21
+ const name = (displayName ?? "").trim();
22
+ if (name.length > MAX_DISPLAY_NAME_LENGTH) {
23
+ throw new QaAccountsError("invalid-display-name", `display name must be at most ${MAX_DISPLAY_NAME_LENGTH} characters`);
24
+ }
25
+ return { email: normalized, displayName: name };
26
+ }
27
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/accounts/credentials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAC9B,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,aAAa,GAAG,6BAA6B,CAAC;AASpD;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAa,EACb,QAAgB,EAChB,WAA+B;IAE/B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC/D,MAAM,IAAI,eAAe,CAAC,eAAe,EAAE,+BAA+B,CAAC,CAAC;IAC9E,CAAC;IACD,IACE,QAAQ,CAAC,MAAM,GAAG,mBAAmB;QACrC,QAAQ,CAAC,MAAM,GAAG,mBAAmB,EACrC,CAAC;QACD,MAAM,IAAI,eAAe,CACvB,eAAe,EACf,oBAAoB,mBAAmB,OAAO,mBAAmB,aAAa,CAC/E,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACxC,IAAI,IAAI,CAAC,MAAM,GAAG,uBAAuB,EAAE,CAAC;QAC1C,MAAM,IAAI,eAAe,CACvB,sBAAsB,EACtB,gCAAgC,uBAAuB,aAAa,CACrE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AAClD,CAAC"}
@@ -0,0 +1,9 @@
1
+ export class QaAccountsError extends Error {
2
+ reason;
3
+ constructor(reason, message) {
4
+ super(message);
5
+ this.reason = reason;
6
+ this.name = "QaAccountsError";
7
+ }
8
+ }
9
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/accounts/errors.ts"],"names":[],"mappings":"AAqBA,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAE7B,MAAM;IADjB,YACW,MAA6B,EACtC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;sBAHN,MAAM;QAIf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,101 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
3
+ import path from "node:path";
4
+ import { base64Url } from "./token.js";
5
+ /** A stamp no real file can match, so the first load always reads the disk. */
6
+ export function createFileStampRef() {
7
+ return { current: { mtimeMs: Number.NEGATIVE_INFINITY, size: -1 } };
8
+ }
9
+ export function stampOf(stat) {
10
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
11
+ }
12
+ const SECRET_BYTES = 32;
13
+ /** The accounts file lives next to the DSH home the launcher exports. */
14
+ export function defaultAccountsFilePath() {
15
+ const home = process.env.DSH_HOME?.trim();
16
+ return path.join(home !== undefined && home !== "" ? home : process.cwd(), "qa-accounts.json");
17
+ }
18
+ /**
19
+ * Read the accounts file, or create it on first run with a fresh token
20
+ * secret. The stamp ref is left pointing at the version just read.
21
+ */
22
+ export function loadAccountsFile(filePath, stamp) {
23
+ let raw;
24
+ let stat;
25
+ try {
26
+ raw = readFileSync(filePath, "utf8");
27
+ stat = statSync(filePath);
28
+ }
29
+ catch (error) {
30
+ // Only a missing file means "first run": anything else (permissions,
31
+ // a directory in the way, I/O failure) must surface, or the store
32
+ // would silently reset every account it cannot read.
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ const created = {
36
+ version: 1,
37
+ secret: base64Url(randomBytes(SECRET_BYTES)),
38
+ users: [],
39
+ ownership: {},
40
+ };
41
+ persistAccountsFile(filePath, created, stamp);
42
+ return created;
43
+ }
44
+ const parsed = JSON.parse(raw);
45
+ if (typeof parsed !== "object" ||
46
+ parsed === null ||
47
+ parsed.version !== 1 ||
48
+ typeof parsed.secret !== "string" ||
49
+ !Array.isArray(parsed.users)) {
50
+ throw new Error(`qa-accounts: ${filePath} is not a recognizable accounts file; refusing to overwrite it`);
51
+ }
52
+ const file = parsed;
53
+ if (stat !== undefined)
54
+ stamp.current = stampOf(stat);
55
+ return {
56
+ version: 1,
57
+ secret: file.secret,
58
+ users: file.users,
59
+ ownership: file.ownership ?? {},
60
+ };
61
+ }
62
+ /**
63
+ * Persist the accounts file atomically (temp file + rename) and leave the
64
+ * stamp ref pointing at the freshly written version.
65
+ */
66
+ export function persistAccountsFile(filePath, file, stamp) {
67
+ mkdirSync(path.dirname(filePath), { recursive: true });
68
+ // A per-process temp name: the Host and the CLI never race on one file.
69
+ const temp = `${filePath}.${process.pid}.tmp`;
70
+ writeFileSync(temp, `${JSON.stringify(file, null, 2)}\n`, "utf8");
71
+ renameSync(temp, filePath);
72
+ stamp.current = stampOf(statSync(filePath));
73
+ }
74
+ /**
75
+ * The qa-accounts CLI runs in its own process against the same file. Re-read
76
+ * it whenever it changed on disk, before every read or read-modify-write:
77
+ * without this a running Host would keep honoring tokens the CLI revoked
78
+ * and its next save would resurrect the CLI's change from a stale snapshot.
79
+ * One file, one writer at a time remains the deployment's discipline; LAN
80
+ * scale keeps the stat per operation trivial.
81
+ *
82
+ * Returns the freshly loaded file, or undefined when the caller should keep
83
+ * serving its in-memory state.
84
+ */
85
+ export function reloadAccountsFileIfChanged(filePath, stamp) {
86
+ let stat;
87
+ try {
88
+ stat = statSync(filePath);
89
+ }
90
+ catch {
91
+ // The file vanished between operations (never by our hand): keep
92
+ // serving the in-memory state instead of resetting every account.
93
+ return undefined;
94
+ }
95
+ if (stat.mtimeMs === stamp.current.mtimeMs &&
96
+ stat.size === stamp.current.size) {
97
+ return undefined;
98
+ }
99
+ return loadAccountsFile(filePath, stamp);
100
+ }
101
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/accounts/file.ts"],"names":[],"mappings":"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"}