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