@yadsh/dsh-qa-surface 0.3.0 → 0.5.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 (276) hide show
  1. package/README.md +192 -15
  2. package/cordis.patch.yml +8 -0
  3. package/lib/account-remotes.js +100 -0
  4. package/lib/account-remotes.js.map +1 -0
  5. package/lib/accounts/credentials.js +35 -0
  6. package/lib/accounts/credentials.js.map +1 -0
  7. package/lib/accounts/errors.js +9 -0
  8. package/lib/accounts/errors.js.map +1 -0
  9. package/lib/accounts/file.js +101 -0
  10. package/lib/accounts/file.js.map +1 -0
  11. package/lib/accounts/store.js +159 -161
  12. package/lib/accounts/store.js.map +1 -1
  13. package/lib/accounts/token.js +61 -0
  14. package/lib/accounts/token.js.map +1 -0
  15. package/lib/approvals.js +179 -0
  16. package/lib/approvals.js.map +1 -0
  17. package/lib/attachment-rules.js +111 -0
  18. package/lib/attachment-rules.js.map +1 -0
  19. package/lib/cli.js +132 -9
  20. package/lib/cli.js.map +1 -1
  21. package/lib/client/QaAccountsController.js +32 -2
  22. package/lib/client/QaAccountsController.js.map +1 -1
  23. package/lib/client/QaSessionController.js +140 -65
  24. package/lib/client/QaSessionController.js.map +1 -1
  25. package/lib/client/QaSurface.js +134 -107
  26. package/lib/client/QaSurface.js.map +1 -1
  27. package/lib/client/QaTranscriptAdapter.js +71 -6
  28. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  29. package/lib/client/approvals.js +104 -0
  30. package/lib/client/approvals.js.map +1 -0
  31. package/lib/client/attachments.js +150 -0
  32. package/lib/client/attachments.js.map +1 -0
  33. package/lib/client/attestation.js +4 -1
  34. package/lib/client/attestation.js.map +1 -1
  35. package/lib/client/base64.js +16 -0
  36. package/lib/client/base64.js.map +1 -0
  37. package/lib/client/chat-files.js +48 -0
  38. package/lib/client/chat-files.js.map +1 -0
  39. package/lib/client/components/Markdown.js +1 -1
  40. package/lib/client/components/Markdown.js.map +1 -1
  41. package/lib/client/components/QaApproval.js +23 -0
  42. package/lib/client/components/QaApproval.js.map +1 -0
  43. package/lib/client/components/QaChangelog.js +62 -19
  44. package/lib/client/components/QaChangelog.js.map +1 -1
  45. package/lib/client/components/QaComposer.js +56 -74
  46. package/lib/client/components/QaComposer.js.map +1 -1
  47. package/lib/client/components/QaFileAttachment.js +19 -0
  48. package/lib/client/components/QaFileAttachment.js.map +1 -0
  49. package/lib/client/components/QaFilesPanel.js +45 -0
  50. package/lib/client/components/QaFilesPanel.js.map +1 -0
  51. package/lib/client/components/QaHeader.js +34 -0
  52. package/lib/client/components/QaHeader.js.map +1 -0
  53. package/lib/client/components/QaMessage.js +17 -4
  54. package/lib/client/components/QaMessage.js.map +1 -1
  55. package/lib/client/components/QaModal.js +31 -0
  56. package/lib/client/components/QaModal.js.map +1 -0
  57. package/lib/client/components/QaProfile.js +64 -0
  58. package/lib/client/components/QaProfile.js.map +1 -0
  59. package/lib/client/components/QaQuestions.js +96 -0
  60. package/lib/client/components/QaQuestions.js.map +1 -0
  61. package/lib/client/components/QaRightRail.js +15 -0
  62. package/lib/client/components/QaRightRail.js.map +1 -0
  63. package/lib/client/components/QaSidebar.js +6 -1
  64. package/lib/client/components/QaSidebar.js.map +1 -1
  65. package/lib/client/components/{QaSourcesDrawer.js → QaSourcesPanel.js} +3 -3
  66. package/lib/client/components/QaSourcesPanel.js.map +1 -0
  67. package/lib/client/components/QaTurnRail.js +15 -0
  68. package/lib/client/components/QaTurnRail.js.map +1 -1
  69. package/lib/client/components/QaWidthHandle.js +25 -20
  70. package/lib/client/components/QaWidthHandle.js.map +1 -1
  71. package/lib/client/components/QaWorkGroup.js +19 -20
  72. package/lib/client/components/QaWorkGroup.js.map +1 -1
  73. package/lib/client/components/surface-utils.js +28 -0
  74. package/lib/client/components/surface-utils.js.map +1 -0
  75. package/lib/client/components/thinking-phrases.js +24 -0
  76. package/lib/client/components/thinking-phrases.js.map +1 -0
  77. package/lib/client/create-session.js +7 -32
  78. package/lib/client/create-session.js.map +1 -1
  79. package/lib/client/failure-copy.js +63 -0
  80. package/lib/client/failure-copy.js.map +1 -0
  81. package/lib/client/index.js +49 -7
  82. package/lib/client/index.js.map +1 -1
  83. package/lib/client/project-session-state.js +5 -3
  84. package/lib/client/project-session-state.js.map +1 -1
  85. package/lib/client/prompt-content.js +54 -0
  86. package/lib/client/prompt-content.js.map +1 -0
  87. package/lib/client/questions.js +114 -0
  88. package/lib/client/questions.js.map +1 -0
  89. package/lib/client/settings/card.js +143 -0
  90. package/lib/client/settings/card.js.map +1 -0
  91. package/lib/client/settings/fields.js +145 -0
  92. package/lib/client/settings/fields.js.map +1 -0
  93. package/lib/client/settings/format.js +255 -0
  94. package/lib/client/settings/format.js.map +1 -0
  95. package/lib/client/settings/sections/access.js +25 -0
  96. package/lib/client/settings/sections/access.js.map +1 -0
  97. package/lib/client/settings/sections/accounts.js +47 -0
  98. package/lib/client/settings/sections/accounts.js.map +1 -0
  99. package/lib/client/settings/sections/attachments.js +46 -0
  100. package/lib/client/settings/sections/attachments.js.map +1 -0
  101. package/lib/client/settings/sections/branding.js +28 -0
  102. package/lib/client/settings/sections/branding.js.map +1 -0
  103. package/lib/client/settings/sections/common.js +20 -0
  104. package/lib/client/settings/sections/common.js.map +1 -0
  105. package/lib/client/settings/sections/embedding.js +19 -0
  106. package/lib/client/settings/sections/embedding.js.map +1 -0
  107. package/lib/client/settings/sections/interface.js +60 -0
  108. package/lib/client/settings/sections/interface.js.map +1 -0
  109. package/lib/client/settings/sections/lockdown.js +82 -0
  110. package/lib/client/settings/sections/lockdown.js.map +1 -0
  111. package/lib/client/settings/sections/session.js +53 -0
  112. package/lib/client/settings/sections/session.js.map +1 -0
  113. package/lib/client/settings/sections/sources.js +74 -0
  114. package/lib/client/settings/sections/sources.js.map +1 -0
  115. package/lib/client/settings/sections/status.js +37 -0
  116. package/lib/client/settings/sections/status.js.map +1 -0
  117. package/lib/client/settings/sections.js +23 -0
  118. package/lib/client/settings/sections.js.map +1 -0
  119. package/lib/client/settings/styles.js +57 -0
  120. package/lib/client/settings/styles.js.map +1 -0
  121. package/lib/client/stream-publisher.js +47 -0
  122. package/lib/client/stream-publisher.js.map +1 -0
  123. package/lib/client/styles.js +127 -19
  124. package/lib/client/styles.js.map +1 -1
  125. package/lib/client/types.js +2 -0
  126. package/lib/client/types.js.map +1 -1
  127. package/lib/client/use-right-rail.js +96 -0
  128. package/lib/client/use-right-rail.js.map +1 -0
  129. package/lib/client/use-session-ui-state.js +26 -19
  130. package/lib/client/use-session-ui-state.js.map +1 -1
  131. package/lib/client.js +5694 -813
  132. package/lib/client.js.map +1 -1
  133. package/lib/config-resolvers/accounts.js +60 -0
  134. package/lib/config-resolvers/accounts.js.map +1 -0
  135. package/lib/config-resolvers/attachments.js +28 -0
  136. package/lib/config-resolvers/attachments.js.map +1 -0
  137. package/lib/config-resolvers/basics.js +61 -0
  138. package/lib/config-resolvers/basics.js.map +1 -0
  139. package/lib/config-resolvers/defaults.js +136 -0
  140. package/lib/config-resolvers/defaults.js.map +1 -0
  141. package/lib/config-resolvers/lockdown.js +90 -0
  142. package/lib/config-resolvers/lockdown.js.map +1 -0
  143. package/lib/config-resolvers/session.js +42 -0
  144. package/lib/config-resolvers/session.js.map +1 -0
  145. package/lib/config-resolvers/shared.js +51 -0
  146. package/lib/config-resolvers/shared.js.map +1 -0
  147. package/lib/config-resolvers/sources.js +83 -0
  148. package/lib/config-resolvers/sources.js.map +1 -0
  149. package/lib/config-resolvers/ui.js +20 -0
  150. package/lib/config-resolvers/ui.js.map +1 -0
  151. package/lib/config.js +76 -6
  152. package/lib/config.js.map +1 -1
  153. package/lib/entry-redirect.js +2 -1
  154. package/lib/entry-redirect.js.map +1 -1
  155. package/lib/index.js +198 -79
  156. package/lib/index.js.map +1 -1
  157. package/lib/profile.js +214 -0
  158. package/lib/profile.js.map +1 -0
  159. package/lib/prompt-notes.js +231 -0
  160. package/lib/prompt-notes.js.map +1 -0
  161. package/lib/provenance/host-store.js +25 -9
  162. package/lib/provenance/host-store.js.map +1 -1
  163. package/lib/provenance/reported.js +37 -12
  164. package/lib/provenance/reported.js.map +1 -1
  165. package/lib/questions.js +185 -0
  166. package/lib/questions.js.map +1 -0
  167. package/lib/resolve-config.js +35 -410
  168. package/lib/resolve-config.js.map +1 -1
  169. package/lib/secure-session.js +139 -21
  170. package/lib/secure-session.js.map +1 -1
  171. package/lib/session-ownership.js +55 -0
  172. package/lib/session-ownership.js.map +1 -0
  173. package/lib/shared/session-key.js +18 -0
  174. package/lib/shared/session-key.js.map +1 -0
  175. package/lib/shared/settings.js +10 -0
  176. package/lib/shared/settings.js.map +1 -0
  177. package/lib/thinking-phrases.js +68 -0
  178. package/lib/thinking-phrases.js.map +1 -0
  179. package/lib/typert.host.js +506 -21
  180. package/lib/typert.remote-client.d.ts +15 -1
  181. package/lib/typert.remote-client.js +407 -15
  182. package/lib/types/account-remotes.d.ts +42 -0
  183. package/lib/types/accounts/credentials.d.ts +20 -0
  184. package/lib/types/accounts/errors.d.ts +11 -0
  185. package/lib/types/accounts/file.d.ts +82 -0
  186. package/lib/types/accounts/store.d.ts +74 -43
  187. package/lib/types/accounts/token.d.ts +31 -0
  188. package/lib/types/approvals.d.ts +65 -0
  189. package/lib/types/attachment-rules.d.ts +44 -0
  190. package/lib/types/cli.d.ts +2 -2
  191. package/lib/types/client/QaAccountsController.d.ts +8 -1
  192. package/lib/types/client/QaSessionController.d.ts +53 -16
  193. package/lib/types/client/QaSurface.d.ts +14 -1
  194. package/lib/types/client/approvals.d.ts +39 -0
  195. package/lib/types/client/attachments.d.ts +54 -0
  196. package/lib/types/client/base64.d.ts +10 -0
  197. package/lib/types/client/chat-files.d.ts +30 -0
  198. package/lib/types/client/components/QaApproval.d.ts +13 -0
  199. package/lib/types/client/components/QaChangelog.d.ts +1 -1
  200. package/lib/types/client/components/QaComposer.d.ts +8 -4
  201. package/lib/types/client/components/QaFileAttachment.d.ts +14 -0
  202. package/lib/types/client/components/QaFilesPanel.d.ts +15 -0
  203. package/lib/types/client/components/QaHeader.d.ts +41 -0
  204. package/lib/types/client/components/QaMessage.d.ts +3 -1
  205. package/lib/types/client/components/QaModal.d.ts +26 -0
  206. package/lib/types/client/components/QaProfile.d.ts +25 -0
  207. package/lib/types/client/components/QaQuestions.d.ts +13 -0
  208. package/lib/types/client/components/QaRightRail.d.ts +23 -0
  209. package/lib/types/client/components/QaSidebar.d.ts +14 -0
  210. package/lib/types/client/components/{QaSourcesDrawer.d.ts → QaSourcesPanel.d.ts} +7 -4
  211. package/lib/types/client/components/QaTurnRail.d.ts +6 -0
  212. package/lib/types/client/components/QaWidthHandle.d.ts +14 -7
  213. package/lib/types/client/components/QaWorkGroup.d.ts +5 -3
  214. package/lib/types/client/components/surface-utils.d.ts +11 -0
  215. package/lib/types/client/components/thinking-phrases.d.ts +9 -0
  216. package/lib/types/client/create-session.d.ts +6 -9
  217. package/lib/types/client/failure-copy.d.ts +19 -0
  218. package/lib/types/client/project-session-state.d.ts +8 -1
  219. package/lib/types/client/prompt-content.d.ts +30 -0
  220. package/lib/types/client/questions.d.ts +45 -0
  221. package/lib/types/client/settings/card.d.ts +23 -0
  222. package/lib/types/client/settings/fields.d.ts +139 -0
  223. package/lib/types/client/settings/format.d.ts +108 -0
  224. package/lib/types/client/settings/sections/access.d.ts +9 -0
  225. package/lib/types/client/settings/sections/accounts.d.ts +9 -0
  226. package/lib/types/client/settings/sections/attachments.d.ts +15 -0
  227. package/lib/types/client/settings/sections/branding.d.ts +8 -0
  228. package/lib/types/client/settings/sections/common.d.ts +28 -0
  229. package/lib/types/client/settings/sections/embedding.d.ts +8 -0
  230. package/lib/types/client/settings/sections/interface.d.ts +9 -0
  231. package/lib/types/client/settings/sections/lockdown.d.ts +9 -0
  232. package/lib/types/client/settings/sections/session.d.ts +9 -0
  233. package/lib/types/client/settings/sections/sources.d.ts +9 -0
  234. package/lib/types/client/settings/sections/status.d.ts +22 -0
  235. package/lib/types/client/settings/sections.d.ts +25 -0
  236. package/lib/types/client/settings/styles.d.ts +8 -0
  237. package/lib/types/client/stream-publisher.d.ts +28 -0
  238. package/lib/types/client/types.d.ts +76 -1
  239. package/lib/types/client/use-right-rail.d.ts +56 -0
  240. package/lib/types/client/use-session-ui-state.d.ts +13 -7
  241. package/lib/types/config-resolvers/accounts.d.ts +15 -0
  242. package/lib/types/config-resolvers/attachments.d.ts +6 -0
  243. package/lib/types/config-resolvers/basics.d.ts +8 -0
  244. package/lib/types/config-resolvers/defaults.d.ts +4 -0
  245. package/lib/types/config-resolvers/lockdown.d.ts +10 -0
  246. package/lib/types/config-resolvers/session.d.ts +6 -0
  247. package/lib/types/config-resolvers/shared.d.ts +23 -0
  248. package/lib/types/config-resolvers/sources.d.ts +6 -0
  249. package/lib/types/config-resolvers/ui.d.ts +6 -0
  250. package/lib/types/index.d.ts +48 -15
  251. package/lib/types/profile.d.ts +76 -0
  252. package/lib/types/prompt-notes.d.ts +110 -0
  253. package/lib/types/provenance/reported.d.ts +9 -1
  254. package/lib/types/questions.d.ts +83 -0
  255. package/lib/types/resolve-config.d.ts +8 -3
  256. package/lib/types/secure-session.d.ts +46 -2
  257. package/lib/types/session-ownership.d.ts +30 -0
  258. package/lib/types/shared/session-key.d.ts +23 -0
  259. package/lib/types/shared/settings.d.ts +10 -0
  260. package/lib/types/thinking-phrases.d.ts +17 -0
  261. package/lib/types/types.d.ts +203 -9
  262. package/lib/types/user-workspace.d.ts +42 -0
  263. package/lib/user-workspace.js +246 -0
  264. package/lib/user-workspace.js.map +1 -0
  265. package/package.json +15 -8
  266. package/CHANGELOG.md +0 -176
  267. package/SPEC-dsh-qa-surface.md +0 -2425
  268. package/docs/ARCHITECTURE.md +0 -66
  269. package/docs/COMPATIBILITY.md +0 -14
  270. package/docs/CONFIGURATION.md +0 -199
  271. package/docs/EMBEDDING.md +0 -13
  272. package/docs/SOURCES-MIGRATION.md +0 -28
  273. package/docs/SOURCES-PROVENANCE-AUDIT.md +0 -88
  274. package/docs/SPEC-dsh-qa-surface-accounts.md +0 -219
  275. package/docs/SPEC-dsh-qa-surface-sources-provenance.md +0 -1692
  276. package/lib/client/components/QaSourcesDrawer.js.map +0 -1
