@yadsh/dsh-qa-surface 0.19.0 → 0.21.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 (252) hide show
  1. package/README.md +235 -0
  2. package/lib/access/model-choices.js +103 -0
  3. package/lib/access/model-choices.js.map +1 -0
  4. package/lib/access/model-policy.js +79 -7
  5. package/lib/access/model-policy.js.map +1 -1
  6. package/lib/access/model.js +29 -1
  7. package/lib/access/model.js.map +1 -1
  8. package/lib/access/queue-policy.js +36 -0
  9. package/lib/access/queue-policy.js.map +1 -0
  10. package/lib/access/service.js +124 -7
  11. package/lib/access/service.js.map +1 -1
  12. package/lib/account-remotes.js +10 -1
  13. package/lib/account-remotes.js.map +1 -1
  14. package/lib/accounts/database.js +44 -3
  15. package/lib/accounts/database.js.map +1 -1
  16. package/lib/accounts/store.js +28 -0
  17. package/lib/accounts/store.js.map +1 -1
  18. package/lib/admin/service.js +6 -1
  19. package/lib/admin/service.js.map +1 -1
  20. package/lib/chat-sharing/transport.js +6 -2
  21. package/lib/chat-sharing/transport.js.map +1 -1
  22. package/lib/client/QaAccountsController.js +43 -8
  23. package/lib/client/QaAccountsController.js.map +1 -1
  24. package/lib/client/QaSessionController.js +55 -23
  25. package/lib/client/QaSessionController.js.map +1 -1
  26. package/lib/client/QaSurface.js +61 -3
  27. package/lib/client/QaSurface.js.map +1 -1
  28. package/lib/client/QaTranscriptAdapter.js +36 -1
  29. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  30. package/lib/client/admin/ModelPolicyEditor.js +28 -2
  31. package/lib/client/admin/ModelPolicyEditor.js.map +1 -1
  32. package/lib/client/admin/RoleEditor.js +18 -1
  33. package/lib/client/admin/RoleEditor.js.map +1 -1
  34. package/lib/client/admin/pages/SkillFiles.js +1 -1
  35. package/lib/client/admin/pages/SkillFiles.js.map +1 -1
  36. package/lib/client/admin/pages/Users.js +36 -16
  37. package/lib/client/admin/pages/Users.js.map +1 -1
  38. package/lib/client/admin/styles.js +3 -0
  39. package/lib/client/admin/styles.js.map +1 -1
  40. package/lib/client/chat-artifacts.js +1 -0
  41. package/lib/client/chat-artifacts.js.map +1 -1
  42. package/lib/client/code-source-styles.js +12 -0
  43. package/lib/client/code-source-styles.js.map +1 -0
  44. package/lib/client/components/QaArtifactAttachment.js +12 -0
  45. package/lib/client/components/QaArtifactAttachment.js.map +1 -0
  46. package/lib/client/components/QaChangelog.js +94 -0
  47. package/lib/client/components/QaChangelog.js.map +1 -1
  48. package/lib/client/components/QaChatActions.js +36 -14
  49. package/lib/client/components/QaChatActions.js.map +1 -1
  50. package/lib/client/components/QaCodeSourcePicker.js +18 -0
  51. package/lib/client/components/QaCodeSourcePicker.js.map +1 -0
  52. package/lib/client/components/QaComposer.js +11 -12
  53. package/lib/client/components/QaComposer.js.map +1 -1
  54. package/lib/client/components/QaComposerSettings.js +202 -0
  55. package/lib/client/components/QaComposerSettings.js.map +1 -0
  56. package/lib/client/components/QaFilesPanel.js +2 -8
  57. package/lib/client/components/QaFilesPanel.js.map +1 -1
  58. package/lib/client/components/QaFormItem.js +74 -38
  59. package/lib/client/components/QaFormItem.js.map +1 -1
  60. package/lib/client/components/QaHistoryCatalog.js +261 -0
  61. package/lib/client/components/QaHistoryCatalog.js.map +1 -0
  62. package/lib/client/components/QaInputForm.js +115 -32
  63. package/lib/client/components/QaInputForm.js.map +1 -1
  64. package/lib/client/components/QaMessage.js +3 -8
  65. package/lib/client/components/QaMessage.js.map +1 -1
  66. package/lib/client/components/QaModelSelector.js +21 -0
  67. package/lib/client/components/QaModelSelector.js.map +1 -0
  68. package/lib/client/components/QaSidebar.js +25 -24
  69. package/lib/client/components/QaSidebar.js.map +1 -1
  70. package/lib/client/components/QaWorkGroup.js +1 -3
  71. package/lib/client/components/QaWorkGroup.js.map +1 -1
  72. package/lib/client/composer-settings-styles.js +21 -0
  73. package/lib/client/composer-settings-styles.js.map +1 -0
  74. package/lib/client/forms-styles.js +35 -11
  75. package/lib/client/forms-styles.js.map +1 -1
  76. package/lib/client/history-organization-styles.js +31 -0
  77. package/lib/client/history-organization-styles.js.map +1 -0
  78. package/lib/client/history-organization.js +155 -0
  79. package/lib/client/history-organization.js.map +1 -0
  80. package/lib/client/index.js +13 -4
  81. package/lib/client/index.js.map +1 -1
  82. package/lib/client/settings/card.js +2 -2
  83. package/lib/client/settings/card.js.map +1 -1
  84. package/lib/client/settings/sections/attachments.js +7 -1
  85. package/lib/client/settings/sections/attachments.js.map +1 -1
  86. package/lib/client/settings/sections/file-writes.js +15 -0
  87. package/lib/client/settings/sections/file-writes.js.map +1 -0
  88. package/lib/client/settings/sections/session.js +1 -1
  89. package/lib/client/settings/sections/session.js.map +1 -1
  90. package/lib/client/settings/sections.js +1 -0
  91. package/lib/client/settings/sections.js.map +1 -1
  92. package/lib/client/styles.js +18 -1
  93. package/lib/client/styles.js.map +1 -1
  94. package/lib/client/tool-presentation.js +21 -3
  95. package/lib/client/tool-presentation.js.map +1 -1
  96. package/lib/client/types.js.map +1 -1
  97. package/lib/client/use-chat-code-source.js +156 -0
  98. package/lib/client/use-chat-code-source.js.map +1 -0
  99. package/lib/client/use-history-organization.js +48 -0
  100. package/lib/client/use-history-organization.js.map +1 -0
  101. package/lib/client/use-model-selection.js +96 -0
  102. package/lib/client/use-model-selection.js.map +1 -0
  103. package/lib/client/use-qa-file-drop.js +81 -0
  104. package/lib/client/use-qa-file-drop.js.map +1 -0
  105. package/lib/client/user-settings/IntegrationTokensPage.js +42 -6
  106. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -1
  107. package/lib/client/user-settings/UserSettingsDialog.js +1 -1
  108. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  109. package/lib/client.js +125 -27
  110. package/lib/client.js.map +1 -1
  111. package/lib/code-source-selection.js +102 -0
  112. package/lib/code-source-selection.js.map +1 -0
  113. package/lib/config-resolvers/accounts.js +26 -1
  114. package/lib/config-resolvers/accounts.js.map +1 -1
  115. package/lib/config-resolvers/defaults.js +7 -0
  116. package/lib/config-resolvers/defaults.js.map +1 -1
  117. package/lib/config-resolvers/integration.js +8 -1
  118. package/lib/config-resolvers/integration.js.map +1 -1
  119. package/lib/config-resolvers/session.js +2 -0
  120. package/lib/config-resolvers/session.js.map +1 -1
  121. package/lib/config-resolvers/tools.js +2 -0
  122. package/lib/config-resolvers/tools.js.map +1 -1
  123. package/lib/config.js +77 -4
  124. package/lib/config.js.map +1 -1
  125. package/lib/enforcement/skill-policy.js +37 -2
  126. package/lib/enforcement/skill-policy.js.map +1 -1
  127. package/lib/forms/errors.js +6 -1
  128. package/lib/forms/errors.js.map +1 -1
  129. package/lib/forms/gate.js +28 -2
  130. package/lib/forms/gate.js.map +1 -1
  131. package/lib/forms/schema-errors.js +47 -0
  132. package/lib/forms/schema-errors.js.map +1 -0
  133. package/lib/forms/schema.js +29 -20
  134. package/lib/forms/schema.js.map +1 -1
  135. package/lib/index.js +161 -9
  136. package/lib/index.js.map +1 -1
  137. package/lib/integration/http.js +9 -10
  138. package/lib/integration/http.js.map +1 -1
  139. package/lib/integration/service.js +9 -0
  140. package/lib/integration/service.js.map +1 -1
  141. package/lib/personal-skills/provider.js +9 -1
  142. package/lib/personal-skills/provider.js.map +1 -1
  143. package/lib/personal-skills/service.js +4 -0
  144. package/lib/personal-skills/service.js.map +1 -1
  145. package/lib/personal-skills/shared-seed.js +65 -0
  146. package/lib/personal-skills/shared-seed.js.map +1 -0
  147. package/lib/prompt-notes.js +24 -2
  148. package/lib/prompt-notes.js.map +1 -1
  149. package/lib/provenance/collector.js +1 -0
  150. package/lib/provenance/collector.js.map +1 -1
  151. package/lib/provenance/extractors.js +6 -1
  152. package/lib/provenance/extractors.js.map +1 -1
  153. package/lib/qa-tools/catalog.js +33 -1
  154. package/lib/qa-tools/catalog.js.map +1 -1
  155. package/lib/qa-tools/code-files.js +207 -0
  156. package/lib/qa-tools/code-files.js.map +1 -0
  157. package/lib/qa-tools/code-tools.js +158 -0
  158. package/lib/qa-tools/code-tools.js.map +1 -0
  159. package/lib/qa-tools/docs-tools.js +49 -12
  160. package/lib/qa-tools/docs-tools.js.map +1 -1
  161. package/lib/qa-tools/file-write-gate.js +47 -0
  162. package/lib/qa-tools/file-write-gate.js.map +1 -0
  163. package/lib/qa-tools/index.js +10 -0
  164. package/lib/qa-tools/index.js.map +1 -1
  165. package/lib/qa-tools/request-user-form.js +26 -6
  166. package/lib/qa-tools/request-user-form.js.map +1 -1
  167. package/lib/qa-tools/subagent-tools.js +159 -0
  168. package/lib/qa-tools/subagent-tools.js.map +1 -0
  169. package/lib/request-scheduler.js +32 -5
  170. package/lib/request-scheduler.js.map +1 -1
  171. package/lib/resolve-config.js +9 -1
  172. package/lib/resolve-config.js.map +1 -1
  173. package/lib/secure-session.js +16 -4
  174. package/lib/secure-session.js.map +1 -1
  175. package/lib/session-model.js +43 -1
  176. package/lib/session-model.js.map +1 -1
  177. package/lib/shared/file-write-policy.js +29 -0
  178. package/lib/shared/file-write-policy.js.map +1 -0
  179. package/lib/shared/history-folder.js +18 -0
  180. package/lib/shared/history-folder.js.map +1 -0
  181. package/lib/typert.host.js +757 -162
  182. package/lib/typert.remote-client.d.ts +19 -7
  183. package/lib/typert.remote-client.js +688 -153
  184. package/lib/types/access/model-choices.d.ts +16 -0
  185. package/lib/types/access/model-policy.d.ts +13 -7
  186. package/lib/types/access/model.d.ts +9 -0
  187. package/lib/types/access/queue-policy.d.ts +30 -0
  188. package/lib/types/access/service.d.ts +27 -0
  189. package/lib/types/account-remotes.d.ts +5 -4
  190. package/lib/types/accounts/database.d.ts +5 -0
  191. package/lib/types/accounts/store.d.ts +6 -0
  192. package/lib/types/chat-sharing/transport.d.ts +1 -1
  193. package/lib/types/client/QaAccountsController.d.ts +3 -1
  194. package/lib/types/client/QaSessionController.d.ts +19 -5
  195. package/lib/types/client/QaSurface.d.ts +2 -0
  196. package/lib/types/client/admin/ModelPolicyEditor.d.ts +2 -0
  197. package/lib/types/client/code-source-styles.d.ts +2 -0
  198. package/lib/types/client/components/QaArtifactAttachment.d.ts +8 -0
  199. package/lib/types/client/components/QaChatActions.d.ts +3 -1
  200. package/lib/types/client/components/QaCodeSourcePicker.d.ts +9 -0
  201. package/lib/types/client/components/QaComposer.d.ts +2 -0
  202. package/lib/types/client/components/QaComposerSettings.d.ts +16 -0
  203. package/lib/types/client/components/QaFormItem.d.ts +4 -2
  204. package/lib/types/client/components/QaHistoryCatalog.d.ts +19 -0
  205. package/lib/types/client/components/QaInputForm.d.ts +3 -2
  206. package/lib/types/client/components/QaModelSelector.d.ts +4 -0
  207. package/lib/types/client/components/QaSidebar.d.ts +3 -0
  208. package/lib/types/client/composer-settings-styles.d.ts +2 -0
  209. package/lib/types/client/forms-styles.d.ts +1 -1
  210. package/lib/types/client/history-organization-styles.d.ts +3 -0
  211. package/lib/types/client/history-organization.d.ts +38 -0
  212. package/lib/types/client/settings/sections/file-writes.d.ts +4 -0
  213. package/lib/types/client/settings/sections.d.ts +1 -0
  214. package/lib/types/client/tool-presentation.d.ts +1 -0
  215. package/lib/types/client/types.d.ts +12 -4
  216. package/lib/types/client/use-chat-code-source.d.ts +20 -0
  217. package/lib/types/client/use-history-organization.d.ts +8 -0
  218. package/lib/types/client/use-model-selection.d.ts +24 -0
  219. package/lib/types/client/use-qa-file-drop.d.ts +12 -0
  220. package/lib/types/code-source-selection.d.ts +28 -0
  221. package/lib/types/code-sources.js +2 -0
  222. package/lib/types/code-sources.js.map +1 -0
  223. package/lib/types/config-resolvers/integration.d.ts +7 -1
  224. package/lib/types/forms/errors.d.ts +3 -2
  225. package/lib/types/forms/gate.d.ts +4 -2
  226. package/lib/types/forms/schema-errors.d.ts +7 -0
  227. package/lib/types/forms/types.d.ts +14 -0
  228. package/lib/types/index.d.ts +38 -5
  229. package/lib/types/integration/http.d.ts +1 -3
  230. package/lib/types/integration/service.d.ts +2 -0
  231. package/lib/types/personal-skills/shared-seed.d.ts +12 -0
  232. package/lib/types/prompt-notes.d.ts +6 -2
  233. package/lib/types/qa-tools/catalog.d.ts +5 -2
  234. package/lib/types/qa-tools/code-files.d.ts +59 -0
  235. package/lib/types/qa-tools/code-tools.d.ts +7 -0
  236. package/lib/types/qa-tools/docs-tools.d.ts +4 -0
  237. package/lib/types/qa-tools/file-write-gate.d.ts +6 -0
  238. package/lib/types/qa-tools/index.d.ts +3 -0
  239. package/lib/types/qa-tools/subagent-tools.d.ts +40 -0
  240. package/lib/types/request-scheduler.d.ts +21 -2
  241. package/lib/types/session-model.d.ts +11 -2
  242. package/lib/types/shared/file-write-policy.d.ts +10 -0
  243. package/lib/types/shared/history-folder.d.ts +4 -0
  244. package/lib/types/types/accounts.d.ts +11 -0
  245. package/lib/types/types/capability.d.ts +31 -4
  246. package/lib/types/types/code-sources.d.ts +12 -0
  247. package/lib/types/types/config.d.ts +23 -0
  248. package/lib/types/types/documents.d.ts +2 -0
  249. package/lib/types/types.d.ts +1 -0
  250. package/lib/user-workspace.js +7 -4
  251. package/lib/user-workspace.js.map +1 -1
  252. package/package.json +6 -5
