@yadsh/dsh-qa-surface 0.10.0 → 0.11.1

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 (169) hide show
  1. package/README.md +310 -48
  2. package/lib/access/service.js +49 -10
  3. package/lib/access/service.js.map +1 -1
  4. package/lib/account-remotes.js +41 -0
  5. package/lib/account-remotes.js.map +1 -1
  6. package/lib/accounts/database.js +140 -0
  7. package/lib/accounts/database.js.map +1 -1
  8. package/lib/accounts/errors.js.map +1 -1
  9. package/lib/accounts/service-token.js +103 -0
  10. package/lib/accounts/service-token.js.map +1 -0
  11. package/lib/accounts/store.js +291 -5
  12. package/lib/accounts/store.js.map +1 -1
  13. package/lib/admin/conversation-log.js +14 -8
  14. package/lib/admin/conversation-log.js.map +1 -1
  15. package/lib/admin/permissions.js +5 -0
  16. package/lib/admin/permissions.js.map +1 -1
  17. package/lib/admin/quality-store.js +4 -0
  18. package/lib/admin/quality-store.js.map +1 -1
  19. package/lib/admin/service.js +173 -0
  20. package/lib/admin/service.js.map +1 -1
  21. package/lib/admin/session-log.js +39 -8
  22. package/lib/admin/session-log.js.map +1 -1
  23. package/lib/attachment-rules.js +39 -6
  24. package/lib/attachment-rules.js.map +1 -1
  25. package/lib/cli.js +83 -1
  26. package/lib/cli.js.map +1 -1
  27. package/lib/client/QaAccountsController.js +213 -4
  28. package/lib/client/QaAccountsController.js.map +1 -1
  29. package/lib/client/QaSessionController.js +37 -21
  30. package/lib/client/QaSessionController.js.map +1 -1
  31. package/lib/client/QaSurface.js +28 -4
  32. package/lib/client/QaSurface.js.map +1 -1
  33. package/lib/client/admin/QaAdmin.js +9 -1
  34. package/lib/client/admin/QaAdmin.js.map +1 -1
  35. package/lib/client/admin/pages/SkillFiles.js +78 -0
  36. package/lib/client/admin/pages/SkillFiles.js.map +1 -0
  37. package/lib/client/admin/pages/Users.js +65 -1
  38. package/lib/client/admin/pages/Users.js.map +1 -1
  39. package/lib/client/admin/routes.js +8 -1
  40. package/lib/client/admin/routes.js.map +1 -1
  41. package/lib/client/admin/styles.js +2 -0
  42. package/lib/client/admin/styles.js.map +1 -1
  43. package/lib/client/attachments.js +41 -11
  44. package/lib/client/attachments.js.map +1 -1
  45. package/lib/client/components/QaAuthGate.js +19 -10
  46. package/lib/client/components/QaAuthGate.js.map +1 -1
  47. package/lib/client/components/QaChangelog.js +43 -0
  48. package/lib/client/components/QaChangelog.js.map +1 -1
  49. package/lib/client/index.js +13 -0
  50. package/lib/client/index.js.map +1 -1
  51. package/lib/client/lineage.js +64 -0
  52. package/lib/client/lineage.js.map +1 -1
  53. package/lib/client/settings/sections/attachments.js +6 -5
  54. package/lib/client/settings/sections/attachments.js.map +1 -1
  55. package/lib/client/settings/sections/notes.js +22 -4
  56. package/lib/client/settings/sections/notes.js.map +1 -1
  57. package/lib/client/styles.js +9 -0
  58. package/lib/client/styles.js.map +1 -1
  59. package/lib/client/types.js.map +1 -1
  60. package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
  61. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
  62. package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
  63. package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
  64. package/lib/client/user-settings/SkillCatalog.js +2 -2
  65. package/lib/client/user-settings/SkillCatalog.js.map +1 -1
  66. package/lib/client/user-settings/UserSettingsDialog.js +17 -2
  67. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  68. package/lib/client/user-settings/copy.js +20 -0
  69. package/lib/client/user-settings/copy.js.map +1 -1
  70. package/lib/client.js +2480 -137
  71. package/lib/client.js.map +1 -1
  72. package/lib/config-resolvers/attachments.js +6 -5
  73. package/lib/config-resolvers/attachments.js.map +1 -1
  74. package/lib/config-resolvers/defaults.js +23 -2
  75. package/lib/config-resolvers/defaults.js.map +1 -1
  76. package/lib/config-resolvers/integration.js +94 -0
  77. package/lib/config-resolvers/integration.js.map +1 -0
  78. package/lib/config-resolvers/notes.js +2 -0
  79. package/lib/config-resolvers/notes.js.map +1 -1
  80. package/lib/config.js +72 -0
  81. package/lib/config.js.map +1 -1
  82. package/lib/index.js +197 -2
  83. package/lib/index.js.map +1 -1
  84. package/lib/integration/answer.js +100 -0
  85. package/lib/integration/answer.js.map +1 -0
  86. package/lib/integration/attachments.js +246 -0
  87. package/lib/integration/attachments.js.map +1 -0
  88. package/lib/integration/contract.js +53 -0
  89. package/lib/integration/contract.js.map +1 -0
  90. package/lib/integration/host-runner.js +243 -0
  91. package/lib/integration/host-runner.js.map +1 -0
  92. package/lib/integration/http.js +576 -0
  93. package/lib/integration/http.js.map +1 -0
  94. package/lib/integration/service.js +373 -0
  95. package/lib/integration/service.js.map +1 -0
  96. package/lib/integration/transcript-reader.js +202 -0
  97. package/lib/integration/transcript-reader.js.map +1 -0
  98. package/lib/integration/transcript.js +148 -0
  99. package/lib/integration/transcript.js.map +1 -0
  100. package/lib/personal-skills/index.js +2 -2
  101. package/lib/personal-skills/index.js.map +1 -1
  102. package/lib/personal-skills/paths.js +14 -8
  103. package/lib/personal-skills/paths.js.map +1 -1
  104. package/lib/personal-skills/provider.js +16 -4
  105. package/lib/personal-skills/provider.js.map +1 -1
  106. package/lib/personal-skills/service.js +240 -30
  107. package/lib/personal-skills/service.js.map +1 -1
  108. package/lib/prompt-notes.js +58 -0
  109. package/lib/prompt-notes.js.map +1 -1
  110. package/lib/provenance/document-preview.js +10 -1
  111. package/lib/provenance/document-preview.js.map +1 -1
  112. package/lib/qa-tools/catalog.js +12 -1
  113. package/lib/qa-tools/catalog.js.map +1 -1
  114. package/lib/qa-tools/docs-tools.js +563 -0
  115. package/lib/qa-tools/docs-tools.js.map +1 -0
  116. package/lib/resolve-config.js +4 -0
  117. package/lib/resolve-config.js.map +1 -1
  118. package/lib/secure-session.js +83 -3
  119. package/lib/secure-session.js.map +1 -1
  120. package/lib/shared/integration-tokens.js +23 -0
  121. package/lib/shared/integration-tokens.js.map +1 -0
  122. package/lib/typert.host.js +1176 -213
  123. package/lib/typert.remote-client.d.ts +27 -1
  124. package/lib/typert.remote-client.js +911 -69
  125. package/lib/types/access/service.d.ts +33 -2
  126. package/lib/types/account-remotes.d.ts +30 -1
  127. package/lib/types/accounts/database.d.ts +53 -0
  128. package/lib/types/accounts/errors.d.ts +5 -1
  129. package/lib/types/accounts/service-token.d.ts +70 -0
  130. package/lib/types/accounts/store.d.ts +131 -1
  131. package/lib/types/admin/conversation-log.d.ts +12 -0
  132. package/lib/types/admin/service.d.ts +56 -1
  133. package/lib/types/admin/session-log.d.ts +21 -0
  134. package/lib/types/attachment-rules.d.ts +26 -3
  135. package/lib/types/client/QaAccountsController.d.ts +64 -5
  136. package/lib/types/client/QaSessionController.d.ts +24 -2
  137. package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
  138. package/lib/types/client/admin/routes.d.ts +2 -0
  139. package/lib/types/client/attachments.d.ts +21 -4
  140. package/lib/types/client/lineage.d.ts +42 -0
  141. package/lib/types/client/settings/sections/notes.d.ts +5 -3
  142. package/lib/types/client/types.d.ts +71 -1
  143. package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
  144. package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
  145. package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
  146. package/lib/types/client/user-settings/copy.d.ts +10 -1
  147. package/lib/types/config-resolvers/integration.d.ts +47 -0
  148. package/lib/types/config.d.ts +10 -0
  149. package/lib/types/index.d.ts +73 -1
  150. package/lib/types/integration/answer.d.ts +65 -0
  151. package/lib/types/integration/attachments.d.ts +85 -0
  152. package/lib/types/integration/contract.d.ts +196 -0
  153. package/lib/types/integration/host-runner.d.ts +44 -0
  154. package/lib/types/integration/http.d.ts +79 -0
  155. package/lib/types/integration/service.d.ts +120 -0
  156. package/lib/types/integration/transcript-reader.d.ts +100 -0
  157. package/lib/types/integration/transcript.d.ts +59 -0
  158. package/lib/types/personal-skills/index.d.ts +2 -2
  159. package/lib/types/personal-skills/paths.d.ts +15 -3
  160. package/lib/types/personal-skills/provider.d.ts +14 -2
  161. package/lib/types/personal-skills/service.d.ts +89 -14
  162. package/lib/types/prompt-notes.d.ts +28 -0
  163. package/lib/types/provenance/document-preview.d.ts +4 -0
  164. package/lib/types/qa-tools/catalog.d.ts +1 -1
  165. package/lib/types/qa-tools/docs-tools.d.ts +160 -0
  166. package/lib/types/secure-session.d.ts +52 -1
  167. package/lib/types/shared/integration-tokens.d.ts +24 -0
  168. package/lib/types/types.d.ts +170 -2
  169. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