package/README.md CHANGED
@@ -15,17 +15,25 @@ Session and Agent Loop.
15
15
  a responsive first-party-style conversation layout;
16
16
  - optionally shows a minimal per-browser chat-history sidebar
17
17
  (`ui.showSessionList`) whose switching re-runs policy attestation;
18
- - blocks unsupported approvals/questions instead of auto-approving them;
19
- - pins locked sessions to `read-only` + `approval=never` before Send is enabled;
18
+ - blocks unsupported approvals/questions instead of auto-approving them, and can
19
+ park them for the operator to answer (`interaction.approvals: interactive`,
20
+ `interaction.questions: interactive`) instead of refusing or stalling on a
21
+ card the QA view cannot show;
22
+ - pins locked sessions to the configured `read-only` or isolated
23
+ `workspace-write` policy plus `approval=never` before Send is enabled;
20
24
  - applies a Host-side tool allow-list plus a monotonic execution guard;
21
25
  - optionally gates the surface behind email + password accounts
22
26
  (`accounts.enabled`) with server-side session ownership, a first-login
23
27
  migration of the browser's existing chats, a `qa-accounts` management CLI
24
- (list/add/set-role/disable/revoke), and a coarse honest boundary:
28
+ (list/add/set-password/set-role/disable/revoke), and a coarse honest boundary:
25
29
  accounts identify QA users, they do not fence the harness root;