package/README.md CHANGED
@@ -23,10 +23,74 @@ Session and Agent Loop.
23
23
 
24
24
  ## What it does
25
25
 
26
+ Chat history supports virtual folders and a separate pinned section. Create,
27
+ rename, collapse and delete folders; drag chats into folders, out to the root,
28
+ or into the pinned section. Drag rows or folder headers to arrange them, or use
29
+ the chat menu and folder settings with a keyboard. Unpinning returns a chat to
30
+ its previous folder. Moving a pinned chat into a folder unpins it. Deleting a
31
+ folder leaves its chats in history, preserving any pins. Search includes folder
32
+ names and reveals matching chats inside collapsed folders.
33
+
34
+ This organization is browser-local and scoped to the QA route and account ID.
35
+ It survives reloads and updates other tabs on the same origin through storage
36
+ events; it does not synchronize between devices. Folder membership and pins
37
+ change only presentation metadata: session IDs, workspace roots, physical files,
38
+ ownership and access remain as they were.
39
+
40
+ Under **Integration tokens**, choose **Folder for new chats** when creating a
41
+ credential, or change the destination on an existing active token without
42
+ reissuing it. This default is stored on the server as a virtual folder ID and
43
+ display name. Each newly created API conversation snapshots it before its first
44
+ answer, even when no browser is open. Authenticated history imports that folder
45
+ and assignment once; manual moves and folder deletions are preserved. Changing
46
+ or clearing a token's default affects future conversations, not continuations
47
+ of an existing `session_id`. The default does not change workspace paths or
48
+ grant access to another account's chats. The folder picker lists folders from
49
+ this browser's history and retains an existing server selection from another
50
+ browser. New API chats can recreate a selected folder that was deleted locally;
51
+ clear or change the token default to stop using that destination.
52
+
26
53
  Corporate sign-in through Keycloak or another OpenID Connect provider is opt-in.