3
  import type { QaTurnSources } from "./provenance/types.js";
4
- import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaApprovalDecision, QaClaimResult, QaDocumentPreview, QaLockdownProof, QaOwnershipEntry, QaPendingApproval, QaPendingQuestion, QaQuestionAnswerItem, QaSurfaceConfig, ResolvedQaSurfaceConfig, QaSkillDocument, QaSkillDraftInput, QaSkillRemoval, QaSkillSummary, QaSkillToolDescriptor, QaSkillValidation, QaSourceFilePreview, QaWhoamiResult, QaWorkspaceFile, QaWorkspaceListing, QaPrincipal, QaAccessAdminSnapshot, QaAdminAuditEvent, QaAdminOverview, QaAdminPage, QaAdminUserDetail, QaAdminUserRow, QaAdminUserUpdate, QaAuditQuery, QaCapabilitySelection, QaConversationDetail, QaConversationDeletion, QaConversationQuery, QaConversationReview, QaConversationReviewInput, QaConversationSummary, QaCurrentAccess, QaFeedbackQuery, QaFeedbackRow, QaMessageFeedback, QaMessageFeedbackInput, QaQualityMetrics, QaReviewQueueItem, QaReviewQueueRow, QaSessionAccess, QaSlashCatalog, QaSlashExecution, QaSlashSubmitAttachment, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, QaUserQuery } from "./types.js";
4
+ import type { QaAccountProfileInput, QaAccountSession, QaAccountStartersInput, QaAccountUserPublic, QaApprovalDecision, QaClaimResult, QaDocumentPreview, QaIssuedServiceToken, QaLockdownProof, QaOwnershipEntry, QaPasswordResetRequest, QaPendingApproval, QaPendingQuestion, QaQuestionAnswerItem, QaServiceTokenCreateInput, QaServiceTokenSummary, QaSurfaceConfig, ResolvedQaSurfaceConfig, QaAdminSkillScope, QaAdminSkillsView, QaSkillDocument, QaSkillDraftInput, QaSkillRemoval, QaSkillSummary, QaSkillToolDescriptor, QaSkillValidation, QaSourceFilePreview, QaWhoamiResult, QaWorkspaceFile, QaWorkspaceListing, QaPrincipal, QaAccessAdminSnapshot, QaAdminAuditEvent, QaAdminOverview, QaAdminPage, QaAdminUserDetail, QaAdminUserRow, QaAdminUserUpdate, QaAuditQuery, QaCapabilitySelection, QaConversationDetail, QaConversationDeletion, QaConversationQuery, QaConversationReview, QaConversationReviewInput, QaConversationSummary, QaCurrentAccess, QaFeedbackQuery, QaFeedbackRow, QaMessageFeedback, QaMessageFeedbackInput, QaQualityMetrics, QaReviewQueueItem, QaReviewQueueRow, QaSessionAccess, QaSlashCatalog, QaSlashExecution, QaSlashSubmitAttachment, QaSkillActivationRecord, QaSkillAssignmentOverride, QaSubrole, QaUserAccess, QaUserQuery } from "./types.js";
5
5
  export declare const name = "qa-surface";