26
30
  - optionally redirects non-loopback hostnames from the harness root into the
27
31
  QA route (`entry.redirectNonLoopback`), keeping the operator's localhost
28
32
  harness UI untouched;
33
+ - ships an operator settings card (Settings → Plugins → plugin configuration →
34
+ «Помощник QA») that edits the `qa-surface` namespace in place — route,
35
+ branding, session, interface, lockdown, accounts, sources, embedding — and
36
+ reports the configuration the running Host resolved;
29
37
  - uses the existing same-origin DSH connection and trust boundary.
30
38
 
31
39
  It does not add another HTTP server, provider proxy, permissive CORS rule, or
@@ -64,6 +72,17 @@ Configuration is registered under the Host settings namespace `qa-surface`.
64
72
  Composition values form the base layer; normal DSH user settings can override
65
73
  them when the deployment provides writable settings.
66
74
 
75
+ The browser half carries a settings card for that namespace: **Settings →
76
+ Plugins → plugin configuration → «Помощник QA»**. It writes the user layer of
77
+ `qa-surface` — so every change is revertible through the card's own reset — and
78
+ shows the configuration the running Host resolved next to it. Combinations the
79
+ Host refuses are either written together in one mutation (a provider with its
80
+ model, per-user workspaces with the `workspace-write` sandbox) or disabled with
81
+ the reason stated. The card renders only where the settings namespace is
82
+ readable, which the DSH gateway pins to loopback; a browser served over the LAN
83
+ reads the same configuration read-only through `qaSurface/describe` on the QA
84
+ page itself.
85
+
67
86
  ```yaml
68
87
  config:
69
88
  enabled: true
@@ -96,18 +115,41 @@ config:
96
115
  showToolActivity: false
97
116
  showReasoning: false
98
117
  renderMarkdown: true
99
- maxContentWidth: 900
118
+ minContentWidth: 650
100
119
  showSessionList: false
101
120
  accounts:
102
121
  enabled: false
103
122
  allowRegistration: true
104
123
  sessionTtlDays: 30
105
124
  showOtherUsersChats: false
125
+ perUserWorkspace: false
126
+ profile:
127
+ enabled: true
128
+ inject: true
129
+ identities: [] # e.g. [{ key: jira, label: Jira }]
130
+ instructionsMaxLength: 2000
106
131
  suggestedQuestions:
107
132
  - Как запросить доступ?
108
133
  - Где лежит инструкция?
134
+ # Phrases the running indicator cycles through (optional). An empty list
135
+ # restores the built-in ones.
136
+ thinkingPhrases:
137
+ - Уточняю по регламенту…
138
+ - Сверяюсь с инструкцией…
139
+ # What a visitor may attach (optional). Files are staged on the Host and
140
+ # read from the stored copy, so keep `read` in lockdown.toolPolicy.allow.
141
+ attachments:
142
+ textFiles: true
143
+ pastedTextLines: 200
144
+ maxFileBytes: 10485760
145
+ maxPending: 8
146
+ extensions: [md, txt, log, json, yaml, csv, sql]
109
147
  interaction:
148
+ # blocked: refuse a composed gate's `ask` with the QA reason.
149
+ # interactive: park it over the composer for the operator to answer.
110
150
  approvals: blocked
151
+ # unsupported: refuse ask_user_question with an actionable reason.
152
+ # interactive: park the request over the composer as an answerable form.
111
153
  questions: unsupported
112
154
  lockdown:
113
155
  enabled: true
@@ -157,6 +199,7 @@ config:
157
199
  inheritSources: true
158
200
  enableReportToolFallback: true
159
201
  markIncompleteOpaqueRuns: true
202
+ validateReportedSources: true
160
203
  legacy:
161
204
  parseAssistantSourcesBlock: false
162
205
  ```