27
54
  The source tree includes `plugins/dsh-qa-surface/docs/oidc.md` with client settings,
28
55
  account linking, role admission, session limits and deployment acceptance.
29
56
 
57
+ The composer has one compact **Chat settings** menu beside the attachment
58
+ button. Its summary shows the current model and code version. The **Model**
59
+ and **Code version** entries open nested choice lists with a checkmark for the
60
+ current value; Back, Escape and arrow keys navigate the levels. The menu closes
61
+ after a successful choice and stays open with an error if saving fails. Model
62
+ choices appear when the role or account permits alternatives; code choices
63
+ appear when a code-source catalog is configured.
64
+
65
+ Choosing a model applies to the current chat and
66
+ survives reopening it; new chats keep the configured default. Switching waits
67
+ until the chat has finished its response and has no queued request. Readers,
68
+ participants and delegated-agent views cannot change the owner's model.
69
+
70
+ Administrators set the default and **Alternative models for the user** in the
71
+ role's Model tab or the account's model settings. `model` keeps its existing
72
+ priority: account, role, deployment. The optional `allowedModels` array adds
73
+ provider/model pairs to that default. An omitted account array inherits its
74
+ role's alternatives; `[]` explicitly restricts that account to its default.
75
+ An omitted role array grants no alternatives. Saved policies keep their
76
+ previous single-model behavior until an administrator grants alternatives.
77
+ The server filters the catalog and validates every selection against the
78
+ owner's current policy, including model replacement rules.
79
+
80
+ Chat-local selection uses the Session controller's guarded selection commit
81
+ capability, which records the native `model/selection` event and updates the
82
+ next-request selection without saving the Host operator's global default.
83
+ A Host without that capability rejects the switch rather than changing a
84
+ deployment-wide preference. The rest of the QA surface remains available.
85
+
86
+ The focused packed browser check is `pnpm --filter @yadsh/dsh-qa-surface
87
+ e2e:browser --model-selection`. Set `DSH_QA_SMOKE_VERSION` to the Host version
88
+ being checked, and optionally `DSH_QA_HISTORY_EVIDENCE_DIR` to a local evidence
89
+ directory. This pass exercises synthetic model routes without real provider
90
+ credentials, including access filtering, switching, reload persistence,
91
+ new-chat defaults, and desktop/phone layout. It does not run the separate LAN
92
+ deployment-overlay acceptance pass.
93
+
30
94
  - contributes one root-scoped entry to the additive `shell.overlay` slot;