6
6
  export declare const inject: string[];
7
7
  import { QA_SURFACE_SETTINGS_NAMESPACE } from "./shared/settings.js";
@@ -51,6 +51,14 @@ export declare class QaSurface extends TypertRemoteService {
51
51
  private webServer;
52
52
  private disposeRoute;
53
53
  private routeKey;
54
+ /**
55
+ * The HTTP API an external application asks questions through. Constructed
56
+ * always — it is cheap and stateless until a request arrives — and exposed
57
+ * on the network only while the deployment turns it on.
58
+ */
59
+ private readonly integration;
60
+ private disposeIntegrationRoutes;
61
+ private integrationRouteKey;
54
62
  /**
55
63
  * The quality store opens its file on first use, so a deployment that never
56
64
  * opens the admin console never grows one.
@@ -108,6 +116,15 @@ export declare class QaSurface extends TypertRemoteService {
108
116
  accountsRegister(email: string, password: string): QaAccountSession;
109
117
  accountsLogin(email: string, password: string): QaAccountSession;
110
118
  /** Identity probe; safe to call with an empty or expired token. */
119
+ accountsChangePassword(token: string, currentPassword: string, nextPassword: string): QaAccountSession;
120
+ /**
121
+ * The sign-in screen's "забыли пароль?" path. It answers the same way for
122
+ * every address: whether an account exists is not something this endpoint
123
+ * tells a caller.
124
+ */
125
+ accountsRequestPasswordReset(email: string): {
126
+ readonly accepted: true;
127
+ };
111
128
  accountsWhoami(token: string): QaWhoamiResult;
112
129
  /**
113
130
  * Migrate a browser's local chat index into server-side ownership. The
@@ -140,6 +157,29 @@ export declare class QaSurface extends TypertRemoteService {
140
157
  * deployment's limits are enforced in the store before anything is written.
141
158
  */
142
159
  accountsUpdateStarters(token: string, input: QaAccountStartersInput): QaAccountUserPublic;
160
+ /**
161
+ * The caller's own integration tokens. The list is the account's own: the
162
+ * token names it, and a secret is never part of a summary, so reading this
163
+ * cannot repeat a credential that was already handed over.
164
+ */
165
+ accountsListServiceTokens(token: string): {
166
+ readonly tokens: readonly QaServiceTokenSummary[];
167
+ };
168
+ /**
169
+ * Mint one integration token for the caller. The answer carries the
170
+ * plaintext exactly once — nothing stores it, so nothing can repeat it — and
171
+ * the account is always the authenticated one: there is no owner field on
172
+ * the wire.
173
+ */
174
+ accountsCreateServiceToken(token: string, input: QaServiceTokenCreateInput): QaIssuedServiceToken;
175
+ /**
176
+ * Revoke one of the caller's own integration tokens. This is the immediate
177
+ * kill switch the profile page offers: the next request that presents the
178
+ * credential is refused, without waiting for its expiry.
179
+ */
180
+ accountsRevokeServiceToken(token: string, tokenId: string): {
181
+ readonly revoked: boolean;
182
+ };
143
183
  /**
144
184
  * Serve the effective QA configuration to the browser. The DSH gateway pins
145
185
  * settings RPCs to loopback, so a browser served over the LAN always sees
@@ -171,12 +211,35 @@ export declare class QaSurface extends TypertRemoteService {
171
211
  accessUpdateSkillOverride(token: string, input: QaSkillAssignmentOverride): readonly QaSkillAssignmentOverride[];
172
212
  /** What each skill activation actually granted in this session. */
173
213
  accessSkillActivations(token: string, sessionId: string): readonly QaSkillActivationRecord[];
214
+ /**
215
+ * The skill catalog of one scope: the deployment's shared skills, or the
216
+ * personal skills of the account the scope names. Reading another account's
217
+ * skills is an administrative act, so it needs `skills.manage`.
218
+ */
219
+ adminSkills(token: string, scope: QaAdminSkillScope): Promise<QaAdminSkillsView>;
220
+ /** One skill file with the body, revision and administrator mark. */
221
+ adminSkill(token: string, scope: QaAdminSkillScope, name: string): Promise<QaSkillDocument>;
222
+ /**
223
+ * Create (`name` null) or replace one skill in place. The write is recorded
224
+ * as this administrator's, and the owner sees that it was.
225
+ */
226
+ adminSkillSave(token: string, scope: QaAdminSkillScope, name: string | null, input: QaSkillDraftInput): Promise<QaSkillDocument>;
227
+ /** Remove one skill into the trash beside its own skills root. */
228
+ adminSkillDelete(token: string, scope: QaAdminSkillScope, name: string, expectedRevision: string | null): Promise<QaSkillRemoval>;
229
+ /** The file a save would write, plus every diagnostic for the draft. */
230
+ adminSkillValidate(token: string, scope: QaAdminSkillScope, name: string | null, input: QaSkillDraftInput): Promise<QaSkillValidation>;
231
+ /** The tool catalog the picker offers for one scope. */
232
+ adminSkillTools(token: string, scope: QaAdminSkillScope): Promise<{
233
+ readonly tools: readonly QaSkillToolDescriptor[];
234
+ }>;
174
235
  /** Counters, attention lines and the newest quality signals. */
175
236
  adminOverview(token: string): Promise<QaAdminOverview>;
176
237
  adminUsers(token: string, query: QaUserQuery, cursor: string | null, limit: number | null): Promise<QaAdminPage<QaAdminUserRow>>;
177
238
  adminUser(token: string, userId: string): Promise<QaAdminUserDetail>;
178
239
  /** Authorization role, status and subrole assignment of one account. */
179
240
  adminUpdateUser(token: string, userId: string, update: QaAdminUserUpdate): Promise<QaAdminUserDetail>;
241
+ adminPasswordResetRequests(token: string): Promise<readonly QaPasswordResetRequest[]>;
242
+ adminResetPassword(token: string, userId: string, password: string): Promise<QaAdminUserDetail>;
180
243
  adminConversations(token: string, query: QaConversationQuery, cursor: string | null, limit: number | null): Promise<QaAdminPage<QaConversationSummary>>;
181
244
  /** One conversation with its messages, tool calls and review state. */
182
245
  adminConversation(token: string, conversationId: string): Promise<QaConversationDetail>;
@@ -322,6 +385,15 @@ export declare class QaSurface extends TypertRemoteService {
322
385
  * the transcript row is read from.
323
386
  */
324
387
  slashExecute(token: string, sessionId: string, line: string, attachments: readonly QaSlashSubmitAttachment[]): Promise<QaSlashExecution>;
388
+ /**
389
+ * Register (or drop) the integration API's routes for the current config.
390
+ *
391
+ * The key is the whole registration decision — whether the API is on and
392
+ * where it lives — so a config change that does not move the endpoints
393
+ * leaves the live routes alone, and turning the API off disposes them
394
+ * immediately rather than at the next restart.
395
+ */
396
+ private refreshIntegrationRoutes;
325
397
  private refreshRoute;
326
398
  /**
327
399
  * A deployment that set `lockdown.allowSlashCommands: true` before this
@@ -0,0 +1,65 @@
1
+ import type { StoredSessionEvent } from "../admin/conversation-log.js";
2
+ /**
3
+ * Reading one answered turn out of a session log.
4
+ *
5
+ * The log is the durable record of the conversation, and it is what the
6
+ * integration API publishes: the live assistant stream is a presentation
7
+ * channel that can be abandoned mid-attempt, while `assistant/message` events
8
+ * are what was actually committed. A turn with tools in it commits several
9
+ * assistant messages — the intermediate steps carry tool calls and usually no
10
+ * prose — so the answer is the *last one that has text*, not simply the last
11
+ * one written. If a later empty message follows a text one, publishing the
12
+ * empty message would answer the bridge with nothing.
13
+ */
14
+ /** The answer of one settled turn, plus how the turn ended. */
15
+ export interface QaTurnAnswer {
16
+ /** The publishable text; empty when the turn committed no prose at all. */
17
+ readonly answer: string;
18
+ /** True when the last committed assistant message was interrupted. */
19
+ readonly interrupted: boolean;
20
+ /** Durable seq of the message the answer came from, when there was one. */
21
+ readonly seq: number | null;
22
+ }
23
+ /** The empty answer: what a turn that committed nothing readable looks like. */
24
+ export declare const QA_EMPTY_ANSWER: QaTurnAnswer;
25
+ /**
26
+ * Project the answer of the turn that starts after `afterSeq`.
27
+ *
28
+ * Only events after the caller's own prompt are read: a chat is continued
29
+ * across questions, and picking the last assistant message of the whole log
30
+ * would hand question #2 the answer to question #1 whenever the new turn
31
+ * committed nothing.
32
+ *
33
+ * @param events - durable events, in any order.
34
+ * @param afterSeq - the seq of the prompt whose answer is wanted.
35
+ * @returns the publishable answer and whether the turn was interrupted.
36
+ */
37
+ export declare function answerAfter(events: readonly StoredSessionEvent[], afterSeq: number): QaTurnAnswer;
38
+ /**
39
+ * Cut an answer down to the size the caller publishes.
40
+ *
41
+ * The bridge posts the answer into a ticket comment, which has a size budget
42
+ * of its own: an over-long comment is not rejected there, it is cut — and a cut
43
+ * that lands in the middle of a sentence, or worse inside a code block, reads
44
+ * like a broken answer. The cut therefore prefers the last paragraph break,
45
+ * then the last line, then the last sentence, and only then an arbitrary word
46
+ * boundary, and it appends an ellipsis so the reader sees that text is missing
47
+ * rather than finished. A boundary that would leave less than half the budget
48
+ * used is refused: a single early paragraph break must not turn a full answer
49
+ * into one line.
50
+ * @param answer - the settled answer, already trimmed.
51
+ * @param maxCharacters - the publish budget; a non-positive value never cuts.
52
+ * @returns the answer, or its bounded head.
53
+ */
54
+ export declare function boundAnswer(answer: string, maxCharacters: number): string;
55
+ /**
56
+ * The seq of the newest user-authored message in a log, which is the floor
57
+ * `answerAfter` reads from when the caller has just submitted a prompt.
58
+ * Synthetic context (injected notes, skill payloads) is skipped: it is model
59
+ * input recorded as a user message, and one arriving after the answer would
60
+ * otherwise hide it.
61
+ * @param events - durable events, in any order.
62
+ * @returns the seq, or 0 when the log holds no human prompt yet.
63
+ */
64
+ export declare function lastPromptSeq(events: readonly StoredSessionEvent[]): number;
65
+ //# sourceMappingURL=answer.d.ts.map
@@ -0,0 +1,85 @@
1
+ import type { PromptContentPart } from "@deepseek-ai/dsh-api-session-controller";
2
+ import type { DocumentsFace } from "@yadsh/dsh-documents";
3
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
+ import { QaIntegrationAttachmentError, type QaFileAttachment } from "./contract.js";
5
+ /**
6
+ * Bringing a ticket's attachments into the prompt.
7
+ *
8
+ * An image rides the prompt inline, because that is what the harness carries.
9
+ * A file does not: `PromptContentPart` names one as an opaque receipt minted by
10
+ * the harness's own upload service, which an external caller has no session to
11
+ * reach — and that service is what stores the copy the model later reads with
12
+ * its file tools. The bridge's non-image attachments are therefore read *here*
13
+ * and arrive as text: a text file is decoded, a document is extracted through
14
+ * the deployment's own document pipeline (the same runtime, providers and
15
+ * limits the `document_*` tools use), and the result joins the prompt as one
16
+ * more text part.
17
+ *
18
+ * That is a deliberate trade. It costs the fidelity of a format the pipeline
19
+ * cannot read — such a request is refused with the bridge's own fallback signal
20
+ * rather than answered with a question the model cannot see the attachment for.
21
+ * It buys a path that works in every deployment shape: nothing is written into
22
+ * a shared workspace, the staging directory is temporary and removed after the
23
+ * extraction, and no second converter is grown inside this plugin.
24
+ */
25
+ /**
26
+ * Plain-text formats the bridge sends; their bytes *are* the content, so
27
+ * decoding them needs no pipeline and cannot fail on a deployment.
28
+ */
29
+ export declare const QA_INTEGRATION_TEXT_MEDIA_TYPES: readonly string[];
30
+ /**
31
+ * Documents the deployment's pipeline is asked to extract. PDF and the OOXML
32
+ * family are what the bridge's own filter lets through; whether a particular
33
+ * one is readable is the pipeline's answer, not a list kept here.
34
+ */
35
+ export declare const QA_INTEGRATION_DOCUMENT_MEDIA_TYPES: readonly string[];
36
+ /** Total inlined attachment text per question, across every attachment. */
37
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS = 120000;
38
+ /** No single attachment may claim the whole budget. */
39
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS_PER_FILE = 60000;
40
+ /** A file name is a label in the prompt, not a path. */
41
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_NAME = 120;
42
+ /** Whether the bytes are the content: decoding needs only UTF-8. */
43
+ export declare function isIntegrationTextMediaType(mediaType: string): boolean;
44
+ /** Whether the attachment has to go through the document pipeline. */
45
+ export declare function isIntegrationDocumentMediaType(mediaType: string): boolean;
46
+ /**
47
+ * Reduce a caller-supplied file name to the label a prompt can carry.
48
+ *
49
+ * The name arrives from another system and is used as a log field, a heading in
50
+ * the prompt and the file name of one temporary file — so directory parts,
51
+ * traversal, control characters and an unbounded length all have to go before
52
+ * it is any of those. An empty result is named by its media type.
53
+ * @param raw - the name the caller sent.
54
+ * @param mediaType - its media type, for the fallback name.
55
+ * @returns a safe, non-empty label.
56
+ */
57
+ export declare function safeAttachmentName(raw: string, mediaType: string): string;
58
+ /** What the extraction needs from the Host, as a port. */
59
+ export interface QaAttachmentDependencies {
60
+ /** The document pipeline, or undefined on a deployment without it. */
61
+ readonly documents: DocumentsFace | undefined;
62
+ /** The chat the prompt belongs to; the pipeline records artifacts against it. */
63
+ readonly sessionId: string;
64
+ readonly signal: AbortSignal;
65
+ readonly logger: PluginLogger;
66
+ /** Total inlined characters for this question. */
67
+ readonly maxChars?: number;
68
+ }
69
+ /**
70
+ * Turn the request's file attachments into prompt text parts.
71
+ *
72
+ * The parts are appended after the question, in the order the caller sent them,
73
+ * and the question itself is untouched. A file the Host cannot read refuses the
74
+ * whole request: answering with the question but not its attachments would hand
75
+ * the model a question about material it never received, which is exactly what
76
+ * the bridge's 415 fallback exists to avoid.
77
+ *
78
+ * @param files - the non-image attachments of one request.
79
+ * @param deps - pipeline, session and budgets.
80
+ * @returns one text part per readable attachment, plus a note when the budget cut the list.
81
+ */
82
+ export declare function attachmentPromptParts(files: readonly QaFileAttachment[], deps: QaAttachmentDependencies): Promise<readonly PromptContentPart[]>;
83
+ /** Re-exported so the runner does not reach into the contract for one name. */
84
+ export { QaIntegrationAttachmentError };
85
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The integration API's wire contract: what an external application may post to
3
+ * `/qa/api/ask`, what it gets back, and the refusal vocabulary the HTTP layer
4
+ * maps onto status codes.
5
+ *
6
+ * The shape follows the ticket bridge specification the deployment was asked to
7
+ * implement (issue #177): a Markdown `answer`, the `sources` that back it, a
8
+ * `confidence` bucket, an `escalate` flag for "the assistant could not answer,
9
+ * send it to a specialist", and a `reason` for the logs. `chat_id` is the DSH
10
+ * session identity: sending it back as `session_id` continues the same
11
+ * conversation, which is the whole point of the bridge keeping it.
12
+ */
13
+ import type { QaSourceReference } from "../provenance/types.js";
14
+ /** Why one integration request was refused, in the bridge's own vocabulary. */
15
+ export type QaIntegrationReason = "unauthorized" | "forbidden" | "not-found" | "invalid-request" | "unsupported-media" | "payload-too-large" | "rate-limited" | "busy" | "unavailable" | "timeout";
16
+ /**
17
+ * HTTP status per refusal. The mapping is part of the contract, not a detail:
18
+ * the bridge retries on 5xx and 429, falls back to a bodyless JSON request on
19
+ * 415, and treats 401/403 as a configuration error it must not spin on.
20
+ */
21
+ export declare const QA_INTEGRATION_STATUS: Readonly<Record<QaIntegrationReason, number>>;
22
+ /** One refusal, carrying the status the HTTP layer answers with. */
23
+ export declare class QaIntegrationError extends Error {
24
+ readonly reason: QaIntegrationReason;
25
+ constructor(reason: QaIntegrationReason, message: string, options?: {
26
+ readonly cause?: unknown;
27
+ });
28
+ get status(): number;
29
+ }
30
+ /** One image the caller attached, already bounded and decoded to base64. */
31
+ export interface QaInlineImageAttachment {
32
+ readonly kind: "image";
33
+ /** A raster media type; anything else is refused before it gets here. */
34
+ readonly mediaType: string;
35
+ /** base64 bytes, exactly as the harness prompt content carries them. */
36
+ readonly data: string;
37
+ readonly name?: string;
38
+ }
39
+ /**
40
+ * One non-image file the caller attached, still as bytes.
41
+ *
42
+ * Files cannot ride a prompt the way images do: the harness carries a file as
43
+ * an opaque receipt minted by its own upload service, which an external caller
44
+ * has no session to reach. What the bridge sends instead is read on the Host —
45
+ * text files decoded, documents extracted through the deployment's document
46
+ * pipeline — and the resulting text joins the prompt, which is the same
47
+ * material the model would have read out of the stored copy.
48
+ */
49
+ export interface QaFileAttachment {
50
+ readonly kind: "file";
51
+ readonly mediaType: string;
52
+ readonly name: string;
53
+ /** Raw bytes, bounded by the configured per-attachment ceiling. */
54
+ readonly bytes: Buffer;
55
+ }
56
+ /** One attachment of a parsed request, in the order the caller sent it. */
57
+ export type QaIntegrationAttachment = QaInlineImageAttachment | QaFileAttachment;
58
+ /**
59
+ * Why one attachment could not become prompt content, in the vocabulary the
60
+ * service maps onto HTTP.
61
+ *
62
+ * `unsupported` is the bridge's own fallback signal (415): repeat the question
63
+ * without attachments. `unavailable` is the same answer for a document the
64
+ * deployment cannot read right now, and the two are deliberately distinct only
65
+ * in the log — a caller sees one status either way.
66
+ */
67
+ export type QaIntegrationAttachmentReason = "unsupported" | "unavailable";
68
+ /** One attachment the Host could not turn into prompt content. */
69
+ export declare class QaIntegrationAttachmentError extends Error {
70
+ readonly attachment: string;
71
+ readonly reason: QaIntegrationAttachmentReason;
72
+ constructor(attachment: string, reason: QaIntegrationAttachmentReason, message: string);
73
+ }
74
+ /** A parsed `POST /qa/api/ask` body, whichever encoding carried it. */
75
+ export interface QaAskRequest {
76
+ /** The question; at least one non-whitespace character. */
77
+ readonly message: string;
78
+ /** Product version the bridge extracted from the ticket, when it knew one. */
79
+ readonly version: string | null;
80
+ /** The chat to continue, or null for a new one. */
81
+ readonly sessionId: string | null;
82
+ /** Ticket metadata the operator sees in the log line. */
83
+ readonly context: QaAskContext;
84
+ readonly attachments: readonly QaIntegrationAttachment[];
85
+ }
86
+ /** The ticket metadata the bridge sends along; all of it is optional. */
87
+ export interface QaAskContext {
88
+ readonly ticketKey: string | null;
89
+ readonly reporter: string | null;
90
+ readonly reporterName: string | null;
91
+ }
92
+ /** A successful answer, in the exact shape the bridge publishes. */
93
+ export interface QaAskAnswer {
94
+ readonly chatId: string;
95
+ readonly answer: string;
96
+ readonly sources: readonly string[];
97
+ readonly confidence: "high" | "medium" | "low";
98
+ /** True when the assistant produced nothing worth publishing. */
99
+ readonly escalate: boolean;
100
+ /** Why it escalated — for the bridge's log, never for a client. */
101
+ readonly reason: string;
102
+ }
103
+ /**
104
+ * One message of a conversation as an external caller reads it.
105
+ *
106
+ * The text is the same flattening the answer itself uses (prose only, with
107
+ * attachments named), so a caller that keeps asking questions and a caller
108
+ * that reads back the transcript never see two versions of one message.
109
+ */
110
+ export interface QaIntegrationMessage {
111
+ /** Durable position of the message; the cursor for the next read. */
112
+ readonly seq: number;
113
+ readonly role: "user" | "assistant";
114
+ readonly text: string;
115
+ /** ISO timestamp, when the log recorded one for the event. */
116
+ readonly at?: string;
117
+ }
118
+ /**
119
+ * A conversation as the caller that owns it reads it back.
120
+ *
121
+ * `sessions:read` exists for exactly this: a bridge that escalated a question,
122
+ * or that wants to show the specialist what was already said, reads the chat
123
+ * it started without asking it again. The read is bounded from both ends —
124
+ * `after` is the caller's own cursor, the newest `limit` messages are the
125
+ * window — so a long conversation costs one page, not one transcript per call.
126
+ */
127
+ export interface QaIntegrationTranscript {
128
+ readonly chatId: string;
129
+ /** The window, oldest first. */
130
+ readonly messages: readonly QaIntegrationMessage[];
131
+ /** Newest seq the caller has now seen; 0 when the chat holds no messages. */
132
+ readonly lastSeq: number;
133
+ /** True when older messages exist below the window. */
134
+ readonly truncated: boolean;
135
+ }
136
+ /** `GET /qa/api/health` payload; the bridge only needs `ok`. */
137
+ export interface QaHealth {
138
+ readonly ok: boolean;
139
+ readonly version: string;
140
+ readonly models: readonly string[];
141
+ readonly uptimeS: number;
142
+ }
143
+ /**
144
+ * What one answered turn hands back to the service: the answer text, the
145
+ * sources that back it and whether the turn was cut short. The runner owns the
146
+ * session mechanics; nothing about sessions leaks into the wire contract.
147
+ */
148
+ export interface QaIntegrationTurn {
149
+ readonly chatId: string;
150
+ readonly answer: string;
151
+ readonly sources: readonly QaSourceReference[];
152
+ /** True when the turn was cancelled or the log recorded an interruption. */
153
+ readonly interrupted: boolean;
154
+ }
155
+ /** The one host capability the integration service needs, as a port. */
156
+ export interface QaIntegrationRunner {
157
+ /**
158
+ * Send one question into a chat and resolve once its turn has settled.
159
+ *
160
+ * Implementations must enforce that `userId` may use `chatId` — the service
161
+ * has already checked the credential, and whoever owns session creation owns
162
+ * the ownership boundary that goes with it.
163
+ */
164
+ run(input: {
165
+ readonly userId: string;
166
+ /** The chat to continue, or null for a new conversation. */
167
+ readonly chatId: string | null;
168
+ readonly message: string;
169
+ readonly attachments: readonly QaIntegrationAttachment[];
170
+ readonly ticketKey: string | null;
171
+ readonly signal: AbortSignal;
172
+ /**
173
+ * Called as soon as the chat exists, before the turn settles. The service
174
+ * needs the identity of a chat whose answer it may never see: a timed-out
175
+ * question still has to hand the bridge a chat it can continue.
176
+ */
177
+ readonly onChat?: (chatId: string) => void;
178
+ }): Promise<QaIntegrationTurn>;
179
+ /**
180
+ * Read back the messages of one conversation.
181
+ *
182
+ * The service has already authenticated the credential and established that
183
+ * the chat belongs to that account; the implementation reads the log and
184
+ * projects it, and reports an unreadable log rather than an empty answer.
185
+ */
186
+ transcript(input: {
187
+ readonly chatId: string;
188
+ /** Exclusive lower bound: only messages after this seq are returned. */
189
+ readonly after: number;
190
+ /** Newest messages to return; older ones are reported as truncated. */
191
+ readonly limit: number;
192
+ }): Promise<QaIntegrationTranscript>;
193
+ /** Model ids the deployment can route right now, for the health payload. */
194
+ models(): Promise<readonly string[]>;
195
+ }
196
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,44 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
3
+ import type { QaAccessService } from "../access/service.js";
4
+ import type { QaAccounts } from "../accounts/store.js";
5
+ import type { QaSessionLogReader } from "../admin/session-log.js";
6
+ import type { QaProvenanceHost } from "../provenance/host-store.js";
7
+ import type { QaPolicyAdmission } from "../secure-session.js";
8
+ import type { ResolvedQaSurfaceConfig } from "../types.js";
9
+ import type { DocumentsFace } from "@yadsh/dsh-documents";
10
+ import type { QaIntegrationRunner } from "./contract.js";
11
+ /**
12
+ * The session half of the integration API: open a chat, send one question,
13
+ * wait for the turn, read the answer.
14
+ *
15
+ * It is deliberately not a new turn engine. Opening a chat goes through the
16
+ * same three steps the browser's `createSession` takes — the deployment
17
+ * preflight, the per-user workspace, then `secureSession` — because that last
18
+ * call is what pins the QA tool policy, attaches the QA catalog and records
19
+ * the chat as attested. A question is admitted with
20
+ * `sessionController.prompt`, the same call the browser makes; the wait is
21
+ * `agent.whenIdle()`, which is the harness's own definition of "this turn is
22
+ * over"; the answer is read back from the durable log rather than from the
23
+ * live assistant stream, so what the bridge publishes is what was committed.
24
+ */
25
+ export interface QaIntegrationRunnerOptions {
26
+ ctx: Context;
27
+ getConfig(): ResolvedQaSurfaceConfig;
28
+ /** The accounts store, or undefined while accounts are disabled. */
29
+ accounts(): QaAccounts | undefined;
30
+ admission: QaPolicyAdmission;
31
+ access: QaAccessService;
32
+ sessionLog: QaSessionLogReader;
33
+ provenance: QaProvenanceHost;
34
+ /**
35
+ * The deployment's document pipeline, resolved per call so a provider that
36
+ * installs later still counts. Absent on a deployment that has none, where
37
+ * document attachments are refused with the caller's own fallback signal.
38
+ */
39
+ documents(): DocumentsFace | undefined;
40
+ logger: PluginLogger;
41
+ }
42
+ /** Build the runner for one plugin instance. */
43
+ export declare function createQaIntegrationRunner(options: QaIntegrationRunnerOptions): QaIntegrationRunner;
44
+ //# sourceMappingURL=host-runner.d.ts.map
@@ -0,0 +1,79 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { WebServer } from "@deepseek-ai/dsh-host-webserver";
3
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
+ import type { ResolvedQaSurfaceConfig } from "../types.js";
5
+ import { type QaAskRequest } from "./contract.js";
6
+ import type { QaIntegrationService } from "./service.js";
7
+ /** The bridge's own ceiling: five attachments per question. */
8
+ export declare const QA_INTEGRATION_MAX_FILES = 5;
9
+ export interface QaIntegrationRouteOptions {
10
+ config: ResolvedQaSurfaceConfig;
11
+ service: QaIntegrationService;
12
+ logger: PluginLogger;
13
+ }
14
+ /**
15
+ * Register the integration routes on the host web server.
16
+ * @param webServer - the route table owner.
17
+ * @param options - resolved config, the service and the plugin logger.
18
+ * @returns the disposer removing every route this call registered.
19
+ */
20
+ export declare function registerQaIntegrationRoutes(webServer: Pick<WebServer, "register">, options: QaIntegrationRouteOptions): () => void;
21
+ /** The page size a caller gets when it does not ask for one. */
22
+ export declare const QA_INTEGRATION_DEFAULT_TRANSCRIPT_LIMIT = 50;
23
+ /**
24
+ * Parse one `POST /ask` body out of whichever encoding carried it.
25
+ *
26
+ * JSON is the primary form. Multipart exists because the bridge sends the
27
+ * ticket's attachments through it, and images ride a prompt inline in this
28
+ * harness — so an image-only multipart request is answered like a JSON one. A
29
+ * non-image attachment has nowhere to go: files reach a prompt as a receipt
30
+ * minted by the browser upload path, which an external caller has no session
31
+ * to mint. That request is refused with 415, which is exactly the signal the
32
+ * bridge's own fallback waits for: it repeats the question without
33
+ * attachments rather than failing.
34
+ *
35
+ * @param body - the raw request body.
36
+ * @param contentType - the request's content type, parameters included.
37
+ * @param config - resolved config, for the attachment ceilings.
38
+ * @returns the normalized request.
39
+ */
40
+ export declare function parseAskBody(body: Buffer, contentType: string | undefined, config: ResolvedQaSurfaceConfig): QaAskRequest;
41
+ /** One image part of a multipart body, bounded as it was read. */
42
+ interface MultipartFile {
43
+ readonly filename: string;
44
+ readonly mediaType: string;
45
+ readonly bytes: Buffer;
46
+ }
47
+ interface MultipartBody {
48
+ readonly fields: ReadonlyMap<string, string>;
49
+ readonly files: readonly MultipartFile[];
50
+ }
51
+ /**
52
+ * Split a multipart body into its text fields and files.
53
+ *
54
+ * Hand-written and deliberately minimal: one request shape (a flat form of
55
+ * scalar fields and `files` parts), one boundary, no nested multiparts. The
56
+ * body is already bounded by the caller, so no part can grow without limit.
57
+ *
58
+ * @param body - the raw request body.
59
+ * @param boundary - the boundary parameter of the content type.
60
+ * @returns text fields and file parts, in the order they appeared.
61
+ */
62
+ export declare function parseMultipart(body: Buffer, boundary: string): MultipartBody;
63
+ /**
64
+ * Read the whole request body under a byte ceiling.
65
+ * @param request - the incoming request.
66
+ * @param limit - accepted body size in bytes.
67
+ * @returns the body.
68
+ */
69
+ export declare function readBody(request: IncomingMessage, limit: number): Promise<Buffer>;
70
+ /**
71
+ * One refusal, mapped onto its status.
72
+ *
73
+ * An unexpected error is answered as `unavailable` rather than leaking a stack
74
+ * trace: the bridge retries a 5xx, and the deployment's own log holds the
75
+ * detail.
76
+ */
77
+ export declare function sendRefusal(response: ServerResponse, error: unknown): void;
78
+ export {};
79
+ //# sourceMappingURL=http.d.ts.map