@@ -166,6 +209,68 @@ uses the Host's normal default working directory. Put the system prompt, tools,
166
209
  skills, knowledge connections and permission policy in `agentPreset`, not in
167
210
  this UI plugin.
168
211
 
212
+ ### Writable per-user research space
213
+
214
+ Set `accounts.perUserWorkspace: true` only together with accounts, a registered
215
+ `session.workspaceId`, `lockdown.enforceFixedWorkspace: true`, and a
216
+ `workspace-write` + `never` permission preset:
217
+
218
+ ```yaml
219
+ session:
220
+ workspaceId: "<registered-workspace-uuid>"
221
+ accounts:
222
+ enabled: true
223
+ perUserWorkspace: true
224
+ lockdown:
225
+ enabled: true
226
+ enforceFixedWorkspace: true
227
+ sandboxMode: workspace-write
228
+ approvalPolicy: never
229
+ permissionPreset: qa-workspace-write
230
+ toolPolicy:
231
+ mode: allow-list
232
+ allow:
233
+ [
234
+ read,
235
+ read_image,
236
+ glob,
237
+ grep,
238
+ write,
239
+ edit,
240
+ web_search,
241
+ web_fetch,
242
+ dsh_git_context,
243
+ dsh_git_history,
244
+ dsh_git_show,
245
+ dsh_git_blame,
246
+ ]
247
+ sharedReadOnlyRoots:
248
+ - E:/qa-assistant/workspaces/docs
249
+ - E:/qa-assistant/workspaces/code
250
+ ```
251
+
252
+ The Host resolves that Workspace record's path and creates
253
+ `<workspace>/.qa-users/<account UUID>` with private Unix directory mode. It
254
+ passes the child as session `cwd` but deliberately does not register or attach
255
+ it as another DSH Workspace. Chats therefore remain ordinary entries in the
256
+ global DSH session list rather than creating one Workspace row per account.
257
+
258
+ The boundary combines DSH `workspace-write` with a Host tool guard for both
259
+ read and write paths, canonicalizes existing ancestors to reject symlink
260
+ escapes, propagates the root to subagent sessions, rejects shell/process/LSP
261
+ escape hatches, permits filesystem reads in explicitly configured shared
262
+ read-only roots, and leaves repository selection to the separately configured
263
+ read-only Git plugin. Writes remain confined to the account directory. The
264
+ guard limits one model-controlled write to 10 MiB,
265
+ and limits an account directory to 256 MiB. `web_fetch` plus `write` is the
266
+ intended bounded research-download path; there is no unrestricted URL-to-disk
267
+ or shell downloader. Account directories are persistent scratch space and are
268
+ not deleted automatically.
269
+
270
+ For the layout above, configure `@yadsh/dsh-git-readonly` separately with
271
+ `repositoryRoots: ["E:/qa-assistant/workspaces/code"]`. That plugin is the
272
+ single repository-selection authority and exposes no mutating Git operation.
273
+
169
274
  Model override is opt-in: `provider` and `model` must be set together. Slash