31
95
  - stays invisible outside the configured route;
32
96
  - creates or restores one real DSH Session;
@@ -327,6 +391,11 @@ config:
327
391
  # Presets whose sessions may unlock the catalog. Empty leaves the gate open:
328
392
  # only do that when this Host serves one agent composition.
329
393
  activationPresets: []
394
+ # Native file writes and edits in QA chats, delegated calls included.
395
+ # Exact case-insensitive suffixes; [] disables this additional restriction.
396
+ fileWritePolicy:
397
+ blockedExtensions: [.py, .pyw]
398
+ message: "Do not create or execute scripts. Use the available document and spreadsheet tools. If an operation is unavailable, report the limitation."
330
399
  # Where the documentation readers look. Empty reads `docs/` inside each
331
400
  # chat's own workspace; name the corpus when it is published once, outside
332
401
  # every per-user workspace (see "Dynamic QA tools" below).
@@ -338,6 +407,35 @@ uses the Host's normal default working directory. Put the system prompt, tools,
338
407
  skills, knowledge connections and permission policy in `agentPreset`, not in
339
408
  this UI plugin.
340
409
 
410
+ ### File write policy
411
+
412
+ The operator's QA settings card includes **Запись файлов**: a list of blocked
413
+ extensions and the refusal message returned to the agent. The default list is
414
+ `.py`, `.pyw`. Changes apply to the next tool call, including existing chats
415
+ and their delegated agents; restarting the Host is unnecessary after changing
416
+ the settings on a build that includes this feature.
417
+
418
+ The native `write`, `edit` and mutating `str_replace_editor` commands are denied
419
+ before dispatch. Reads and the editor's `view` command remain subject to their
420
+ existing access rules. Matching ignores case, accepts extensions with or without
421
+ a leading dot, and supports compound suffixes such as `.tar.gz`. The policy
422
+ checks both the requested filename and its canonical target, so an allowed
423
+ symlink name cannot overwrite a blocked file. `[]` disables only this policy;
424
+ it does not grant sandbox or tool access. The message must contain 1–4000
425
+ characters. Paths and wildcards are refused in the extension list.
426
+
427
+ This is a native file-tool policy, not a content classifier or a general process
428
+ sandbox: it does not inspect scripts disguised with an allowed extension or
429
+ outputs produced by other tools. Existing QA restrictions on shell execution
430
+ and `apply_patch` remain in force independently.
431
+
432
+ Run `pnpm --filter @yadsh/dsh-qa-surface smoke:packed --file-write-settings`
433
+ to check the settings in Chromium against a disposable packed Host. Set
434
+ `DSH_QA_FILE_WRITE_EVIDENCE` to a local directory to retain screenshots and
435
+ geometry measurements. The round covers save/reload, empty-list handling,
436
+ invalid-message refusal, reset, and light/dark layouts at 390 and 1440 pixels;
437
+ it also runs as part of the full `e2e:browser` pass.
438
+
341
439
  ### Dynamic QA tools
342
440
 
343
441
  A QA deployment tends to grow a large toolset, and attaching all of it at boot
@@ -423,6 +521,83 @@ dynamically attached tool gets the same scrutiny as an allow-listed one. Tool
423
521
  visibility is not an authorization boundary — a QA tool that writes must still
