@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.
- package/README.md +310 -48
- package/lib/access/service.js +49 -10
- package/lib/access/service.js.map +1 -1
- package/lib/account-remotes.js +41 -0
- package/lib/account-remotes.js.map +1 -1
- package/lib/accounts/database.js +140 -0
- package/lib/accounts/database.js.map +1 -1
- package/lib/accounts/errors.js.map +1 -1
- package/lib/accounts/service-token.js +103 -0
- package/lib/accounts/service-token.js.map +1 -0
- package/lib/accounts/store.js +291 -5
- package/lib/accounts/store.js.map +1 -1
- package/lib/admin/conversation-log.js +14 -8
- package/lib/admin/conversation-log.js.map +1 -1
- package/lib/admin/permissions.js +5 -0
- package/lib/admin/permissions.js.map +1 -1
- package/lib/admin/quality-store.js +4 -0
- package/lib/admin/quality-store.js.map +1 -1
- package/lib/admin/service.js +173 -0
- package/lib/admin/service.js.map +1 -1
- package/lib/admin/session-log.js +39 -8
- package/lib/admin/session-log.js.map +1 -1
- package/lib/attachment-rules.js +39 -6
- package/lib/attachment-rules.js.map +1 -1
- package/lib/cli.js +83 -1
- package/lib/cli.js.map +1 -1
- package/lib/client/QaAccountsController.js +213 -4
- package/lib/client/QaAccountsController.js.map +1 -1
- package/lib/client/QaSessionController.js +37 -21
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +28 -4
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/admin/QaAdmin.js +9 -1
- package/lib/client/admin/QaAdmin.js.map +1 -1
- package/lib/client/admin/pages/SkillFiles.js +78 -0
- package/lib/client/admin/pages/SkillFiles.js.map +1 -0
- package/lib/client/admin/pages/Users.js +65 -1
- package/lib/client/admin/pages/Users.js.map +1 -1
- package/lib/client/admin/routes.js +8 -1
- package/lib/client/admin/routes.js.map +1 -1
- package/lib/client/admin/styles.js +2 -0
- package/lib/client/admin/styles.js.map +1 -1
- package/lib/client/attachments.js +41 -11
- package/lib/client/attachments.js.map +1 -1
- package/lib/client/components/QaAuthGate.js +19 -10
- package/lib/client/components/QaAuthGate.js.map +1 -1
- package/lib/client/components/QaChangelog.js +43 -0
- package/lib/client/components/QaChangelog.js.map +1 -1
- package/lib/client/index.js +13 -0
- package/lib/client/index.js.map +1 -1
- package/lib/client/lineage.js +64 -0
- package/lib/client/lineage.js.map +1 -1
- package/lib/client/settings/sections/attachments.js +6 -5
- package/lib/client/settings/sections/attachments.js.map +1 -1
- package/lib/client/settings/sections/notes.js +22 -4
- package/lib/client/settings/sections/notes.js.map +1 -1
- package/lib/client/styles.js +9 -0
- package/lib/client/styles.js.map +1 -1
- package/lib/client/types.js.map +1 -1
- package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
- package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
- package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
- package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
- package/lib/client/user-settings/SkillCatalog.js +2 -2
- package/lib/client/user-settings/SkillCatalog.js.map +1 -1
- package/lib/client/user-settings/UserSettingsDialog.js +17 -2
- package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
- package/lib/client/user-settings/copy.js +20 -0
- package/lib/client/user-settings/copy.js.map +1 -1
- package/lib/client.js +2480 -137
- package/lib/client.js.map +1 -1
- package/lib/config-resolvers/attachments.js +6 -5
- package/lib/config-resolvers/attachments.js.map +1 -1
- package/lib/config-resolvers/defaults.js +23 -2
- package/lib/config-resolvers/defaults.js.map +1 -1
- package/lib/config-resolvers/integration.js +94 -0
- package/lib/config-resolvers/integration.js.map +1 -0
- package/lib/config-resolvers/notes.js +2 -0
- package/lib/config-resolvers/notes.js.map +1 -1
- package/lib/config.js +72 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +197 -2
- package/lib/index.js.map +1 -1
- package/lib/integration/answer.js +100 -0
- package/lib/integration/answer.js.map +1 -0
- package/lib/integration/attachments.js +246 -0
- package/lib/integration/attachments.js.map +1 -0
- package/lib/integration/contract.js +53 -0
- package/lib/integration/contract.js.map +1 -0
- package/lib/integration/host-runner.js +243 -0
- package/lib/integration/host-runner.js.map +1 -0
- package/lib/integration/http.js +576 -0
- package/lib/integration/http.js.map +1 -0
- package/lib/integration/service.js +373 -0
- package/lib/integration/service.js.map +1 -0
- package/lib/integration/transcript-reader.js +202 -0
- package/lib/integration/transcript-reader.js.map +1 -0
- package/lib/integration/transcript.js +148 -0
- package/lib/integration/transcript.js.map +1 -0
- package/lib/personal-skills/index.js +2 -2
- package/lib/personal-skills/index.js.map +1 -1
- package/lib/personal-skills/paths.js +14 -8
- package/lib/personal-skills/paths.js.map +1 -1
- package/lib/personal-skills/provider.js +16 -4
- package/lib/personal-skills/provider.js.map +1 -1
- package/lib/personal-skills/service.js +240 -30
- package/lib/personal-skills/service.js.map +1 -1
- package/lib/prompt-notes.js +58 -0
- package/lib/prompt-notes.js.map +1 -1
- package/lib/provenance/document-preview.js +10 -1
- package/lib/provenance/document-preview.js.map +1 -1
- package/lib/qa-tools/catalog.js +12 -1
- package/lib/qa-tools/catalog.js.map +1 -1
- package/lib/qa-tools/docs-tools.js +563 -0
- package/lib/qa-tools/docs-tools.js.map +1 -0
- package/lib/resolve-config.js +4 -0
- package/lib/resolve-config.js.map +1 -1
- package/lib/secure-session.js +83 -3
- package/lib/secure-session.js.map +1 -1
- package/lib/shared/integration-tokens.js +23 -0
- package/lib/shared/integration-tokens.js.map +1 -0
- package/lib/typert.host.js +1176 -213
- package/lib/typert.remote-client.d.ts +27 -1
- package/lib/typert.remote-client.js +911 -69
- package/lib/types/access/service.d.ts +33 -2
- package/lib/types/account-remotes.d.ts +30 -1
- package/lib/types/accounts/database.d.ts +53 -0
- package/lib/types/accounts/errors.d.ts +5 -1
- package/lib/types/accounts/service-token.d.ts +70 -0
- package/lib/types/accounts/store.d.ts +131 -1
- package/lib/types/admin/conversation-log.d.ts +12 -0
- package/lib/types/admin/service.d.ts +56 -1
- package/lib/types/admin/session-log.d.ts +21 -0
- package/lib/types/attachment-rules.d.ts +26 -3
- package/lib/types/client/QaAccountsController.d.ts +64 -5
- package/lib/types/client/QaSessionController.d.ts +24 -2
- package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
- package/lib/types/client/admin/routes.d.ts +2 -0
- package/lib/types/client/attachments.d.ts +21 -4
- package/lib/types/client/lineage.d.ts +42 -0
- package/lib/types/client/settings/sections/notes.d.ts +5 -3
- package/lib/types/client/types.d.ts +71 -1
- package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
- package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
- package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
- package/lib/types/client/user-settings/copy.d.ts +10 -1
- package/lib/types/config-resolvers/integration.d.ts +47 -0
- package/lib/types/config.d.ts +10 -0
- package/lib/types/index.d.ts +73 -1
- package/lib/types/integration/answer.d.ts +65 -0
- package/lib/types/integration/attachments.d.ts +85 -0
- package/lib/types/integration/contract.d.ts +196 -0
- package/lib/types/integration/host-runner.d.ts +44 -0
- package/lib/types/integration/http.d.ts +79 -0
- package/lib/types/integration/service.d.ts +120 -0
- package/lib/types/integration/transcript-reader.d.ts +100 -0
- package/lib/types/integration/transcript.d.ts +59 -0
- package/lib/types/personal-skills/index.d.ts +2 -2
- package/lib/types/personal-skills/paths.d.ts +15 -3
- package/lib/types/personal-skills/provider.d.ts +14 -2
- package/lib/types/personal-skills/service.d.ts +89 -14
- package/lib/types/prompt-notes.d.ts +28 -0
- package/lib/types/provenance/document-preview.d.ts +4 -0
- package/lib/types/qa-tools/catalog.d.ts +1 -1
- package/lib/types/qa-tools/docs-tools.d.ts +160 -0
- package/lib/types/secure-session.d.ts +52 -1
- package/lib/types/shared/integration-tokens.d.ts +24 -0
- package/lib/types/types.d.ts +170 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Session and Agent Loop.
|
|
|
52
52
|
(list/add/set-password/set-role/disable/revoke), and a coarse honest boundary:
|
|
53
53
|
accounts identify QA users, they do not fence the harness root;
|
|
54
54
|
- gives each account a `Настройки` dialog — profile, starter messages,
|
|
55
|
-
general, and **personal skills**: ordinary Agent Skills stored as `SKILL.md` in the account's own
|
|
55
|
+
integration tokens, general, and **personal skills**: ordinary Agent Skills stored as `SKILL.md` in the account's own
|
|
56
56
|
directory (`accounts.skills`), edited with a catalog, an invocation-flag
|
|
57
57
|
form, a Markdown body, a tool picker over the deployment's registry, and a
|
|
58
58
|
preview of the exact file a save writes. Skills reach the model through a
|
|
@@ -269,6 +269,10 @@ config:
|
|
|
269
269
|
# Presets whose sessions may unlock the catalog. Empty leaves the gate open:
|
|
270
270
|
# only do that when this Host serves one agent composition.
|
|
271
271
|
activationPresets: []
|
|
272
|
+
# Where the documentation readers look. Empty reads `docs/` inside each
|
|
273
|
+
# chat's own workspace; name the corpus when it is published once, outside
|
|
274
|
+
# every per-user workspace (see "Dynamic QA tools" below).
|
|
275
|
+
docsRoot: ""
|
|
272
276
|
```
|
|
273
277
|
|
|
274
278
|
`workspaceId` is recommended for a deterministic assistant. Without it, DSH
|
|
@@ -285,12 +289,13 @@ the plugin's catalog is registered into the agent's own scope only after the
|
|
|
285
289
|
model successfully loads `tools.activationSkill`, and `qa_tools_selfcheck`
|
|
286
290
|
reports the resulting state.
|
|
287
291
|
|
|
288
|
-
As of catalog version
|
|
289
|
-
`qa_tools_selfcheck`, the activation diagnostic
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
As of catalog version 3 the shipped catalog carries four tools:
|
|
293
|
+
`qa_tools_selfcheck`, the activation diagnostic; `docs_search` and
|
|
294
|
+
`docs_read`, the documentation surface; and `file_delete`, the one destructive
|
|
295
|
+
capability — it deletes a single regular file strictly inside the calling
|
|
296
|
+
chat's workspace and refuses directories, missing paths and anything that
|
|
297
|
+
escapes the root, symlink escapes included, with an explicit reason that never
|
|
298
|
+
echoes a host path. Every `file_delete` call is answered `ask` by an
|
|
294
299
|
inner gate that sits inside the approval flow, so on a deployment with
|
|
295
300
|
`interaction.approvals: interactive` the interactive approval card parks the
|
|
296
301
|
call for the operator, and on `blocked` the call is refused outright: nothing
|
|
@@ -299,6 +304,22 @@ dynamic name at execution time — it needs no `lockdown.toolPolicy` entry —
|
|
|
299
304
|
and a role-managed deployment grants it through the same Tools baskets as any
|
|
300
305
|
other tool.
|
|
301
306
|
|
|
307
|
+
`docs_search` and `docs_read` are the documentation surface. Documentation is
|
|
308
|
+
published into the `docs/` directory of the chat's workspace and the tools read
|
|
309
|
+
exactly that tree: `docs_search` matches a phrase inside single lines and
|
|
310
|
+
reports every hit with its path and line number, tagged with the module and
|
|
311
|
+
version parsed out of the layout `docs/<module>/<version>/…`; both names are
|
|
312
|
+
also accepted as filters, so a chat that was told "3.8" stops sweeping every
|
|
313
|
+
edition, and `path` narrows a search to one subtree. `docs_read` opens one file
|
|
314
|
+
at a bounded window of lines. Both stay inside the tree — a path outside it, a
|
|
315
|
+
`docs/` that is missing or is not a real directory, a directory passed to a
|
|
316
|
+
read, a binary file and a link that leaves the tree are refused with an
|
|
317
|
+
explicit reason that never echoes a host path — and both bound what they
|
|
318
|
+
return: `limit` and a byte budget on the reported hits, a line budget on a
|
|
319
|
+
read, and a truncated answer says so instead of quietly dropping matches. The
|
|
320
|
+
tool descriptions carry the routing rule the catalog exists for: documentation
|
|
321
|
+
is looked up here, not in memory and not by sweeping guessed paths.
|
|
322
|
+
|
|
302
323
|
The trigger is the authoritative result of the built-in `skill` tool, not the
|
|
303
324
|
model's attempt, not a keyword in the transcript, and not a coincidentally
|
|
304
325
|
matching skill description. A failed or refused load activates nothing, loading
|
|
@@ -322,6 +343,17 @@ name. `tools.dynamicActivation: false` restores the always-on behaviour and
|
|
|
322
343
|
attaches the catalog to every managed agent at creation — useful for a
|
|
323
344
|
deployment that would rather debug the tool surface than the trigger.
|
|
324
345
|
|
|
346
|
+
`docs_search` and `docs_read` read the `docs/` directory of the calling chat's
|
|
347
|
+
own workspace by default. A deployment that publishes the corpus **once** — for
|
|
348
|
+
example `/workspace/docs` next to a per-user workspace layout — sets
|
|
349
|
+
`tools.docsRoot` to that absolute path, because with
|
|
350
|
+
`accounts.perUserWorkspace: true` the chat's workspace is a per-account
|
|
351
|
+
directory that holds no documentation: the readers would answer "there is no
|
|
352
|
+
documentation tree here" in every chat, while the `glob`/`grep`/`read` tools
|
|
353
|
+
reached the corpus by absolute path all along. The root is held to the same
|
|
354
|
+
rules either way: an absolute path, a real directory, no symlinks, and every
|
|
355
|
+
path is canonicalized and refused if it leaves the tree.
|
|
356
|
+
|
|
325
357
|
These tools are not `lockdown.toolPolicy.allow` entries, and cannot be: that
|
|
326
358
|
list is validated against the mounted catalog at attestation time, and a tool
|
|
327
359
|
that only appears later would fail the check. The QA execution guard authorizes
|
|
@@ -462,11 +494,15 @@ in. See
|
|
|
462
494
|
[Configuration](https://github.com/xarleyn/dsh-plugins/blob/main/plugins/dsh-qa-surface/docs/CONFIGURATION.md)
|
|
463
495
|
for the limits and the prompt's exact wording.
|
|
464
496
|
|
|
465
|
-
The wording of that note — and of the source-provenance
|
|
466
|
-
notes — is editable without touching
|
|
467
|
-
of the settings card (the `notes`
|
|
468
|
-
its text, keeping the generated parts
|
|
469
|
-
`{reportTool}`) as placeholders.
|
|
497
|
+
The wording of that note — and of the source-provenance, delegation-naming and
|
|
498
|
+
attached-documents and source-priority notes — is editable without touching
|
|
499
|
+
the source: the «Заметки модели» section of the settings card (the `notes`
|
|
500
|
+
config block) mutes each note and rewords its text, keeping the generated parts
|
|
501
|
+
(`{identity}`, `{instructions}`, `{reportTool}`) as placeholders. The
|
|
502
|
+
attached-documents note is the one that sends a `.docx` or `.pdf` from the chat
|
|
503
|
+
to the document pipeline instead of the plain file reader, which refuses those
|
|
504
|
+
formats as binary. The source-priority note is the one that says an answer
|
|
505
|
+
belongs to the documentation or the expert before it belongs to memory.
|
|
470
506
|
|
|
471
507
|
### Starter messages
|
|
472
508
|
|
|
@@ -481,6 +517,26 @@ is pure UI preference — none of it reaches the agent prompt.
|
|
|
481
517
|
`accounts.starters.enabled` (default `true`) turns the section off for
|
|
482
518
|
deployments that want the buttons to stay operator-defined.
|
|
483
519
|
|
|
520
|
+
### Integration tokens
|
|
521
|
+
|
|
522
|
+
The same `Настройки` dialog carries an «Интеграционные токены» section, so the
|
|
523
|
+
account that runs an integration issues and revokes its own credential instead
|
|
524
|
+
of asking the operator to run the CLI. It lists the tokens that account owns —
|
|
525
|
+
label, scopes, creation and expiry dates, use count and last use — mints a new
|
|
526
|
+
one, shows its secret exactly once (with a copy button and the warning that it
|
|
527
|
+
is never recoverable, because only the digest is stored), and revokes one with
|
|
528
|
+
a second confirming click. A revoked or expired token stays in the list as a
|
|
529
|
+
record, with its revoke button gone.
|
|
530
|
+
|
|
531
|
+
Creating is offered only where the credential has somewhere to go: with
|
|
532
|
+
`integration.enabled: false` the section explains that the API is off and hides
|
|
533
|
+
the form, while listing and revoking keep working — a credential that already
|
|
534
|
+
exists has to remain revocable. The section is part of the accounts domain, so
|
|
535
|
+
it appears wherever `accounts.enabled` is on, and the same self-service rules
|
|
536
|
+
apply: the token always belongs to the signed-in account, and one account never
|
|
537
|
+
sees another's tokens. The CLI (`qa-accounts token create|list|revoke`) remains
|
|
538
|
+
the operator's path, including issuing a token for somebody else.
|
|
539
|
+
|
|
484
540
|
Session policies:
|
|
485
541
|
|
|
486
542
|
- `browser-persistent` restores the id stored in this browser, replacing stale
|
|
@@ -584,13 +640,13 @@ lockdown:
|
|
|
584
640
|
|
|
585
641
|
slashCommands:
|
|
586
642
|
skills:
|
|
587
|
-
mode: allow-list
|
|
643
|
+
mode: allow-list # deny-all | allow-list | all
|
|
588
644
|
allow:
|
|
589
645
|
- generate-tkp
|
|
590
646
|
- generate-tz
|
|
591
647
|
- gap-analysis
|
|
592
648
|
commands:
|
|
593
|
-
mode: deny-all
|
|
649
|
+
mode: deny-all # deny-all | allow-list | all
|
|
594
650
|
allow: []
|
|
595
651
|
palette:
|
|
596
652
|
enabled: true
|
|
@@ -658,42 +714,41 @@ through the `qaSurfacePanels` service; the React body is registered separately
|
|
|
658
714
|
in the keyed `qa.surface.panel` slot under the same implementation id:
|
|
659
715
|
|
|
660
716
|
```ts
|
|
661
|
-
import type { Context } from "@deepseek-ai/cordis"
|
|
662
|
-
import type { PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots"
|
|
663
|
-
import type {} from "@deepseek-ai/dsh-client-ui-renderer/client"
|
|
664
|
-
import { QA_SURFACE_PANEL_SLOT } from "@yadsh/dsh-qa-surface/client/panels"
|
|
717
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
718
|
+
import type { PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
|
|
719
|
+
import type {} from "@deepseek-ai/dsh-client-ui-renderer/client";
|
|
720
|
+
import { QA_SURFACE_PANEL_SLOT } from "@yadsh/dsh-qa-surface/client/panels";
|
|
665
721
|
|
|
666
|
-
const id = "@example/dsh-qa-browser"
|
|
722
|
+
const id = "@example/dsh-qa-browser";
|
|
667
723
|
|
|
668
724
|
function BrowserPanel(props: PropsRuntime<typeof QA_SURFACE_PANEL_SLOT>) {
|
|
669
725
|
// panelId, panelKind, sessionId, visible, presentation, params,
|
|
670
726
|
// actions and a registration-lifetime AbortSignal arrive in props.
|
|
671
|
-
return null
|
|
727
|
+
return null;
|
|
672
728
|
}
|
|
673
729
|
|
|
674
|
-
export const inject = ["slots", "qaSurfacePanels"]
|
|
730
|
+
export const inject = ["slots", "qaSurfacePanels"];
|
|
675
731
|
|
|
676
732
|
export function apply(ctx: Context) {
|
|
677
|
-
ctx.effect(() =>
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
733
|
+
ctx.effect(() =>
|
|
734
|
+
ctx.qaSurfacePanels.register({
|
|
735
|
+
id,
|
|
736
|
+
kind: "browser",
|
|
737
|
+
title: () => "Browser",
|
|
738
|
+
icon: "browser",
|
|
739
|
+
order: 100,
|
|
740
|
+
keepMounted: true,
|
|
741
|
+
}),
|
|
742
|
+
);
|
|
685
743
|
|
|
686
744
|
ctx.slots.inject(QA_SURFACE_PANEL_SLOT, () =>
|
|
687
|
-
ctx.slots.register(
|
|
688
|
-
|
|
689
|
-
BrowserPanel,
|
|
690
|
-
),
|
|
691
|
-
)
|
|
745
|
+
ctx.slots.register({ name: QA_SURFACE_PANEL_SLOT, key: id }, BrowserPanel),
|
|
746
|
+
);
|
|
692
747
|
|
|
693
748
|
ctx.qaSurfacePanels.open("browser", {
|
|
694
749
|
reason: "extension",
|
|
695
750
|
focus: false,
|
|
696
|
-
})
|
|
751
|
+
});
|
|
697
752
|
}
|
|
698
753
|
```
|
|
699
754
|
|
|
@@ -715,17 +770,19 @@ authorize every Host call server-side and must not persist credentials in the
|
|
|
715
770
|
browser.
|
|
716
771
|
|
|
717
772
|
```ts
|
|
718
|
-
import type { Context } from "@deepseek-ai/cordis"
|
|
773
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
719
774
|
|
|
720
|
-
export const inject = ["qaUserSettingsSections"]
|
|
775
|
+
export const inject = ["qaUserSettingsSections"];
|
|
721
776
|
|
|
722
777
|
export function apply(ctx: Context) {
|
|
723
|
-
ctx.effect(() =>
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
778
|
+
ctx.effect(() =>
|
|
779
|
+
ctx.qaUserSettingsSections.register({
|
|
780
|
+
id: "integrations",
|
|
781
|
+
title: "Интеграции",
|
|
782
|
+
order: 40,
|
|
783
|
+
component: IntegrationsPage,
|
|
784
|
+
}),
|
|
785
|
+
);
|
|
729
786
|
}
|
|
730
787
|
```
|
|
731
788
|
|
|
@@ -742,9 +799,9 @@ the credential is transport authentication only: never persist it, log it, or
|
|
|
742
799
|
put it in a URL, a tool argument or any model-visible value.
|
|
743
800
|
|
|
744
801
|
```ts
|
|
745
|
-
import type { Context } from "@deepseek-ai/cordis"
|
|
802
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
746
803
|
|
|
747
|
-
export const inject = ["qaUserSession", "slots"]
|
|
804
|
+
export const inject = ["qaUserSession", "slots"];
|
|
748
805
|
|
|
749
806
|
export function apply(ctx: Context) {
|
|
750
807
|
ctx.effect(() =>
|
|
@@ -752,7 +809,9 @@ export function apply(ctx: Context) {
|
|
|
752
809
|
ctx.slots.register(
|
|
753
810
|
{ name: "settings.plugin.item", key: "my-namespace" },
|
|
754
811
|
MyCard,
|
|
755
|
-
)
|
|
812
|
+
),
|
|
813
|
+
),
|
|
814
|
+
);
|
|
756
815
|
}
|
|
757
816
|
```
|
|
758
817
|
|
|
@@ -916,12 +975,12 @@ metadata:
|
|
|
916
975
|
qa-surface:
|
|
917
976
|
version: 1
|
|
918
977
|
audience:
|
|
919
|
-
type: subroles
|
|
978
|
+
type: subroles # or: type: common
|
|
920
979
|
include: [analyst, presales]
|
|
921
980
|
tools:
|
|
922
981
|
requires: [browser_open, browser_click]
|
|
923
982
|
grant:
|
|
924
|
-
lifecycle: session
|
|
983
|
+
lifecycle: session # the only lifecycle in v1
|
|
925
984
|
requireAll: true
|
|
926
985
|
---
|
|
927
986
|
```
|
|
@@ -947,6 +1006,7 @@ declared audience next to the effective one with a
|
|
|
947
1006
|
`Healthy`/`Degraded`/`Blocked` state. Every activation is recorded on the
|
|
948
1007
|
session record with the requested, granted and denied tools, so a later review
|
|
949
1008
|
can see what a conversation actually gained.
|
|
1009
|
+
|
|
950
1010
|
## Administrative console
|
|
951
1011
|
|
|
952
1012
|
`/qa/admin` is the review and administration surface. It is part of the QA page
|
|
@@ -995,6 +1055,208 @@ boundary. Review material is the most sensitive data the package handles, so
|
|
|
995
1055
|
tool arguments and results are bounded previews with credential shapes masked,
|
|
996
1056
|
behind a redactor a deployment can replace.
|
|
997
1057
|
|
|
1058
|
+
## Integration API (HTTP)
|
|
1059
|
+
|
|
1060
|
+
Another application — a ticket-system bridge, a bot, a script — can ask the same
|
|
1061
|
+
assistant questions over HTTP, with its own credential instead of a browser
|
|
1062
|
+
session, and read its own conversations back. The endpoints are **off by
|
|
1063
|
+
default**.
|
|
1064
|
+
|
|
1065
|
+
```yaml
|
|
1066
|
+
integration:
|
|
1067
|
+
enabled: true # requires accounts.enabled: true
|
|
1068
|
+
basePath: /qa/api # POST {basePath}/ask, GET {basePath}/session, GET {basePath}/health
|
|
1069
|
+
tokenTtlDays: 90
|
|
1070
|
+
requestTimeoutMs: 90000
|
|
1071
|
+
maxConcurrent: 4
|
|
1072
|
+
requestsPerMinute: 60
|
|
1073
|
+
maxAnswerCharacters: 4096 # the answer the ticket comment can hold
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
The API needs accounts: a caller is an account, and the credential it presents
|
|
1077
|
+
is that account's integration token. Switching it on without
|
|
1078
|
+
`accounts.enabled: true` is refused at configuration time rather than served
|
|
1079
|
+
without authentication.
|
|
1080
|
+
|
|
1081
|
+
### Issuing a token
|
|
1082
|
+
|
|
1083
|
+
```bash
|
|
1084
|
+
# the secret is printed exactly once and is never recoverable
|
|
1085
|
+
qa-accounts token create bridge@example.corp --label "ticket bridge" --scopes ask --days 90
|
|
1086
|
+
qa-accounts token list bridge@example.corp # ids, scopes, expiry, last use
|
|
1087
|
+
qa-accounts token revoke bridge@example.corp <token-id>
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
An integration token is a **separate credential** from the browser token:
|
|
1091
|
+
|
|
1092
|
+
- it survives a password change, because a service that is already integrated
|
|
1093
|
+
must not be logged out by a person editing their own profile;
|
|
1094
|
+
- it stores only a SHA-256 digest of its secret, so a copied database is not a
|
|
1095
|
+
copied credential;
|
|
1096
|
+
- it carries scopes (`ask`, `sessions:read`), an independent expiry and its own
|
|
1097
|
+
revocation;
|
|
1098
|
+
- it stops with the account: disabling the account refuses it, and
|
|
1099
|
+
`qa-accounts revoke <email>` revokes it together with every browser session.
|
|
1100
|
+
|
|
1101
|
+
Use one token per integration and revoke it when the integration is retired.
|
|
1102
|
+
Treat the secret like a password: it is a bearer credential with no second
|
|
1103
|
+
factor.
|
|
1104
|
+
|
|
1105
|
+
### Asking a question
|
|
1106
|
+
|
|
1107
|
+
```bash
|
|
1108
|
+
curl -sS https://dsh.example.local/qa/api/ask \
|
|
1109
|
+
-H "Authorization: Bearer qsat.<id>.<secret>" \
|
|
1110
|
+
-H "Content-Type: application/json" \
|
|
1111
|
+
-d '{
|
|
1112
|
+
"message": "TEST получения задач для ИИ Агента\n\nКомпоненты: MDC",
|
|
1113
|
+
"version": "3.8",
|
|
1114
|
+
"session_id": null,
|
|
1115
|
+
"context": {
|
|
1116
|
+
"ticket_key": "PROJ-123",
|
|
1117
|
+
"reporter": "user@example.corp",
|
|
1118
|
+
"reporter_name": "Демо-пользователь"
|
|
1119
|
+
}
|
|
1120
|
+
}'
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
```json
|
|
1124
|
+
{
|
|
1125
|
+
"chat_id": "session-1f0c…",
|
|
1126
|
+
"answer": "**Ответ**\n\nТекст ответа в Markdown…",
|
|
1127
|
+
"sources": ["Документация_v3.8.pdf#стр.12"],
|
|
1128
|
+
"confidence": "medium",
|
|
1129
|
+
"escalate": false,
|
|
1130
|
+
"reason": ""
|
|
1131
|
+
}
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
Send `chat_id` back as `session_id` to continue the same conversation. The chat
|
|
1135
|
+
belongs to the token's account: a token can only continue chats its own account
|
|
1136
|
+
owns, and a chat an integration opened keeps its own workspace, subrole and
|
|
1137
|
+
capability snapshot like any other QA chat.
|
|
1138
|
+
|
|
1139
|
+
`multipart/form-data` is accepted with the same fields (`message`, `version`,
|
|
1140
|
+
`session_id`, `context` as a JSON **string**) plus repeated `files` parts (at
|
|
1141
|
+
most five, 10 MiB each by default).
|
|
1142
|
+
|
|
1143
|
+
| Attachment | What the model receives |
|
|
1144
|
+
| ---------------------------------------------------- | ---------------------------------------------------- |
|
|
1145
|
+
| `image/png`, `image/jpeg`, `image/webp`, `image/gif` | the image itself |
|
|
1146
|
+
| `text/plain`, `text/csv`, `text/markdown` | its text, under a heading with the file name |
|
|
1147
|
+
| `application/pdf`, Word, Excel, PowerPoint | text extracted by the deployment's document pipeline |
|
|
1148
|
+
| anything else | `415` — the fallback the bridge already implements |
|
|
1149
|
+
|
|
1150
|
+
A file the Host cannot read refuses the whole question with `415` rather than
|
|
1151
|
+
being skipped: an answer produced without the material it was asked about is
|
|
1152
|
+
worse than asking again without it. Documents need the `documents` plugin
|
|
1153
|
+
installed; without it PDF and Office attachments take the same `415` path, and
|
|
1154
|
+
images and text keep working. An image the deployment itself refuses — one the
|
|
1155
|
+
selected model cannot see, or one past the Host's image limits — answers the
|
|
1156
|
+
same `415`, with the reason (`MODEL_DOES_NOT_SUPPORT_IMAGES`, `IMAGE_TOO_LARGE`,
|
|
1157
|
+
`IMAGE_TYPE_MISMATCH`, …) in the body and in the Host log. Attachments are read,
|
|
1158
|
+
not stored: the bytes live in a temporary directory for the length of one
|
|
1159
|
+
extraction, and the text is
|
|
1160
|
+
bounded (60 000 characters per file, 120 000 per question, truncated with a
|
|
1161
|
+
marker). Everything the bridge's own filter lets through is accepted; archives,
|
|
1162
|
+
executables and media are not.
|
|
1163
|
+
|
|
1164
|
+
A `answer` longer than `maxAnswerCharacters` is cut before it is published, at
|
|
1165
|
+
the last paragraph break (then the last line, then the last sentence) and marked
|
|
1166
|
+
with an ellipsis, so the ticket shows a readable head instead of a comment the
|
|
1167
|
+
ticket system silently truncates mid-sentence. The cut is logged with the chat
|
|
1168
|
+
id; it is still a normal answer, not an escalation.
|
|
1169
|
+
|
|
1170
|
+
`confidence` is `medium` for a published answer and `low` for an escalated one.
|
|
1171
|
+
`high` is deliberately never claimed: nothing in the deployment judges an
|
|
1172
|
+
answer, and a field that always said `high` would train the operator to ignore
|
|
1173
|
+
it. `escalate: true` means the assistant produced nothing publishable — an
|
|
1174
|
+
interrupted turn, an empty answer, or a question that did not finish inside
|
|
1175
|
+
`requestTimeoutMs` (the `chat_id` is still returned, so a retry continues the
|
|
1176
|
+
same chat instead of starting a second one).
|
|
1177
|
+
|
|
1178
|
+
### Reading a conversation back
|
|
1179
|
+
|
|
1180
|
+
```bash
|
|
1181
|
+
# after=0 reads from the start; limit is clamped to 200
|
|
1182
|
+
curl -sS 'https://dsh.example.local/qa/api/session?chat_id=session-1f0c…&after=0&limit=50' \
|
|
1183
|
+
-H "Authorization: Bearer qsat.<id>.<secret>"
|
|
1184
|
+
# {"chat_id":"session-1f0c…",
|
|
1185
|
+
# "messages":[{"seq":1,"role":"user","text":"…","at":"2026-09-21T10:00:00.000Z"},
|
|
1186
|
+
# {"seq":2,"role":"assistant","text":"**Ответ**…","at":null}],
|
|
1187
|
+
# "last_seq":2,"truncated":false}
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
This is what the `sessions:read` scope is for, and it is a separate scope rather
|
|
1191
|
+
than part of `ask`: a bridge that escalates a ticket, or opens the question in a
|
|
1192
|
+
review screen for a specialist, has to show what was already said. Without the
|
|
1193
|
+
endpoint such a bridge could only ask the same question again — spending a turn
|
|
1194
|
+
to answer a question that already has an answer.
|
|
1195
|
+
|
|
1196
|
+
The read is the same conversation the account owns, in the same words: `text`
|
|
1197
|
+
is the flattening the answer itself uses, so a message reads the same whether it
|
|
1198
|
+
was received as an answer or fetched here. Only the prompts the person sent and
|
|
1199
|
+
the assistant's answers are published — injected context, reasoning and tool
|
|
1200
|
+
traffic are not, because those are model input the caller never wrote.
|
|
1201
|
+
|
|
1202
|
+
`after` is the caller's own cursor: pass the `last_seq` of the previous read,
|
|
1203
|
+
and the next call returns only what was written since. `limit` bounds the page
|
|
1204
|
+
(default 50, at most 200) and the window is always the **newest** messages, so a
|
|
1205
|
+
long conversation costs one page rather than one transcript per call; when older
|
|
1206
|
+
messages were left below the window, `truncated` says so. `at` is the instant
|
|
1207
|
+
the log recorded, or `null` when it recorded none — a timestamp is never
|
|
1208
|
+
invented.
|
|
1209
|
+
|
|
1210
|
+
An unknown `chat_id` and another account's chat answer the same `404`, so an id
|
|
1211
|
+
alone never confirms that somebody else's conversation exists.
|
|
1212
|
+
|
|
1213
|
+
Reading is cheap enough to poll: the newest messages of a conversation are kept
|
|
1214
|
+
warm, and a chat this Host is holding is checked against its own memory, so a
|
|
1215
|
+
page costs neither a stored read nor a walk through the whole history — a page
|
|
1216
|
+
of a long conversation is a page. The first read of an old chat is the
|
|
1217
|
+
expensive one, and a chat this Host does not hold is re-read after 30 seconds at
|
|
1218
|
+
the latest, which is the same budget the review console gives its transcripts.
|
|
1219
|
+
|
|
1220
|
+
### Health
|
|
1221
|
+
|
|
1222
|
+
```bash
|
|
1223
|
+
curl -sS https://dsh.example.local/qa/api/health \
|
|
1224
|
+
-H "Authorization: Bearer qsat.<id>.<secret>"
|
|
1225
|
+
# {"ok":true,"version":"0.11.0","models":["gpt-4o-mini"],"uptime_s":86400}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
### Statuses
|
|
1229
|
+
|
|
1230
|
+
| Status | Meaning |
|
|
1231
|
+
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1232
|
+
| 200 | Answered, or escalated with an empty `answer` |
|
|
1233
|
+
| 400 | Malformed body (no `message`, broken JSON or `context`), a missing `chat_id`, or a cursor that is not a whole number |
|
|
1234
|
+
| 401 | Missing, expired, revoked or unknown token |
|
|
1235
|
+
| 403 | Valid token without the scope the call needs (`ask`, `sessions:read`) |
|
|
1236
|
+
| 404 | A chat the token's account does not own (or that does not exist); also `integration.enabled` is false: no route is registered, and the request reaches whatever the deployment serves for unknown paths |
|
|
1237
|
+
| 413 | Body or attachment over the configured limit |
|
|
1238
|
+
| 415 | Unsupported content type, or an attachment the deployment refused |
|
|
1239
|
+
| 429 | Per-token rate limit or the deployment's concurrency limit |
|
|
1240
|
+
| 503 | The QA assistant failed before it could answer; retry |
|
|
1241
|
+
|
|
1242
|
+
Response bodies carry `{ "error": "…", "code": "…" }` with the same reason
|
|
1243
|
+
vocabulary, so a client can branch on the code instead of parsing prose.
|
|
1244
|
+
|
|
1245
|
+
### What to watch
|
|
1246
|
+
|
|
1247
|
+
- Every request that reaches a turn is logged on the Host with the token id,
|
|
1248
|
+
the chat id and the ticket key — never the question or the answer. A read is
|
|
1249
|
+
logged the same way, with the cursor it asked from and the number of messages
|
|
1250
|
+
it got.
|
|
1251
|
+
- `maxConcurrent` bounds how much of the deployment a bridge can occupy;
|
|
1252
|
+
`requestsPerMinute` bounds one token. Both answer `429`, which retries well.
|
|
1253
|
+
- `maxAnswerCharacters` bounds one answer. A question that produced more is
|
|
1254
|
+
logged as truncated, with the chat id and the published length — never the
|
|
1255
|
+
text.
|
|
1256
|
+
- The endpoint is a network surface: publish it only where the integration
|
|
1257
|
+
runs, keep TLS in front of it, and remember that the token's scopes are the
|
|
1258
|
+
only limits on what it can ask.
|
|
1259
|
+
|
|
998
1260
|
## Security and deployment
|
|
999
1261
|
|
|
1000
1262
|
`/qa` is a presentation boundary, not an authentication boundary. Protect it
|
package/lib/access/service.js
CHANGED
|
@@ -369,7 +369,24 @@ export class QaAccessService {
|
|
|
369
369
|
}
|
|
370
370
|
reserveSession(token, sessionId, requestedSubrole, adminPreview) {
|
|
371
371
|
const accounts = this.requireAccounts();
|
|
372
|
-
|
|
372
|
+
return this.reserveForUser(accounts.currentUser(token), sessionId, requestedSubrole, adminPreview);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The same reservation for a chat opened by an already-authenticated
|
|
376
|
+
* account — the integration API's path. The subrole is the account's own
|
|
377
|
+
* default: an external caller is never offered a choice of capabilities,
|
|
378
|
+
* and an administrator's preview flag stays a browser-only affordance.
|
|
379
|
+
*
|
|
380
|
+
* @param user - the account the chat belongs to.
|
|
381
|
+
* @param sessionId - the Host-generated session id to reserve.
|
|
382
|
+
* @returns the account, as the browser path returns it.
|
|
383
|
+
*/
|
|
384
|
+
reserveSessionForUser(user, sessionId) {
|
|
385
|
+
this.requireAccounts();
|
|
386
|
+
return this.reserveForUser(user, sessionId, null, false);
|
|
387
|
+
}
|
|
388
|
+
reserveForUser(user, sessionId, requestedSubrole, adminPreview) {
|
|
389
|
+
const accounts = this.requireAccounts();
|
|
373
390
|
const config = this.roles.snapshot();
|
|
374
391
|
const assignment = normalizeUserAccess(accounts.accessOf(user.id), config);
|
|
375
392
|
const selected = requestedSubrole ?? assignment.defaultSubrole;
|
|
@@ -384,7 +401,7 @@ export class QaAccessService {
|
|
|
384
401
|
else if (!assignment.allowedSubroles.includes(selected)) {
|
|
385
402
|
throw new QaAccountsError("invalid-role", "this QA subrole is not assigned to the account");
|
|
386
403
|
}
|
|
387
|
-
const owner = accounts.
|
|
404
|
+
const owner = accounts.reserveSessionForUser(user, sessionId, {
|
|
388
405
|
subroleId: selected,
|
|
389
406
|
...(adminPreview ? { adminPreview: true } : {}),
|
|
390
407
|
});
|
|
@@ -395,16 +412,25 @@ export class QaAccessService {
|
|
|
395
412
|
this.maybeSweepDelegatedSessions();
|
|
396
413
|
return owner;
|
|
397
414
|
}
|
|
398
|
-
|
|
415
|
+
/**
|
|
416
|
+
* The policy of one session, addressed by its authenticated account.
|
|
417
|
+
*
|
|
418
|
+
* Split from {@link policyForSession} so the integration API — whose caller
|
|
419
|
+
* proved an account with a service token instead of a browser one — freezes
|
|
420
|
+
* exactly the same capability snapshot. What the two entry points share is
|
|
421
|
+
* everything after identity; the identity check itself is the only part that
|
|
422
|
+
* differs, and it stays with whoever holds the credential.
|
|
423
|
+
*
|
|
424
|
+
* @param owner - the account that owns the session, or undefined when the
|
|
425
|
+
* deployment has no accounts service at all.
|
|
426
|
+
* @param sessionId - the session being admitted.
|
|
427
|
+
* @param agent - its live agent, for the tool catalog the policy resolves against.
|
|
428
|
+
* @returns the policy, or undefined without an accounts service.
|
|
429
|
+
*/
|
|
430
|
+
async policyForSessionOwner(owner, sessionId, agent) {
|
|
399
431
|
const accounts = this.options.accounts();
|
|
400
|
-
if (accounts === undefined)
|
|
432
|
+
if (accounts === undefined || owner === undefined)
|
|
401
433
|
return undefined;
|
|
402
|
-
const owner = accounts.ensureSessionAccess(token, sessionId, {
|
|
403
|
-
createdAt: agent.session.header.createdAt,
|
|
404
|
-
...(agent.session.header.parentSession === undefined
|
|
405
|
-
? {}
|
|
406
|
-
: { hasParent: true }),
|
|
407
|
-
});
|
|
408
434
|
let record = accounts.sessionAccess(sessionId);
|
|
409
435
|
const config = this.roles.snapshot();
|
|
410
436
|
const assignment = normalizeUserAccess(accounts.accessOf(owner.id), config);
|
|
@@ -457,6 +483,19 @@ export class QaAccessService {
|
|
|
457
483
|
}),
|
|
458
484
|
};
|
|
459
485
|
}
|
|
486
|
+
/** The browser's entry point: the token resolves the owning account. */
|
|
487
|
+
async policyForSession(token, sessionId, agent) {
|
|
488
|
+
const accounts = this.options.accounts();
|
|
489
|
+
if (accounts === undefined)
|
|
490
|
+
return undefined;
|
|
491
|
+
const owner = accounts.ensureSessionAccess(token, sessionId, {
|
|
492
|
+
createdAt: agent.session.header.createdAt,
|
|
493
|
+
...(agent.session.header.parentSession === undefined
|
|
494
|
+
? {}
|
|
495
|
+
: { hasParent: true }),
|
|
496
|
+
});
|
|
497
|
+
return this.policyForSessionOwner(owner, sessionId, agent);
|
|
498
|
+
}
|
|
460
499
|
/** Activation history of one session, for review and quality analysis. */
|
|
461
500
|
skillActivations(token, sessionId) {
|
|
462
501
|
const { accounts } = this.requireAdmin(token);
|