170
275
  commands are rejected as plain QA input. Reasoning and tool details remain
171
276
  hidden by default. Enabling `ui.showReasoning` and `ui.showToolActivity` adds a
@@ -174,6 +279,42 @@ then collapses to `Worked for ...` before the final answer. Tool capability is
174
279
  still controlled exclusively by `lockdown.toolPolicy.allow`; the display flags
175
280
  do not grant tools.
176
281
 
282
+ ### User profile
283
+
284
+ Clicking the account name in the sidebar footer opens the signed-in user's
285
+ profile: full name, one handle per external system the deployment declares,
286
+ and free-form instructions about how they want answers. The Host hands both
287
+ to the QA agent as a note in the conversation, so "покажи мои задачи" resolves
288
+ to a tracker lookup with the right login instead of a question. Because a QA
289
+ preset can declare its persona the complete system prompt (the shipped
290
+ `qa-research` one does, which discards every plugin prompt section), the note
291
+ travels as injected context on the conversation instead of as prompt text. It
292
+ is written once per profile, and subagents of the chat get their own copy.
293
+ Values are self-declared and the note says so: the agent names the identifier
294
+ it searched by and asks when the results contradict the request. The feature
295
+ needs no switch beyond accounts, though `accounts.profile.enabled` and
296
+ `inject` exist for deployments that want the form without the note, or
297
+ neither:
298
+
299
+ ```yaml
300
+ accounts:
301
+ enabled: true
302
+ profile:
303
+ identities:
304
+ - key: jira
305
+ label: Jira
306
+ - key: gitlab
307
+ label: GitLab
308
+ instructionsMaxLength: 2000
309
+ ```
310
+
311
+ `qa-accounts profile <email>` fills the same fields from an operator shell
312
+ (`--identity jira=i.ivanov`, `--instructions-file`, `--clear-identity`), which
313
+ is how a fresh deployment gets everyone's handles in place before users log
314
+ in. See
315
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md)
316
+ for the limits and the prompt's exact wording.
317
+
177
318
  Session policies:
178
319
 
179
320
  - `browser-persistent` restores the id stored in this browser, replacing stale
@@ -200,8 +341,9 @@ Sources are structured Host-owned provenance, independent of
200
341
  `ui.showToolActivity`. Successful reads/fetches, bounded web-search evidence,
201
342
  Jira/Confluence/knowledge results, and inherited subagent sources are
202
343
  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.
344
+ answer footer and the right rail's sources tab and is persisted as
345
+ `qa/sources`, so reload does not rerun tools. Search-only discovery stays
346
+ hidden by default.
205
347
 
206
348
  Local file cards open a source-scoped, read-only preview after Host-side real
207
349
  path validation against the attested session root. Markdown opens rendered by
@@ -213,8 +355,8 @@ Observable local subagents are inherited recursively. The internal
213
355
  `qa_report_sources` tool covers opaque delegated providers and is admitted as
214
356
  a provenance-only capability even when it is not listed among ordinary QA
215
357
  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.
358
+ turn provenance incomplete. A note in the conversation tells the model not to
359
+ append a manual `Sources`/`Источники` bibliography.
218
360
 
219
361
  Images: the composer accepts PNG/JPEG/WebP/GIF via drag & drop onto the
220
362
  composer, paste, and the picker button, several at once (soft client caps:
@@ -223,6 +365,30 @@ promotes to durable attachments, so they survive reloads; sent images
223
365
  render as clickable thumbnails on the message. Whether the model can see
224
366
  them depends on the deployment's model (vision).
225
367
 
368
+ Attachments: the same picker, drop zone and paste path also take text files
369
+ (`md`, `txt`, `log`, and the other extensions in `attachments.extensions`), and
370
+ pasted text longer than `attachments.pastedTextLines` (default 200) becomes an
371
+ attachment named after its line count instead of filling the input field. A
372
+ file is staged on the Host through the browser upload service and the prompt
373
+ cites the returned receipt, so the durable copy survives reloads; the transcript
374
+ shows it as an extension badge, its name and its size. Unlike an image, a file
375
+ reaches the model as the path of that stored copy rather than as content, so
376
+ `lockdown.toolPolicy.allow` has to keep `read` for an attachment to be usable.
377
+ `attachments.textFiles: false` restricts the composer to images again, while
378
+ `maxFileBytes` and `maxPending` cap one file and the combined number of images
379
+ plus files per message.
380
+
381
+ The right rail is the chat's side panel, mirroring the Harness right Sidebar's
382
+ pattern: a tab strip is the panel's whole top edge, and the strip's close
383
+ control collapses the column. The sources tab carries the grouped list and
384
+ preview the sources drawer used to render — a message footnote opens it pinned
385
+ to that answer's subset, and «Все источники» returns to the whole chat. The
386
+ «Файлы» tab (header button with a live count) lists every attachment this chat
387
+ sent, grouped per message newest first, with the same file handles the
388
+ transcript shows and image thumbnails resolved from the session's asset
389
+ repository; each group jumps back to its message. Below 600px the rail goes
390
+ full-bleed. The agents drawer keeps its own header drawer for now.
391
+
226
392
  Subagents: the deployment may opt the delegation family (`subagent`,
227
393
  `subagent_fork`, `send_message`, `list_agents`, `interrupt_agent`) into the
228
394
  lockdown allow-list; the preset must mount them. Launches then render as
@@ -240,7 +406,11 @@ used first) and intersects it with the Host session list, so users sharing the
240
406
  deployment never see each other's chats. An admin can explicitly enable