424
522
  enforce its own permissions.
425
523
 
524
+ ### Multiple code sources
525
+
526
+ `codeSources` defines named code trees independently of folder and branch
527
+ names. Configure it on the QA Surface row; preset-mounted Git tools read
528
+ this same catalogue unless their own non-empty catalogue overrides it.
529
+
530
+ ```yaml
531
+ codeSources:
532
+ defaultSource: stable
533
+ entries:
534
+ - id: stable
535
+ label: "Stable release"
536
+ path: /srv/code/release-a
537
+ docsVersion: "2.0"
538
+ - id: preview
539
+ label: "Development"
540
+ path: /srv/code/work-in-progress
541
+ docsVersion: "2.0"
542
+ lockdown:
543
+ sharedReadOnlyRoots:
544
+ - /srv/code
545
+ ```
546
+
547
+ Grant Git access separately with `repositoryRoots: [/srv/code]` on the
548
+ Git plugin row mounted in the agent preset. Adding a catalogue entry does
549
+ not grant either filesystem or Git access. The chat's writable scratch
550
+ directory stays separate from the read-only code trees.
551
+
552
+ After QA tool activation, `code_sources {}` reports the catalogue, its default
553
+ and each tree's read availability. `code_read` and `code_search` accept
554
+ `source: stable` or `source: preview` and source-relative `path` values.
555
+ Omitting `source` uses `defaultSource`, or the only entry when there is one;
556
+ several entries without a default require an explicit choice. A missing,
557
+ unreadable or unknown source fails without falling back to another tree.
558
+ Use the same id in `dsh_git_*` calls. Generic `read`, `glob` and `grep` still
559
+ use explicit paths; use the source-aware tools for version-scoped work.
560
+
561
+ Search is literal and case-insensitive, bounded to 1,000 attempted text
562
+ files, 10,000 visited entries, 8 MiB of input, three seconds and 32 KiB of
563
+ results. It skips links, binary/oversized files and common generated
564
+ directories (`.git`, `node_modules`, `.nx`, `.cache`, `bin`, `obj`, `dist`,
565
+ `lib`). `truncated` means incomplete: narrow `path` or query before
566
+ concluding code is absent. Reading accepts UTF-8 files up to 1 MiB and
567
+ returns at most 400 lines and 32 KiB. Both tools refuse traversal and nested
568
+ symbolic links. Results carry the source id, label and root; file provenance
569
+ preserves the source id and actual file address.
570
+
571
+ `docs_search` accepts `source` and uses its optional `docsVersion` mapping.
572
+ When neither `source`, `version` nor `path` is given, the default source's
573
+ mapping takes precedence over `tools.docsDefaultVersion`. Without a mapping,
574
+ the existing documentation default stays in force. An explicit `version`
575
+ or `path` without `source` retains its override behaviour. An explicit
576
+ `source` without a mapping is refused, as is a conflicting explicit edition.
577
+ `docs_read` validates the file's edition against the chat's selected source
578
+ unless a different source is explicitly named. Release and development sources
579
+ may share a documentation edition.
580
+
581
+ The **Версия кода** entry in the composer's shared settings menu saves an explicit source for
582
+ the chat on the server. Reopening the chat or using another browser restores
583
+ that choice. **По умолчанию** clears the override and follows the deployment
584
+ default. Draft selection creates no session: it is saved before the first
585
+ prompt, and a failed save prevents the prompt and can be retried.
586
+
587
+ Only the chat owner can change the source. Shared readers and collaborators
588
+ see the same choice, and changes are refused while the chat or its subagents
589
+ are running, while input is queued, or while the chat waits for a server slot.
590
+ The plugin stores preferences in `qa-code-sources.db` beside the accounts DB;
591
+ include that database in deployment backups. New delegated children and forks
592
+ snapshot the parent's effective source. Descendants keep that snapshot if the
593
+ parent later changes its choice; an ordinary fork can change its own source.
594
+
595
+ An omitted `source` in Git, code or documentation tools uses the persisted
596
+ session choice. Explicit tool parameters still allow comparisons with another
597
+ configured source and do not change the saved choice. Removing a selected
598
+ source from configuration makes it unavailable rather than selecting another
599
+ version. No directory is created, moved or updated by these tools.
600
+
426
601
  ### Writable per-user research space
427
602
 
428
603
  Set `accounts.perUserWorkspace: true` only together with accounts, a registered
@@ -519,6 +694,34 @@ then collapses to `Worked for ...` before the final answer. Tool capability is
519
694
  still controlled exclusively by `lockdown.toolPolicy.allow`; the display flags
520
695
  do not grant tools.
521
696
 
697
+ When a provider retires a model identifier, `session.modelReplacements` defines
698
+ exact central redirects after account → role → deployment precedence:
699
+
700
+ ```yaml
701
+ session:
702
+ provider: local
703
+ model: model-v2
704
+ modelReplacements:
705
+ - from:
706
+ provider: local
707
+ model: model-v1
708
+ to:
709
+ provider: local
710
+ model: model-v2
711
+ ```
712
+
713
+ The rule covers saved account and role pins as well as the deployment default;
714
+ unrelated pairs keep their own selection. The terminal target must exist in the
715
+ Host catalog. Cycles and duplicate sources are configuration errors. Chained
716
+ redirects let a later transition cover older pins without editing every account;
717
+ retain a rule while stored policies still refer to its source. The stored pins
718
+ remain intact. New chats use the replacement immediately. On access attestation,
719
+ an idle owned QA chat can move along an exact redirect to its current policy;
720
+ an active turn is left unchanged. Ownership, workspace and adoption checks still
721
+ precede reconciliation. A missing target fails explicitly instead of selecting
722
+ another model from catalog order. The journal records the original and applied
723
+ pairs, and delegated experts resolve the owning chat's policy through Host lineage.
724
+
522
725
  ### User profile
523
726
 
524
727
  Clicking the account name in the sidebar footer opens the signed-in user's
