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