241
407
  `accounts.showOtherUsersChats: true` to add chats owned by other QA accounts,
242
408
  grouped by owner. Switching re-runs the full policy
243
- attestation; a chat the Host no longer lists is pruned from the index. Each
409
+ attestation, and that attestation materializes the chat's agent when the Host
410
+ does not hold one: DSH builds an agent on demand, so a chat restored after a
411
+ Host restart still opens (its composition is resumed from what the session
412
+ recorded) instead of failing as unavailable. A chat the Host no longer lists is
413
+ pruned from the index. Each
244
414
  row carries a two-click delete control that removes the chat from this
245
415
  browser's index; deleting the chat that is currently open continues in a
246
416
  fresh attested session. Host-side sessions are not deleted — DSH 0.1.x
@@ -259,7 +429,9 @@ scope, and local in-contour model processing. Explicit acknowledgement is
259
429
  stored as a versioned browser-local flag; changing the disclosure version
260
430
  shows it again. Other DSH routes retain the stock onboarding entry.
261
431
 
262
- Locked mode requires a deployment permission preset named `qa-read-only`.
432
+ The default locked mode requires a deployment permission preset named
433
+ `qa-read-only`. Per-user writable mode uses a separate preset such as
434
+ `qa-workspace-write`.
263
435
  Extend the existing `@deepseek-ai/dsh-permission-presets` row without changing