@@ -690,6 +893,17 @@ Changing the limit does not interrupt already active turns: lowering it admits
690
893
  no new chat until the active count fits, and raising it releases more waiters.
691
894
  Commands that do not run a turn remain usable while the stand is full.
692
895
 
896
+ The number is the chain's last layer, and the two above it override it per turn:
897
+ a subrole may carry `maxActiveRequests` (edited on the «Саброли» page's model
898
+ tab), and an account may carry one (the user card's «Слоты очереди этого
899
+ аккаунта» select). The account beats the role, the role beats the deployment,
900
+ and 0 is a real value that lifts the ceiling for that role's or account's turns
901
+ outright. A turn whose principal raises the number fits while a lower ceiling
902
+ would refuse it, and one full principal does not starve the queue behind it.
903
+ The status read answers the caller's own chain: a chat's read uses the chat's
904
+ account, the pre-send read uses the visitor's, and a stand without accounts
905
+ keeps the one deployment number.
906
+
693
907
  Regeneration: the last committed answer offers a retry action. The session log
694
908
  is append-only, so "regenerate" sends a hidden instruction as an ordinary
695
909
  prompt and the answer arrives as a follow-up turn; the projection hides that
@@ -1002,6 +1216,26 @@ export function apply(ctx: Context) {
1002
1216
  }
1003
1217
  ```
1004
1218
 
1219
+ Model-selectable QA delegation uses `qa_list_subagent_models` and `qa_subagent`.
1220
+ Grant both tools in the role's base tools or through an activated skill. They
1221
+ follow normal QA tool activation. Discovery returns only the chat owner's
1222
+ default and explicitly granted alternatives present in the live Host catalog,
1223
+ with the reasoning efforts that policy permits. Account overrides, role grants
1224
+ and model replacements use the same policy as the chat's model selector.
1225
+ `qa_subagent` accepts `description`, a self-contained `prompt`, the exact
1226
+ `provider`/`model` pair and optional `reasoning_effort`. It starts a fresh native
1227
+ `spawn` child, inherits the parent's preset and workspace, and respects Host
1228
+ depth limits, cancellation and parent result delivery. Model and effort grants
1229
+ are checked again after route resolution, before a child is created. The parent
1230
+ chat's model is unchanged. A missing spawn backend refuses the call; a Host
1231
+ without subagents can still load the QA surface.
1232
+
1233
+ Run the packed integration check with
1234
+ `DSH_QA_SMOKE_VERSION=0.2.1-rc.3 pnpm --filter @yadsh/dsh-qa-surface run smoke:packed --subagent-models`.
1235
+ It uses a disposable Host and a scripted local LLM endpoint to verify model
1236
+ discovery for an ordinary account, denial of an unassigned model, a native child
1237
+ request on the selected route and preservation of the parent's model.
1238
+
1005
1239
  Subagents: the deployment may opt the delegation family (`subagent`,
1006
1240
  `subagent_fork`, `send_message`, `list_agents`, `interrupt_agent`) into the
1007
1241
  lockdown allow-list; the preset must mount them. Launches then render as
@@ -1266,6 +1500,7 @@ integration:
1266
1500
  requestTimeoutMs: 3600000 # one-hour answer budget; see docs/INTEGRATION-API.md §2.5
1267
1501
  maxConcurrent: 4
1268
1502
  requestsPerMinute: 60
1503
+ maxAttachments: 0 # 0 inherits attachments.maxPending; positive values set an API limit (1-40)
1269
1504
  maxAnswerCharacters: 4096 # the answer the ticket comment can hold
1270
1505
  ```
1271
1506
 
