@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
@@ -1,66 +0,0 @@
1
- # Architecture
2
-
3
- The Host half validates the Loader configuration, registers the `qa-surface`
4
- settings namespace and owns one narrow GET/HEAD route for the configured QA
5
- path. Published DSH releases return 404 for unknown frontend paths, so the
6
- route redirects navigation through the canonical `/` index with an encoded
7
- same-origin path marker. It also exposes one typed
8
- `secureSession(sessionId)` operation whose policy is read only from Host
9
- settings. The entry delegates that operation to `QaPolicyAdmission`
10
- (`secure-session.ts`), the Host-side admission boundary. It does not serve
11
- HTML, create another server, replace the frontend fallback, or touch
12
- API/plugin routes. The browser removes the
13
- marker and restores the requested path with `history.replaceState` before
14
- route matching. The Host creates no sessions itself.
15
-
16
- The browser half registers `QaSurface` in the root-scoped `shell.overlay` list.
17
- `QaRouteController` keeps that entry dormant outside the configured path and
18
- reversibly observes `pushState`, `replaceState` and `popstate`.
19
-
20
- `QaSessionController` is the only browser-side DSH session-coupled module. It uses
21
- `connection.api.sessions.create/selectModel` for creation policy and the public
22
- `SessionFace` for prompt, cancel and snapshots; the shared client contracts and
23
- the idle state live in `client/types.ts`. Its supporting modules are pure
24
- helpers: `client/wait-for.ts` waits on observable stores, `client/chat-index.ts`
25
- (`QaChatIndex`) keeps the browser-local chat index and persisted active
26
- session in storage, and `client/attestation.ts` parses Host reason markers and
27
- validates proofs against the deployed lockdown config. `QaTranscriptAdapter`
28
- projects session snapshots to plugin-local messages. "New chat" enters a
29
- draft that materializes an attested session only when the first prompt is
30
- sent, and the chat list orders by the host's `updatedAt`. The log is
31
- append-only and there is no truncation seam, so regeneration prompts a
32
- hidden instruction and projects the follow-up turns as variants of one
33
- question; the same projection flattens tool activity into the sources
34
- drawer (web targets, searches, files read). Subagent delegation, when the
35
- deployment opts it in, renders launches and settlement notices inline, and
36
- the agents drawer opens a child session as a read-only live transcript: the
37
- binding skips attestation and every send path stays closed. By default it excludes
38
- reasoning, tool arguments and tool results. When explicitly enabled, it
39
- correlates assistant blocks, paired tool results, running calls and turn
40
- timings into one plugin-local work group per turn; `QaWorkGroup` owns
41
- disclosure state and collapses the group when the turn completes. System
42
- context and raw failures remain excluded.
43
-
44
- In locked mode the Host operation verifies immutable session composition,
45
- refuses to adopt a non-QA session with user history, validates the named
46
- permission preset, installs `ToolRestriction.allow`, installs an agent-scoped
47
- monotonic execution guard, selects the permission preset, and returns only a
48
- sanitized proof. The controller remains non-writable until every proof field
49
- matches its Host-delivered config and repeats the check before prompt
50
- admission. No private RPC handler, slash command, direct provider API, or DSH
51
- core patch is used.
52
-
53
- This narrows a QA agent's effective session/tool policy, but a route overlay on
54
- a shared privileged Host is not an authorization boundary for other DSH
55
- clients. Strong isolation requires a dedicated process/profile, restricted
56
- identity and network boundary.
57
-
58
- All interaction colors come from the company interaction palette
59
- ("Цвета взаимодействия"), defined once as the `QA_BRAND_PALETTE` object at the
60
- top of `styles.ts` and emitted as `--dsh-qa-*` custom properties on the
61
- overlay root; surfaces and typography keep using the themed `--dsw-alias-*`
62
- tokens so light and dark hosts stay coherent.
63
-
64
- The overlay locks document scrolling and contains keyboard focus while active.
65
- It does not locate AppFrame through generated CSS classes and does not dispose
66
- global DSH runtime services when the route changes.
@@ -1,14 +0,0 @@
1
- # Compatibility
2
-
3
- | Plugin version | Tested DSH release | Required public contracts |
4
- | -------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
5
- | 0.1.x | 0.1.5-rc.2 | `shell.overlay`, Typert remote, `agentPresets.composedPreset`, `permissionPresets`, agent-scoped `tools.restrict/guard`, `workspaceRegistry`, session/model APIs, `webServer.register` |
6
-
7
- DSH is developer-preview software. Compatibility-sensitive calls are isolated
8
- in `QaSessionController`, `QaTranscriptAdapter`, `QaConfigController` and
9
- `QaRouteController`; Host lockdown coupling is isolated in
10
- `QaSurface.secureSession`. The Host navigation adapter also works around this tested
11
- release's exact-index static serving: `/qa` is redirected through `/`, then the
12
- client restores the original pathname. Missing settings fall back to default
13
- browser config; missing required client services prevent the browser plugin
14
- from being injected rather than patching DSH internals.
@@ -1,199 +0,0 @@
1
- # Configuration reference
2
-
3
- Defaults and constraints are documented in the root README. Important runtime
4
- rules are:
5
-
6
- - `route.path` starts with `/`, has trailing slashes removed, and cannot claim
7
- `/`, `/api` or `/plugins`;
8
- - `fixed` requires `fixedSessionId`;
9
- - `provider` and `model` are either both absent or both present;
10
- - `maxContentWidth` is an integer from 480 through 1600 and caps the
11
- user-resizable transcript/composer width; each QA route persists its chosen
12
- width in browser storage;
13
- - duplicate/blank suggested questions are removed;
14
- - approval and question policies are fixed to safe blocking behavior;
15
- - reasoning and tool details are opt-in through `ui.showReasoning` and
16
- `ui.showToolActivity`; enable them only where those contents are appropriate
17
- for the QA audience;
18
- - accounts default to disabled; when enabled, `sessionTtlDays` is an integer
19
- from 1 through 365, and other users' chats stay hidden unless an admin view
20
- explicitly enables `accounts.showOtherUsersChats`;
21
- - lockdown defaults to enabled and requires a non-empty permission preset;
22
- - the preset must resolve on the Host to exactly `read-only` + `never`;
23
- - permission, slash-command, settings, rename, delete and arbitrary-open
24
- capability flags cannot be enabled;
25
- - `showReset` requires the independent `allowSessionReset` opt-in;
26
- - the tool policy is an allow-list; unknown configured tool names fail closed.
27
- A name only resolves while its tool is actually mounted: MCP tools
28
- (`mcp__<server>__*`) exist only while their server is reachable, so keep
29
- them out of the allow list on hosts that cannot reach the server. Tools
30
- mounted by an agent preset live in that preset's ancestor scope; the Host
31
- validates and restricts the complete agent-scoped view, not just globals.
32
-
33
- ## Pinning chats to a directory
34
-
35
- By default a QA chat is created against the Host working directory. Two
36
- mutually exclusive pins anchor every new chat (and its subagents) to the
37
- deployment's own directory:
38
-
39
- - `session.cwd: "D:/qa-docs"` - an absolute directory used directly as the
40
- session cwd; no registration needed. Attestation compares the session's
41
- recorded cwd case- and separator-insensitively on Windows.
42
- - `session.workspaceId: "<uuid>"` - a registered DSH workspace (created in
43
- the web UI or via the workspace API; the id is a generated uuid, not the
44
- path). Sessions attach to the workspace, so they group under it in the
45
- host UI. When the id does not resolve, attestation refuses with
46
- `reason: workspace-unavailable` instead of an opaque mismatch.
47
-
48
- With `lockdown.enforceFixedWorkspace: true` (the default) a chat created
49
- anywhere else refuses to attest, so the QA audience can never pull the
50
- assistant out of the pinned directory.
51
-
52
- Browser persistence stores only the DSH session id under
53
- `<storageKey>:v1:<route>:session`, plus — when `ui.showSessionList` is
54
- enabled — a per-browser chat index under `<storageKey>:v1:<route>:chats`
55
- (session ids only, capped at 50). Transcript content, credentials and tool
56
- results remain in the Host-owned DSH Session and are never copied to browser
57
- storage.
58
-
59
- When either work-detail flag is enabled, the QA transcript groups reasoning,
60
- intermediate assistant progress, and tool rows by DSH turn. Running work is
61
- expanded. Completed work is collapsed behind a duration summary and can be
62
- reopened; each tool row can separately reveal its formatted input and output.
63
- These controls change presentation only and never widen the Host allow-list.
64
-
65
- The browser cannot override lockdown settings. Before binding and immediately
66
- before every prompt, it requests a Host attestation for the selected session.
67
- Any unproved agent preset, workspace, model, permission bundle or tool policy
68
- disables Send with the generic message `Настройки помощника недоступны.`
69
- Detailed mismatch facts are written only to Host logs; the
70
- browser console additionally prints one line with a stable coarse reason code
71
- (`reason: unknown-tools`, `workspace-unavailable`, `composition-mismatch`, `permission-preset`,
72
- `adoption-refused`, `proof-mismatch` or `attestation-failed`) plus an operator
73
- hint, so a refused surface can be diagnosed without Host log access.
74
-
75
- The built-in branding, controls, status messages and accessibility labels are
76
- Russian. The default quick questions are rendered directly above the composer
77
- only while the current chat is empty. Set `suggestedQuestions: []` to hide
78
- them, or provide a deployment-specific list to replace them.
79
-
80
- When adding tools, update the deployment's reviewed capability inventory as
81
- part of the same change. The package's
82
- [default inventory](../capability-policy.json) is intentionally empty, matching
83
- the default `toolPolicy.allow`.
84
-
85
- ## Structured sources
86
-
87
- The `sources` block controls provenance independently from Activity rendering.
88
- The defaults collect parent and subagent results, persist a `qa/sources` event,
89
- hide discovery-only candidates, group visible evidence by kind, and promote at
90
- most five substantive `web_search` results when no fetch occurs. URL tracking
91
- parameters and overlapping file ranges are normalized during deduplication.
92
-
93
- `sources.filePreview` is a narrow read capability, not a filesystem browser.
94
- The Host serves only a path already present in the attested session's canonical
95
- evidence bundle, resolves symlinks with `realpath`, rejects root escape, and
96
- applies `maxBytes` plus `maxMarkdownRenderBytes`. Markdown uses the client's
97
- HTML-free renderer; relative links and executable raw HTML are not activated.
98
-
99
- Set `sources.display.showOriginBadges: true` while auditing inheritance. Local
100
- subagent sources carry run/session origins automatically. An opaque provider
101
- can call the internal `qa_report_sources` tool; if it does not, and
102
- `markIncompleteOpaqueRuns` remains enabled, the bundle is explicitly marked
103
- incomplete.
104
-
105
- See [Structured sources migration](SOURCES-MIGRATION.md) before removing an
106
- older prompt-authored bibliography convention.
107
-
108
- ## Accounts and the QA gate
109
-
110
- `accounts.enabled: true` mounts the login/registration gate in front of the
111
- QA surface and turns on server-side session ownership:
112
-
113
- - The store lives at `$DSH_HOME/qa-accounts.json` (created on first use). It
114
- holds scrypt password hashes, a persisted HMAC secret for the account
115
- tokens, and the session ownership map. The path is not configurable and no
116
- secret ever reaches `describe()`.
117
- - `accounts.allowRegistration` (default true) controls self-service signup
118
- in the gate; the first account ever registered becomes `admin`.
119
- - `accounts.sessionTtlDays` (default 30) is the account token lifetime. The
120
- token is an HMAC-signed value the browser keeps in `localStorage`; logout
121
- or expiry returns the browser to the gate, and the Host re-derives the
122
- identity on every attestation.
123
- - `accounts.showOtherUsersChats` (default false) lets admins load and display
124
- chats owned by other QA accounts. It has no effect for ordinary users.
125
- - Session ownership is first come, first served: attesting or bulk-claiming
126
- an unowned session binds it to the caller's account (this is how existing
127
- per-browser chats migrate on the first login). Sessions owned by another
128
- user are refused with the coarse `session-owned-elsewhere` reason and
129
- hidden from the sidebar; admins are not refused.
130
- - Manage accounts with the bundled CLI (no hand-editing of the JSON):
131
-
132
- ```sh
133
- qa-accounts list
134
- qa-accounts add user@example.com --password-stdin --role user
135
- qa-accounts set-role user@example.com admin
136
- qa-accounts disable user@example.com # blocks logins, revokes live tokens
137
- qa-accounts revoke user@example.com # invalidates every issued token
138
- ```
139
-
140
- `disable` bumps the account's token version, so all outstanding tokens die
141
- server-side; `enable` requires a fresh sign-in. After a secret rotation
142
- suspicion, `revoke` is the single-step response.
143
-
144
- - Honest boundary: accounts identify QA users and gate the QA surface and
145
- its remotes. They do not fence the harness: every QA user also holds the
146
- host launch-token cookie, with which the full root UI stays technically
147
- reachable. Keep the network scoping advice from the deployment kit.
148
-
149
- ## Entry redirect
150
-
151
- `entry.redirectNonLoopback: true` (default) injects one script into the
152
- served root `index.html`: browsers whose URL host is not loopback
153
- (`localhost`, `127.0.0.1`, `::1`, `*.localhost`) continue into the QA route,
154
- so LAN visitors never see the full harness root. The check is client-side on
155
- `location.hostname` on purpose - behind a local reverse proxy every request
156
- looks loopback to the server. The navigation hand-off `/?__dsh_qa_route=…`
157
- is never redirected (that would loop), `/?ui=admin` bypasses the redirect
158
- and is remembered for the browser, and `/?ui=qa` clears the bypass.
159
-
160
- `entry.cookieBootstrap: true` (default) additionally lets the `/qa` route
161
- itself bootstrap the host cookie: a browser without any `dsh-auth-` cookie is
162
- sent to the one-time `/?token=…` exchange (relative redirect, token resolved
163
- from the host connection service) before the marker hand-off. This makes the
164
- transparent entry work without the deploy proxy; with the proxy in front,
165
- either side may perform the exchange and the other becomes a no-op.
166
-
167
- ## Configuration channel over the LAN
168
-
169
- The browser normally reads the effective configuration from the Host-owned
170
- `qa-surface` settings namespace. DSH pins settings RPCs to loopback, so a
171
- browser served over the LAN always sees that namespace as unavailable. In
172
- that case the client calls the plugin's `qaSurface/describe` Host Remote and
173
- uses the returned effective configuration; a rejected call falls back to the
174
- client defaults with the same `unavailable` status as before. Only one
175
- describe request runs per page load, and a settled answer survives scope
176
- updates — the namespace (when readable) stays the authority and keeps
177
- delivering live changes.
178
-
179
- Host-side enforcement never depended on the browser's read path: `secureSession`
180
- attestation re-derives everything from the Host-owned configuration on every
181
- bind and every prompt.
182
-
183
- ## Skill catalog scope
184
-
185
- The deployment's agent preset controls which skills the QA assistant sees.
186
- The shipped `qa-research` preset mounts the skill filesystem with
187
- `includeDefaultRoots: false`: project-root skills (for example the harness
188
- checkout's own `.dsh/skills`) and user-home skills stay out of the catalog,
189
- and skills enter only through plugin providers or an explicit
190
- `customSkillDirs` list in the preset. Keep the QA catalog to exactly the
191
- skills the audience is meant to use.
192
-
193
- ## Deleting chats
194
-
195
- The sidebar delete control removes a chat from this browser's index only;
196
- the Host session stays on disk. Deleting the chat that is currently open
197
- continues in a fresh attested session when `lockdown.allowSessionReset` is
198
- enabled. There is no Host session-deletion API in DSH 0.1.x for the plugin
199
- to call.
package/docs/EMBEDDING.md DELETED
@@ -1,13 +0,0 @@
1
- # Embedding and reverse proxies
2
-
3
- Serve `/qa` through the normal DSH origin so it shares the established browser
4
- connection, authentication and trust policy. The plugin's narrow navigation
5
- route redirects GET/HEAD requests through DSH's canonical `/` document; the
6
- client immediately restores `/qa` before mounting. Reverse proxies must pass
7
- the configured QA path to DSH instead of replacing it with a separately hosted
8
- page.
9
-
10
- Iframe embedding is disabled or allowed by deployment headers, not by this
11
- plugin. Configure an explicit CSP `frame-ancestors` allowlist at a trusted
12
- reverse proxy and keep the DSH API same-origin. Do not enable wildcard CORS or
13
- expose settings/session APIs without the deployment's authentication layer.
@@ -1,28 +0,0 @@
1
- # Structured sources migration
2
-
3
- `dsh-qa-surface` now treats `QaTurnSources` as the only canonical provenance
4
- model. Assistant prose is never parsed for a `Sources` or `Источники` section,
5
- and the default `sources.legacy.parseAssistantSourcesBlock` remains `false`.
6
-
7
- ## Deployment steps
8
-
9
- 1. Keep the QA agent's existing read-only retrieval tools in
10
- `lockdown.toolPolicy.allow`.
11
- 2. Leave `sources.collect.persistTurnEvent: true` so completed turns append a
12
- replayable `qa/sources` snapshot.
13
- 3. Remove prompt instructions that ask the assistant to manually write a
14
- bibliography. The plugin contributes its own scoped guidance after QA
15
- attestation.
16
- 4. For an opaque subagent provider, expose the inherited
17
- `qa_report_sources` tool and ask the provider to call it before completion.
18
- Local DSH subagents require no reporting prompt.
19
- 5. Start with `sources.display.showDiscovered: false`; enable it only for
20
- provenance debugging.
21
-
22
- Historical sessions without `qa/sources` remain readable: the Host reconstructs
23
- bundles from durable structured tool metadata when available. Historical
24
- assistant-authored source prose remains ordinary answer text and is not trusted
25
- as provenance.
26
-
27
- The `qa/sources` event is a required plugin-owned session event. Keep
28
- `dsh-qa-surface` loaded in any Host that restores sessions containing it.
@@ -1,88 +0,0 @@
1
- # Sources / provenance implementation audit
2
-
3
- Date: 2026-09-11
4
- Pinned DSH development API: `0.1.5-rc.2`
5
-
6
- This note records the Phase 0 findings for
7
- `SPEC-dsh-qa-surface-sources-provenance.md`. It is intentionally tied to the
8
- versions in this repository rather than to DSH `master`.
9
-
10
- ## Existing QA data path
11
-
12
- - `QaSessionController` binds the client `UiConversation` snapshot.
13
- - `QaTranscriptAdapter` reads the `chat.legacy` projection.
14
- - The former Sources drawer inferred one row from selected tool names and raw
15
- arguments. It counted a search query as a source and did not retain turn,
16
- line-range, evidence, or origin data.
17
- - `ToolResultNode.meta` is available in the client projection. DSH documents it
18
- as the tool-private, JSON-safe result presentation payload persisted on the
19
- corresponding `tool/result` event.
20
-
21
- ## Structured metadata available in the pinned version
22
-
23
- `@deepseek-ai/dsh-tools` exports the provider-neutral result shapes needed by
24
- the first implementation slice:
25
-
26
- - `ReadResultView`: `path`, numbered `lines`, `offset`, `totalLines`, `lang`;
27
- - `SearchResultView`: grouped grep matches or path discovery;
28
- - `WebSearchResultView`: structured `sources[]` and truncation state;
29
- - `WebFetchResultView`: final redirect URL, status, and truncation state;
30
- - generic call `FileLocation` values.
31
-
32
- The QA extractor layer therefore consumes `ToolResultNode.meta` first and uses
33
- tool arguments only as a compatibility fallback for direct read/fetch calls.
34
- It does not parse assistant prose.
35
-
36
- ## Replay and persistence
37
-
38
- The completed implementation rebuilds collectors from persisted
39
- `tool/call`/`tool/result.meta` events and writes one materialized `qa/sources`
40
- snapshot at `agent/turn-stopping`. The type is declaration-merged into
41
- `SessionEventMap`.
42
-
43
- The pinned DSH exports its live `KNOWN_SESSION_EVENT_TYPES` set. As already
44
- demonstrated by the repository's `dsh-session-scope` plugin, registering the
45
- plugin event there is the supported compatibility mechanism when
46
- `Session.append` cannot mark an extension event ignorable. Registration is
47
- effect-scoped and removed on plugin disposal. A Host that owns sessions with
48
- these events must load `dsh-qa-surface`, just as it must load any plugin that
49
- owns a required durable event vocabulary.
50
-
51
- ## Subagent lifecycle
52
-
53
- The pinned `@deepseek-ai/dsh-subagent` does expose process-local
54
- `subagent/start` and `subagent/end` events. Both carry `runId`, `provider`, child
55
- session `id`, and `local`; the terminal payload additionally carries
56
- `stopReason` and optional `lastAssistantMessage`. Scoped dispatch is keyed by
57
- the delegating parent.
58
-
59
- The Host now maintains that lineage registry, uses the child header or causal
60
- `AgentRegistry.currentInitiator()` attribution, bubbles nested local-child
61
- sources to the original root turn, and retains run/session origin fields.
62
- Opaque providers can call `qa_report_sources`; an unreported opaque run sets
63
- `complete=false` with an `incompleteOrigins` entry.
64
-
65
- ## File preview reuse
66
-
67
- The new `readSourceFile` Remote is limited to evidence paths in canonical
68
- bundles, re-attests the session, resolves both root and target with `realpath`,
69
- rejects traversal/symlink escape, caps bytes, and exposes no write/list API.
70
-
71
- ## Implemented scope
72
-
73
- - canonical source and turn-bundle types;
74
- - URL/path/range normalization;
75
- - extractor registry with read, grep discovery, web search, and web fetch;
76
- - evidence promotion, deduplication, ranking, and a default display threshold;
77
- - bounded structured web-search promotion (five results by default);
78
- - Host per-turn collectors and materialized/replayable `qa/sources` events;
79
- - local and nested subagent inheritance plus opaque-provider reporting;
80
- - structured Jira, Confluence, knowledge and reported-source adapters;
81
- - one canonical snapshot used by grouped drawer and answer footer;
82
- - source details, origin/partial badges and completeness state;
83
- - safe local-file preview, rendered/raw Markdown, range chips and exact raw
84
- line highlighting;
85
- - configuration and tests for the feature surface.
86
-
87
- Optional inline citation rendering (SPEC Phase 5) remains intentionally out of
88
- the MVP, as specified.
@@ -1,219 +0,0 @@
1
- # SPEC — dsh-qa-surface accounts and entry routing
2
-
3
- Status: implemented (this document is the design record).
4
- Scope: `plugins/dsh-qa-surface` plus the external `qa-deploy` kit
5
- (proxy + launcher). The DSH harness itself is not modified.
6
-
7
- ## Goals
8
-
9
- 1. A LAN visitor who opens the server root lands on the QA surface, not the
10
- full harness UI; the loopback operator keeps the harness root as-is.
11
- 2. A LAN visitor without the host browser-auth cookie is transparently sent
12
- through the one-time `?token=` exchange and returned to `/qa` without ever
13
- handling the launch token manually.
14
- 3. QA users identify themselves with an email + password account. Existing
15
- per-browser chats migrate to the account on first login. Session ownership
16
- is enforced server-side on the plugin's admission boundary.
17
-
18
- ## Non-goals / honest boundary
19
-
20
- - The host launch token remains a master key: `/api` RPC requires the
21
- host-auth cookie (`rpc-host.ts` rejects 401 without it), so every QA user
22
- must hold it, and with it the full harness UI is technically reachable.
23
- Accounts gate the QA surface and the plugin's own remotes server-side; they
24
- do not isolate a determined user from the harness. This is documented in the
25
- deployment kit and the disclaimer stays visible in the UI.
26
- - No email verification (no SMTP): the email is an identifier, not a contact
27
- channel.
28
- - No admin UI for user management in this phase; the first registered account
29
- becomes `admin`, later roles change via the accounts file.
30
-
31
- ## Wire design: explicit account token, not cookies
32
-
33
- Typert remotes receive only `(endpoint, payload, signal)` — the HTTP request
34
- never reaches a remote method (`packages/client/connection/src/rpc-host.ts`,
35
- `rpcFetchHandler`), so a cookie-based gate inside remotes is impossible
36
- without harness changes. Named plugin routes do see `req`/`res`, but keeping
37
- auth on a parallel HTTP surface would leave the existing typert remotes
38
- unguarded.
39
-
40
- Therefore the account credential is an explicit first argument on every
41
- gated remote, and login/register return it in the value:
42
-
43
- - `accountsLogin(email, password)` → `{ token, user }`
44
- - `accountsRegister(email, password, displayName?)` → `{ token, user }`
45
- - `accountsWhoami(token)` → `{ authenticated: true, user } | { authenticated: false }`
46
- - `accountsClaimSessions(token, sessionIds)` → `{ claimed, conflicts }`
47
- - `accountsOwnedSessions(token)` → `{ ids }`
48
- - `accountsListOwnership(token)` → `{ entries: [{ sessionId, userId,
49
- displayName, claimedAt }] }` — admin-only (`admin-required` refusal for
50
- ordinary accounts, `auth-required` for anonymous)
51
- - `secureSession(token, sessionId)` — now requires a valid token when
52
- accounts are enabled
53
- - `sources(token, sessionId)`, `readSourceFile(token, sessionId, path)` — same
54
-
55
- The token is an HMAC-SHA256-signed `v1.<payload>.<sig>` string
56
- (payload `{ uid, exp }`) keyed by a secret generated once and persisted in
57
- the accounts file. The browser keeps it in `localStorage` under the
58
- deployment's per-route key. Login/register failures are coarse (same message
59
- for unknown email and wrong password); registration rejects duplicate emails;
60
- a per-store in-memory counter rate-limits auth attempts.
61
-
62
- ## Server: accounts store and admission
63
-
64
- - File: `$DSH_HOME/qa-accounts.json` (env fallback: process cwd). Shape:
65
- `{ version, secret, users: [{ id, email, displayName, role, passwordHash,
66
- createdAt, lastLoginAt }], ownership: { [sessionId]: { userId, claimedAt } } }`.
67
- Written atomically (temp file + rename), all mutations synchronous.
68
- - Passwords: scrypt with per-user salt, timing-safe comparison.
69
- - Roles: `user` | `admin`. The first registered user becomes `admin`.
70
- Admins bypass the ownership refusal (the surface is read-only anyway);
71
- non-admins get a coarse `session-owned-elsewhere` attestation refusal for
72
- sessions owned by someone else.
73
- - Claim rule (first come, first served): attesting an _unowned_ session claims
74
- it for the requesting user (this is how pre-accounts sessions migrate when
75
- an old browser re-opens its `activeId` without a bulk claim). Bulk claims
76
- from the client's local index follow the same rule; conflicting ids are
77
- reported back and dropped from that browser's index.
78
- - `QaPolicyAdmission.secureSession` gains an optional accounts gate: when
79
- `accounts.enabled`, an invalid/expired token refuses with
80
- `auth-required` before any policy work.
81
-
82
- ## Entry redirect (host-injected script)
83
-
84
- The plugin listens to the `webserver/index-inject` event and pushes one
85
- inline `script` row into the served root `index.html` (config
86
- `entry.redirectNonLoopback`, default true):
87
-
88
- - Skip when the navigation marker (`__dsh_qa_route`) is present — the `/qa`
89
- route hands off through `/?__dsh_qa_route=…` and a naive redirect would
90
- loop.
91
- - Skip when `?ui=admin` is set (also persists the choice under the
92
- deployment's storage key); `?ui=qa` clears the choice.
93
- - Skip when the persisted choice is `admin`.
94
- - Otherwise redirect non-loopback hostnames (`localhost`, `127.0.0.1`,
95
- `::1`, `*.localhost`) to `/qa`. The check is client-side on
96
- `location.hostname` **on purpose**: behind the qa-deploy proxy every
97
- request appears loopback to the server, and the operator convention is
98
- "operator uses localhost, QA users use the LAN address".
99
-
100
- The script is defensive (try/catch, no dependency on bundle load) and never
101
- redirects when the plugin is disabled or the flag is off.
102
-
103
- ## Client: accounts controller and auth gate
104
-
105
- - `QaAccountsController` owns the state machine
106
- `checking → anonymous → authed`, the token persistence, login/register
107
- calls, the one-shot migration claim (local chat index → `claimSessions`)
108
- after each successful login/registration, and the owned-session id list
109
- that backs the sidebar.
110
- - `QaSurface` renders a full-frame `QaAuthGate` (login/register tabs, email +
111
- password, coarse errors, busy state) instead of the chat while accounts are
112
- enabled and the user is anonymous; the session controller is not created
113
- until `authed`, so nothing attests or sends before login.
114
- - `QaSessionController` threads the token into `secureSession`/`sources`/
115
- `readSourceFile` calls and merges the owned-session list into the chat
116
- index; newly created chats are claimed at creation time. When accounts are
117
- disabled the controller behaves exactly as before.
118
- - An attestation refusal with reason `auth-required` re-opens the gate
119
- (token expired/rotated) instead of surfacing the generic configuration
120
- error.
121
- - A small account chip (email, role, logout) lives in the sidebar footer;
122
- logout clears the token and returns to the gate.
123
-
124
- ## Admin ownership views
125
-
126
- When `accounts.showOtherUsersChats: true`, the admin sidebar switches from the
127
- flat list to per-owner sections. The option defaults to false, so admins see
128
- only their own chats unless the deployment explicitly opts in.
129
- `accountsListOwnership` returns every ownership entry with the owner's
130
- display name resolved at read time (disabled accounts still name their
131
- chats), the controller merges those session ids into the visible list, and
132
- `QaSidebar` renders one section per owner ordered by its freshest chat, with
133
- unclaimed chats trailing under "Без владельца". Search filters rows first,
134
- so empty sections disappear while searching. Ordinary accounts and
135
- deployments with accounts disabled keep the exact flat list as before.
136
-
137
- The same data drives author labels: user messages carry an `author` byline
138
- with the chat owner's display name, but only for an admin reading a foreign
139
- chat — the owner themself sees no label, and authorship is chat-level (the
140
- ownership map), not per-message. Admins replying inside someone else's chat
141
- are therefore labeled with the chat owner, not with their own name; the
142
- per-message identity channel (the client-minted `requestId` on
143
- `session/prompt`) is a possible future refinement.
144
-
145
- ## Config surface
146
-
147
- ```yaml
148
- accounts:
149
- enabled: false # opt-in; the qa-deploy profile patch turns it on
150
- allowRegistration: true # self-service signup in the gate
151
- sessionTtlDays: 30 # account token lifetime
152
- showOtherUsersChats: false # opt-in cross-user admin view
153
- entry:
154
- redirectNonLoopback: true # the root → /qa script above
155
- ```
156
-
157
- No file paths or secrets are configurable through the schema: the store path
158
- is derived from `DSH_HOME` internally, and `describe()` (the LAN config
159
- channel) keeps projecting only client-safe values.
160
-
161
- ## qa-deploy kit (external repo)
162
-
163
- - `run-qa.sh` extracts the launch token from the boot banner
164
- (`dsh web: …?token=…`) and starts `proxy.mjs` in front of the harness
165
- (default proxy port 8088, `DSH_QA_PROXY=0` to disable). LAN visitors are
166
- pointed at the proxy address; the operator keeps the direct loopback URL.
167
- - `proxy.mjs` gains the transparent exchange: a GET/HEAD page navigation to
168
- `/` or `/qa…` without a `dsh-auth-` cookie and without `?token=` gets a 302
169
- to `/?token=<TOKEN>`; the harness exchanges it (server-side 303) and the
170
- injected script finishes the trip to `/qa`. Everything else passes through
171
- unchanged (Host/Origin rewrite, polyfill, WS splice).
172
- - The profile patch (`profile/cordis.patch.yml`) enables
173
- `accounts.enabled`, keeping self-registration on.
174
-
175
- ## Testing
176
-
177
- - Store: scrypt round-trip, duplicate-email rejection, first-user-admin,
178
- atomic persistence, token sign/verify/expire, ownership claim/conflict.
179
- - Admission: token-less refusal when enabled, ownership refusal, admin
180
- bypass, unowned auto-claim, disabled passthrough.
181
- - Entry redirect: script text guards (marker, `ui=admin`/`?ui=qa`,
182
- loopback hostnames, disable flag), injection through the index-inject
183
- event.
184
- - Client: controller state machine, migration claim, token threading,
185
- auth-required recovery; auth gate rendering in the component suite.
186
- - Admin views: ownership listing (display-name resolution, admin/anonymous
187
- refusals), the visible-id merge, author-label rules, owner section
188
- ordering and rendering.
189
- - Packed verification keeps asserting the standard card shell and now the
190
- auth gate markup.
191
-
192
- ## Follow-ups adopted from dsh-auth-gate (same release)
193
-
194
- A review of the independent `dsh-auth-gate` plugin yielded four additions,
195
- folded into this spec rather than shipped as a separate design:
196
-
197
- 1. **Proxy-side config-plane deny list** (qa-deploy kit). The deploy proxy
198
- rewrites Host/Origin to loopback, which makes the `/api` trust fence treat
199
- every proxied request as loopback — including the privileged methods
200
- (`settings.*`, `credentials.*`, `host.*`, `llm.*`). The proxy now answers
201
- 403 for those method prefixes when the caller is not loopback. The QA
202
- surface never calls them (its configuration rides `qaSurface/describe`);
203
- the operator on the serving machine keeps the full plane.
204
- 2. **Plugin-side launch-token bridge.** The `/qa` route handler defers the
205
- marker hand-off for cookie-less browsers: it 302s to a relative
206
- `/?token=…` first (token resolved once per process from
207
- `connection.authenticatedUrl`, the dsh-auth-gate pattern), so the
208
- transparent entry works without the proxy. `entry.cookieBootstrap`
209
- (default true) turns it off. Presence of any `dsh-auth-` cookie skips the
210
- bridge; a token-less bridge or a disabled flag falls back to the marker
211
- hand-off.
212
- 3. **`qa-accounts` management CLI** (`bin` entry). `list`, `add
213
- --password-stdin`, `set-role`, `disable`, `enable`, `revoke` — so account
214
- administration never requires hand-editing the JSON file.
215
- 4. **Account state and token revocation.** Accounts gain a `disabled` flag
216
- (refused at login with `account-disabled`, invisible to `whoami`) and a
217
- `tokenVersion` burned into every token: `disable` and `revoke` bump it,
218
- invalidating all live tokens server-side — logout is no longer purely a
219
- client-side act.