264
436
  its process-wide default:
265
437
 
@@ -282,9 +454,14 @@ its process-wide default:
282
454
  approval: never
283
455
  name: QA Read Only
284
456
  description: No filesystem mutations and no permission escalation.
457
+ qa-workspace-write:
458
+ sandbox: workspace-write
459
+ approval: never
460
+ name: QA User Workspace
461
+ description: Writes only inside the attested per-user workspace.
285
462
  ```
286
463
 
287
- The shipped tool allow-list is empty. Add only reviewed read-only tool names
464
+ The shipped tool allow-list is empty. Add only reviewed tool names
288
465
  from the actual deployment. A name that is not registered fails closed. The
289
466
  Host restriction retains exact allow-listed tools from the agent preset's
290
467
  ancestor scope, and the additional execution guard also denies session-scoped
@@ -342,11 +519,11 @@ What a LAN deployment does not change:
342
519
  version in that browser's local storage. The stock DSH notice remains
343
520
  unchanged on operator routes.
344
521
 
345
- See [Configuration](docs/CONFIGURATION.md) for the config-channel details.
522
+ See [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md) for the config-channel details.
346
523
 
347
- See [Architecture](docs/ARCHITECTURE.md),
348
- [Configuration](docs/CONFIGURATION.md), and
349
- [Compatibility](docs/COMPATIBILITY.md).
524
+ See [Architecture](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/ARCHITECTURE.md),
525
+ [Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md), and
526
+ [Compatibility](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/COMPATIBILITY.md).
350
527
 
351
528
  ## Develop
352
529
 
package/cordis.patch.yml CHANGED
@@ -9,6 +9,14 @@
9
9
  matchChildren: true
10
10
  ui:
11
11
  showReset: false
12
+ # Fail closed: a composed tool gate's `ask` is refused, not answered.
13
+ # A deployment that wants the operator to answer it opts in with
14
+ # `interaction: { approvals: interactive }`.
15
+ interaction:
16
+ approvals: blocked
17
+ # Ask_user_question is refused, not answered (the tool is mounted per
18
+ # preset; opt in with `interaction: { questions: interactive }`).
19
+ questions: unsupported
12
20
  lockdown:
13
21
  enabled: true
14
22
  sandboxMode: read-only
@@ -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,35 @@
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 length gate every path that sets a password goes through — registration,
8
+ * the operator's addUser, and a reset — so a password good enough to register is
9
+ * exactly the one an operator can put back.
10
+ */
11
+ export function validatePassword(password) {
12
+ if (password.length < MIN_PASSWORD_LENGTH ||
13
+ password.length > MAX_PASSWORD_LENGTH) {
14
+ throw new QaAccountsError("weak-password", `password must be ${MIN_PASSWORD_LENGTH} to ${MAX_PASSWORD_LENGTH} characters`);
15
+ }
16
+ }
17
+ /**
18
+ * The email/password/display-name gate shared by self-registration and the
19
+ * operator's addUser, so both paths reject unusable input with the same
20
+ * errors and messages, in the same order. Returns the normalized address and
21
+ * trimmed display name; the caller still decides uniqueness and role.
22
+ */
23
+ export function validateCredentials(email, password, displayName) {
24
+ const normalized = email.trim().toLowerCase();
25
+ if (!EMAIL_PATTERN.test(normalized) || normalized.length > 254) {
26
+ throw new QaAccountsError("invalid-email", "email is not a usable address");
27
+ }
28
+ validatePassword(password);
29
+ const name = (displayName ?? "").trim();
30
+ if (name.length > MAX_DISPLAY_NAME_LENGTH) {
31
+ throw new QaAccountsError("invalid-display-name", `display name must be at most ${MAX_DISPLAY_NAME_LENGTH} characters`);
32
+ }
33
+ return { email: normalized, displayName: name };
34
+ }
35
+ //# 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;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,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;AACH,CAAC;AAED;;;;;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,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC3B,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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"}