@@ -0,0 +1,103 @@
1
+ import { SessionId } from "@deepseek-ai/dsh-session/types";
2
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
3
+ import { sessionModelSelection } from "../session-model.js";
4
+ import { modelPolicySatisfied, projectModelCatalog, } from "./model-policy.js";
5
+ /** Only explicit policy grants are exposed to an ordinary account. */
6
+ export async function userModelOptions(ctx, access, token, sessionId, subroleId, preview) {
7
+ const resolve = () => {
8
+ if (sessionId === null)
9
+ return access.draftModelPolicy(token, subroleId, preview);
10
+ return access.userSessionModelPolicy(token, sessionId);
11
+ };
12
+ resolve(); // Authenticate before reading the Host catalog.
13
+ const catalog = projectModelCatalog(await ctx.sessionController.modelCatalog());
14
+ const policy = resolve(); // Recheck the token and grants after the async read.
15
+ const granted = [
16
+ ...(policy.pair === undefined ? [] : [policy.pair]),
17
+ ...(policy.allowedModels ?? []),
18
+ ];
19
+ const models = catalog.filter((entry) => granted.some((pair) => pair.provider === entry.provider && pair.model === entry.model));
20
+ const agent = sessionId === null ? undefined : ctx.agents.get(SessionId(sessionId));
21
+ const facts = agent === undefined ? policy.pair : sessionModelSelection(ctx, agent);
22
+ const selected = facts?.provider === undefined || facts.model === undefined
23
+ ? undefined
24
+ : {
25
+ provider: facts.provider,
26
+ model: facts.model,
27
+ ...(facts.reasoningEffort === undefined
28
+ ? {}
29
+ : { reasoningEffort: facts.reasoningEffort }),
30
+ };
31
+ return {
32
+ models,
33
+ choices: granted,
34
+ ...(policy.pair === undefined ? {} : { defaultModel: policy.pair }),
35
+ ...(selected === undefined ? {} : { selected }),
36
+ };
37
+ }
38
+ /** Validate both policy membership and live availability before a user mutation. */
39
+ export async function assertUserModelSelection(ctx, policy, request) {
40
+ if (typeof request.provider !== "string" || typeof request.model !== "string")
41
+ throw new Error("Choose a complete provider and model pair.");
42
+ const pair = {
43
+ provider: request.provider,
44
+ model: request.model,
45
+ ...(request.reasoningEffort == null
46
+ ? {}
47
+ : { reasoningEffort: request.reasoningEffort }),
48
+ };
49
+ const catalog = projectModelCatalog(await ctx.sessionController.modelCatalog());
50
+ const entry = catalog.find((item) => item.provider === pair.provider && item.model === pair.model);
51
+ if (entry === undefined)
52
+ throw new Error("This model is currently unavailable.");
53
+ if (pair.reasoningEffort !== undefined &&
54
+ !entry.reasoningEfforts.includes(pair.reasoningEffort))
55
+ throw new Error("This reasoning effort is unavailable for the selected model.");
56
+ const effective = policy();
57
+ if ((effective.pair === undefined && effective.allowedModels === undefined) ||
58
+ !modelPolicySatisfied(effective, pair))
59
+ throw new Error("This model is not allowed by the chat policy.");
60
+ }
61
+ /** Native durable per-session selection, without the operator-wide default save. */
62
+ export async function selectUserModel(ctx, access, token, request) {
63
+ const id = String(request.sessionId);
64
+ const policy = () => access.userSessionModelPolicy(token, id);
65
+ policy();
66
+ const agent = ctx.agents.get(request.sessionId);
67
+ if (agent === undefined)
68
+ throw new Error("The chat is unavailable.");
69
+ // DSH's public selectModel also saves the operator-wide default. Its owned
70
+ // agent controller has the narrower commit primitive. Keep this compatibility
71
+ // bridge guarded and fail closed when the Host no longer exposes it; never
72
+ // fall back to changing a deployment-wide preference from a QA account.
73
+ const controller = ctx.sessionController.agents;
74
+ if (typeof controller?.serializeImageAdmission !== "function" ||
75
+ typeof controller.selectForNextRequest !== "function")
76
+ throw new Error("This Host does not support chat-local model selection.");
77
+ return controller.serializeImageAdmission(agent, async () => {
78
+ if (agent.status !== "idle")
79
+ throw new Error("Wait for the current response before changing the model.");
80
+ await assertUserModelSelection(ctx, policy, request);
81
+ const selected = await ctx.llm.resolveCallConfig({
82
+ provider: request.provider,
83
+ model: request.model,
84
+ ...(request.reasoningEffort === undefined
85
+ ? {}
86
+ : { reasoningEffort: ReasoningEffortId(request.reasoningEffort) }),
87
+ });
88
+ if (!modelPolicySatisfied(policy(), selected))
89
+ throw new Error("This model is not allowed by the chat policy.");
90
+ if (agent.status !== "idle")
91
+ throw new Error("Wait for the current response before changing the model.");
92
+ const pair = {
93
+ provider: selected.provider,
94
+ model: selected.model,
95
+ ...(selected.reasoningEffort === undefined
96
+ ? {}
97
+ : { reasoningEffort: selected.reasoningEffort }),
98
+ };
99
+ controller.selectForNextRequest(agent, pair);
100
+ return { selected: pair };
101
+ });
102
+ }
103
+ //# sourceMappingURL=model-choices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-choices.js","sourceRoot":"","sources":["../../src/access/model-choices.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,28 @@
1
+ /** Preserve an explicit empty override and reject malformed or excessive grants. */
2
+ export function normalizeAllowedModels(value, label) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ if (!Array.isArray(value) || value.length > 100)
6
+ throw new TypeError(`${label} must be an array of at most 100 model pairs`);
7
+ const pairs = value.map((entry, index) => {
8
+ const pair = normalizeModelPair(entry, `${label}[${index}]`);
9
+ if (pair === undefined)
10
+ throw new TypeError(`${label} needs complete pairs`);
11
+ return pair;
12
+ });
13
+ return Object.freeze([
14
+ ...new Map(pairs.map((pair) => [JSON.stringify(pair), pair])).values(),
15
+ ]);
16
+ }
17
+ /** Add explicit grants without changing which model a new chat starts on. */
18
+ export function withAllowedModels(policy, models, replacements) {
19
+ if (models === undefined)
20
+ return policy;
21
+ return Object.freeze({
22
+ ...policy,
23
+ allowedModels: Object.freeze(models.map((pair) => replaceModelPair(pair, replacements))),
24
+ });
25
+ }
1
26
  function text(value) {
2
27
  return typeof value === "string" ? value.trim() : "";
3
28
  }
@@ -47,18 +72,62 @@ export function deploymentModelPair(session) {
47
72
  * @param layers - the layers in priority order, `undefined` where a layer keeps
48
73
  * no opinion.
49
74
  */
50
- export function resolveModelPolicy(layers) {
75
+ export function resolveModelPolicy(layers, replacements = []) {
51
76
  const order = [
52
77
  "account",
53
78
  "subrole",
54
79
  "deployment",
55
80
  ];
56
81
  const layer = order.find((candidate) => layers[candidate] !== undefined);
82
+ const original = layer === undefined ? undefined : layers[layer];
83
+ const pair = replaceModelPair(original, replacements);
57
84
  return Object.freeze({
58
- pair: layer === undefined ? undefined : layers[layer],
85
+ pair,
59
86
  layer,
87
+ ...(original !== undefined && pair !== original
88
+ ? { redirectedFrom: original }
89
+ : {}),
60
90
  });
61
91
  }
92
+ /** Resolve only explicitly configured exact redirects; never guess from catalog order. */
93
+ export function replaceModelPair(pair, replacements) {
94
+ if (pair === undefined)
95
+ return undefined;
96
+ let current = pair;
97
+ const visited = new Set();
98
+ for (;;) {
99
+ const key = JSON.stringify([current.provider, current.model]);
100
+ if (visited.has(key))
101
+ throw new TypeError("session.modelReplacements contains a cycle");
102
+ visited.add(key);
103
+ const next = replacements.find(({ from }) => from.provider === current.provider && from.model === current.model);
104
+ if (next === undefined)
105
+ return current;
106
+ current = next.to;
107
+ }
108
+ }
109
+ export function normalizeModelReplacements(input) {
110
+ if (input === undefined)
111
+ return Object.freeze([]);
112
+ if (!Array.isArray(input) || input.length > 100)
113
+ throw new TypeError("session.modelReplacements must be an array of at most 100 exact replacements");
114
+ const keys = new Set();
115
+ const replacements = input.map((entry, index) => {
116
+ const label = `session.modelReplacements[${index}]`;
117
+ const from = normalizeModelPair(entry?.from, `${label}.from`);
118
+ const to = normalizeModelPair(entry?.to, `${label}.to`);
119
+ if (from === undefined || to === undefined)
120
+ throw new TypeError(`${label} needs complete from/to pairs`);
121
+ const key = JSON.stringify([from.provider, from.model]);
122
+ if (keys.has(key))
123
+ throw new TypeError(`${label} duplicates a source pair`);
124
+ keys.add(key);
125
+ return Object.freeze({ from, to });
126
+ });
127
+ for (const { from } of replacements)
128
+ replaceModelPair(from, replacements);
129
+ return Object.freeze(replacements);
130
+ }
62
131
  /**
63
132
  * Whether one agent's selection sits inside the session's model policy.
64
133
  *
@@ -70,12 +139,15 @@ export function resolveModelPolicy(layers) {
70
139
  */
71
140
  export function modelPolicySatisfied(policy, selection) {
72
141
  const pair = policy.pair;
73
- if (pair === undefined)
142
+ if (pair === undefined && policy.allowedModels === undefined)
74
143
  return true;
75
- return (pair.provider === selection.provider &&
76
- pair.model === selection.model &&
77
- (pair.reasoningEffort === undefined ||
78
- pair.reasoningEffort === selection.reasoningEffort));
144
+ return [
145
+ ...(pair === undefined ? [] : [pair]),
146
+ ...(policy.allowedModels ?? []),
147
+ ].some((candidate) => candidate.provider === selection.provider &&
148
+ candidate.model === selection.model &&
149
+ (candidate.reasoningEffort === undefined ||
150
+ candidate.reasoningEffort === selection.reasoningEffort));
79
151
  }
80
152
  /**
81
153
  * Project the harness catalog into the pairs the administration surface lists.
@@ -1 +1 @@
1
- {"version":3,"file":"model-policy.js","sourceRoot":"","sources":["../../src/access/model-policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"model-policy.js","sourceRoot":"","sources":["../../src/access/model-policy.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,27 @@
1
- import { normalizeModelPair } from "./model-policy.js";
1
+ import { normalizeModelPair, normalizeAllowedModels } from "./model-policy.js";
2
+ import { QA_MAX_ACTIVE_REQUESTS_MAX } from "../config-resolvers/session.js";
2
3
  import { normalizeSkillOverride, resolveSkillVisibility, skillHealth, } from "./skill-metadata.js";
3
4
  const CAPABILITY_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,199}$/u;
4
5
  const SUBROLE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/u;
6
+ /**
7
+ * Normalize one queue-slots override.
8
+ *
9
+ * An absent field keeps no opinion and the layer below it speaks. 0 is a real
10
+ * value — it lifts the ceiling for this role or account — while anything that
11
+ * is not an integer from 0 to {@link QA_MAX_ACTIVE_REQUESTS_MAX} is refused,
12
+ * the same range the deployment's own field accepts.
13
+ */
14
+ export function normalizeQueueSlots(value, label) {
15
+ if (value === undefined || value === null)
16
+ return undefined;
17
+ if (typeof value !== "number" ||
18
+ !Number.isSafeInteger(value) ||
19
+ value < 0 ||
20
+ value > QA_MAX_ACTIVE_REQUESTS_MAX) {
21
+ throw new TypeError(`${label} must be an integer from 0 to ${String(QA_MAX_ACTIVE_REQUESTS_MAX)}`);
22
+ }
23
+ return value;
24
+ }
5
25
  function uniqueIds(values, label) {
6
26
  const result = [
7
27
  ...new Set(values.map((value) => {
@@ -78,6 +98,8 @@ export function normalizeSubrole(value) {
78
98
  const icon = value.ui?.icon?.trim();
79
99
  const accent = value.ui?.accent?.trim();
80
100
  const model = normalizeModelPair(value.model, `subrole ${id}.model`);
101
+ const allowedModels = normalizeAllowedModels(value.allowedModels, `subrole ${id}.allowedModels`);
102
+ const maxActiveRequests = normalizeQueueSlots(value.maxActiveRequests, `subrole ${id}.maxActiveRequests`);
81
103
  return Object.freeze({
82
104
  id,
83
105
  name,
@@ -85,6 +107,8 @@ export function normalizeSubrole(value) {
85
107
  enabled: value.enabled === true,
86
108
  capabilities: normalizeCapabilitySelection(value.capabilities),
87
109
  ...(model === undefined ? {} : { model }),
110
+ ...(allowedModels === undefined ? {} : { allowedModels }),
111
+ ...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
88
112
  ...(icon === undefined && accent === undefined
89
113
  ? {}
90
114
  : {
@@ -196,10 +220,14 @@ export function normalizeUserAccess(value, config) {
196
220
  ? requestedDefault
197
221
  : allowed[0];
198
222
  const model = normalizeModelPair(value?.model, "account model");
223
+ const allowedModels = normalizeAllowedModels(value?.allowedModels, "account allowedModels");
224
+ const maxActiveRequests = normalizeQueueSlots(value?.maxActiveRequests, "account maxActiveRequests");
199
225
  return Object.freeze({
200
226
  allowedSubroles: Object.freeze(allowed),
201
227
  defaultSubrole,
202
228
  ...(model === undefined ? {} : { model }),
229
+ ...(allowedModels === undefined ? {} : { allowedModels }),
230
+ ...(maxActiveRequests === undefined ? {} : { maxActiveRequests }),
203
231
  });
204
232
  }
205
233
  function available(values, installed) {