@yadsh/dsh-qa-surface 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +162 -14
  3. package/SPEC-dsh-qa-surface.md +600 -96
  4. package/capability-policy.json +5 -0
  5. package/compatibility.json +7 -1
  6. package/cordis.patch.yml +11 -0
  7. package/docs/ARCHITECTURE.md +49 -7
  8. package/docs/COMPATIBILITY.md +5 -4
  9. package/docs/CONFIGURATION.md +96 -2
  10. package/lib/attestation.js +10 -0
  11. package/lib/attestation.js.map +1 -0
  12. package/lib/client/QaConfigController.js +111 -4
  13. package/lib/client/QaConfigController.js.map +1 -1
  14. package/lib/client/QaSessionController.js +501 -119
  15. package/lib/client/QaSessionController.js.map +1 -1
  16. package/lib/client/QaSurface.js +137 -20
  17. package/lib/client/QaSurface.js.map +1 -1
  18. package/lib/client/QaTranscriptAdapter.js +534 -45
  19. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  20. package/lib/client/attestation.js +60 -0
  21. package/lib/client/attestation.js.map +1 -0
  22. package/lib/client/chat-index.js +86 -0
  23. package/lib/client/chat-index.js.map +1 -0
  24. package/lib/client/components/Markdown.js +75 -5
  25. package/lib/client/components/Markdown.js.map +1 -1
  26. package/lib/client/components/QaAgentsDrawer.js +40 -0
  27. package/lib/client/components/QaAgentsDrawer.js.map +1 -0
  28. package/lib/client/components/QaComposer.js +132 -15
  29. package/lib/client/components/QaComposer.js.map +1 -1
  30. package/lib/client/components/QaMessage.js +184 -10
  31. package/lib/client/components/QaMessage.js.map +1 -1
  32. package/lib/client/components/QaSidebar.js +124 -0
  33. package/lib/client/components/QaSidebar.js.map +1 -0
  34. package/lib/client/components/QaSourcesDrawer.js +46 -0
  35. package/lib/client/components/QaSourcesDrawer.js.map +1 -0
  36. package/lib/client/components/QaWorkGroup.js +135 -0
  37. package/lib/client/components/QaWorkGroup.js.map +1 -0
  38. package/lib/client/components/VariantSwitcher.js +29 -0
  39. package/lib/client/components/VariantSwitcher.js.map +1 -0
  40. package/lib/client/components/format.js +48 -0
  41. package/lib/client/components/format.js.map +1 -0
  42. package/lib/client/index.js +77 -34
  43. package/lib/client/index.js.map +1 -1
  44. package/lib/client/styles.js +308 -43
  45. package/lib/client/styles.js.map +1 -1
  46. package/lib/client/types.js +13 -0
  47. package/lib/client/types.js.map +1 -0
  48. package/lib/client/wait-for.js +49 -0
  49. package/lib/client/wait-for.js.map +1 -0
  50. package/lib/client.js +8228 -383
  51. package/lib/client.js.map +1 -1
  52. package/lib/config.js +61 -9
  53. package/lib/config.js.map +1 -1
  54. package/lib/index.js +166 -70
  55. package/lib/index.js.map +1 -1
  56. package/lib/lockdown-policy.js +20 -0
  57. package/lib/lockdown-policy.js.map +1 -0
  58. package/lib/resolve-config.js +122 -10
  59. package/lib/resolve-config.js.map +1 -1
  60. package/lib/secure-session.js +170 -0
  61. package/lib/secure-session.js.map +1 -0
  62. package/lib/typert.host.d.ts +3 -0
  63. package/lib/typert.host.js +191 -0
  64. package/lib/typert.remote-client.d.ts +24 -0
  65. package/lib/typert.remote-client.js +129 -0
  66. package/lib/types/attestation.d.ts +17 -0
  67. package/lib/types/client/QaConfigController.d.ts +34 -2
  68. package/lib/types/client/QaSessionController.d.ts +106 -19
  69. package/lib/types/client/QaSurface.d.ts +5 -4
  70. package/lib/types/client/QaTranscriptAdapter.d.ts +17 -2
  71. package/lib/types/client/attestation.d.ts +25 -0
  72. package/lib/types/client/chat-index.d.ts +28 -0
  73. package/lib/types/client/components/Markdown.d.ts +7 -3
  74. package/lib/types/client/components/QaAgentsDrawer.d.ts +25 -0
  75. package/lib/types/client/components/QaComposer.d.ts +12 -2
  76. package/lib/types/client/components/QaMessage.d.ts +14 -1
  77. package/lib/types/client/components/QaSidebar.d.ts +41 -0
  78. package/lib/types/client/components/QaSourcesDrawer.d.ts +8 -0
  79. package/lib/types/client/components/QaWorkGroup.d.ts +19 -0
  80. package/lib/types/client/components/VariantSwitcher.d.ts +19 -0
  81. package/lib/types/client/components/format.d.ts +9 -0
  82. package/lib/types/client/index.d.ts +1 -5
  83. package/lib/types/client/types.d.ts +29 -0
  84. package/lib/types/client/wait-for.d.ts +11 -0
  85. package/lib/types/index.d.ts +22 -6
  86. package/lib/types/lockdown-policy.d.ts +18 -0
  87. package/lib/types/secure-session.d.ts +22 -0
  88. package/lib/types/types.d.ts +148 -1
  89. package/package.json +76 -52
@@ -1,8 +1,9 @@
1
1
  # SPEC / Implementation Plan: `dsh-qa-surface`
2
2
 
3
- **Status:** Draft / implementation-ready
4
- **Date:** 2026-09-05
5
- **Target:** DeepSeek Harness Web
3
+ **Status:** Draft / implementation-ready
4
+ **Date:** 2026-09-05
5
+ **Last updated:** 2026-09-08 — added locked-down / read-only QA execution policy
6
+ **Target:** DeepSeek Harness Web
6
7
  **Artifact type:** external DSH UI plugin with a dedicated `/qa` browser surface
7
8
 
8
9
  ---
@@ -232,6 +233,12 @@ No first-party DSH navigation or admin controls should be visible through the QA
232
233
  - configurable fixed workspace/session target policy;
233
234
  - configurable agent preset;
234
235
  - configurable model override if explicitly set;
236
+ - **locked-down QA mode enabled by default**;
237
+ - fixed agent preset with no QA-user agent switching;
238
+ - forced `read-only` sandbox mode;
239
+ - forced `approval=never` / no permission escalation;
240
+ - allow-list based tool exposure for QA sessions;
241
+ - no model/workspace/permission/mode/settings switching from the QA surface;
235
242
  - safe handling of unsupported interactive states;
236
243
  - no leakage of tool calls/reasoning into the QA transcript by default;
237
244
  - same-origin use of the normal DSH connection/API stack;
@@ -274,6 +281,10 @@ The MVP must **not**:
274
281
  - automatically approve permission requests;
275
282
  - automatically answer agent questions;
276
283
  - expose admin/settings APIs to unauthenticated users;
284
+ - rely on hidden/disabled UI controls as the only security boundary;
285
+ - allow the QA user to switch agent preset, workspace, model, sandbox mode, approval policy, or permission preset;
286
+ - expose a general-purpose shell/terminal/code-execution tool in the default locked-down preset;
287
+ - expose mutable external-service tools by default;
277
288
  - expose raw reasoning by default;
278
289
  - expose raw tool arguments/results by default;
279
290
  - depend on private DSH component implementation details;
@@ -440,6 +451,30 @@ Preferred config transport:
440
451
 
441
452
  Avoid creating an unauthenticated `/qa-config.json` endpoint.
442
453
 
454
+ ### 10.2 LAN config channel
455
+
456
+ DSH pins settings RPCs to loopback (`settings.*` sits in the gateway's
457
+ privileged method set), so a browser served from a non-loopback bind always
458
+ sees the namespace as `unavailable`. The Host half therefore exposes one
459
+ additional read-only Remote, `qaSurface/describe`, returning the effective
460
+ resolved configuration — the same projection `secureSession` attests against.
461
+ It is the network-neutral answer to responsibility 3: still plugin-scoped,
462
+ still no arbitrary Host config, no secrets, no file paths beyond the
463
+ configured ids, and no admission authority (enforcement stays in
464
+ `secureSession`).
465
+
466
+ Client contract: when the settings namespace is `unavailable` — and only
467
+ then — the browser calls `describe` once per controller lifetime, waits in
468
+ `loading`, and adopts the returned configuration as `ready`; a rejected or
469
+ invalid answer falls back to the client defaults under `unavailable`. A
470
+ readable namespace always wins and keeps delivering live updates.
471
+
472
+ LAN serving itself is a deployment composition concern, not plugin config:
473
+ the shipped `deploy/qa-lan.patch.yml` overlay binds the `webserver` row to
474
+ `0.0.0.0`, and the DSH `/api` browser-trust fence derives the LAN authorities
475
+ on its own. See the README "Serving the QA rig over the LAN" section for the
476
+ deployment responsibilities this creates.
477
+
443
478
  ---
444
479
 
445
480
  ## 11. Configuration model
@@ -454,10 +489,10 @@ route:
454
489
  matchChildren: true
455
490
 
456
491
  branding:
457
- title: Assistant
492
+ title: Помощник
458
493
  subtitle: ""
459
- welcomeMessage: "How can I help?"
460
- placeholder: "Ask a question..."
494
+ welcomeMessage: "Чем могу помочь?"
495
+ placeholder: "Задайте вопрос…"
461
496
  logoUrl: null
462
497
 
463
498
  session:
@@ -472,7 +507,7 @@ session:
472
507
 
473
508
  ui:
474
509
  showHeader: true
475
- showReset: true
510
+ showReset: false
476
511
  showStop: true
477
512
  showTimestamps: false
478
513
  showToolActivity: false
@@ -480,12 +515,34 @@ ui:
480
515
  renderMarkdown: true
481
516
  maxContentWidth: 900
482
517
 
483
- suggestedQuestions: []
518
+ suggestedQuestions:
519
+ - "Что ты умеешь?"
520
+ - "С чего начать?"
521
+ - "Помоги разобраться с ошибкой"
484
522
 
485
523
  interaction:
486
524
  approvals: blocked
487
525
  questions: unsupported
488
526
 
527
+ lockdown:
528
+ enabled: true
529
+ enforceFixedAgentPreset: true
530
+ enforceFixedWorkspace: true
531
+ enforceFixedModel: true
532
+ sandboxMode: read-only
533
+ approvalPolicy: never
534
+ permissionPreset: qa-read-only
535
+ allowPermissionChanges: false
536
+ allowSlashCommands: false
537
+ allowSettingsMutation: false
538
+ allowSessionReset: false
539
+ allowSessionRename: false
540
+ allowSessionDelete: false
541
+ allowArbitrarySessionOpen: false
542
+ toolPolicy:
543
+ mode: allow-list
544
+ allow: []
545
+
489
546
  embedding:
490
547
  frameAncestors: null
491
548
  ```
@@ -580,7 +637,247 @@ means use DSH deployment/session defaults.
580
637
 
581
638
  If both are configured, apply the model selection to the QA session through the public session model-selection path.
582
639
 
583
- Do not render a model picker unless a future config explicitly enables it.
640
+ In locked-down mode, never render a model picker and do not expose a controller action for model switching. If provider/model are left `null`, operator deployment defaults may still determine the model for newly created sessions; that is an operator-side policy change, not a QA-user choice.
641
+
642
+
643
+
644
+ ### 11.6 Locked-down execution policy
645
+
646
+ `/qa` must default to a **capability-reduced execution profile**, not merely a visually simplified UI.
647
+
648
+ The desired invariant is:
649
+
650
+ ```text
651
+ QA user can:
652
+ - submit text questions;
653
+ - receive assistant answers;
654
+ - stop the current turn;
655
+ - optionally start a fresh QA session.
656
+
657
+ QA user cannot:
658
+ - change the agent preset;
659
+ - change workspace/cwd;
660
+ - change model/provider/reasoning mode;
661
+ - change permissions/sandbox/approval policy;
662
+ - invoke DSH slash commands;
663
+ - mutate settings;
664
+ - open arbitrary existing sessions;
665
+ - rename/delete sessions;
666
+ - gain a shell/terminal merely because it exists globally;
667
+ - trigger write-capable Jira/GitHub/Confluence/Slack/email/etc. actions;
668
+ - escalate from read-only to a wider sandbox mode.
669
+ ```
670
+
671
+ This must be enforced in layers. Hiding selectors is only the presentation layer.
672
+
673
+ #### Layer 1 — fixed session composition
674
+
675
+ For every newly created QA session:
676
+
677
+ - use exactly the configured `session.agentPreset`;
678
+ - use exactly the configured `session.workspaceId`;
679
+ - if model/provider are configured, use exactly those values;
680
+ - refuse to silently fall back to another agent/workspace when the configured value is missing;
681
+ - persist the selected QA session id, but do not persist user-selectable composition overrides.
682
+
683
+ DSH currently treats the agent preset as a per-session composition fact. Existing upstream behavior already prevents changing the preset after a session has produced history; blank sessions may still have a preset choice before the first turn. Therefore the QA controller must create/bind the session with the configured preset and never expose the blank-session preset selector.
684
+
685
+ Reference:
686
+
687
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-agent-preset/README.md
688
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md
689
+
690
+ #### Layer 2 — force `read-only` + `approval=never`
691
+
692
+ The locked-down QA policy must pin both independent DSH permission knobs:
693
+
694
+ ```yaml
695
+ sandbox: read-only
696
+ approval: never
697
+ ```
698
+
699
+ Prefer defining a named deployment preset:
700
+
701
+ ```yaml
702
+ permissionPresets:
703
+ presets:
704
+ qa-read-only:
705
+ sandbox: read-only
706
+ approval: never
707
+ name: QA Read Only
708
+ description: No filesystem mutations and no permission escalation.
709
+ ```
710
+
711
+ On a **shared operator + QA DSH instance**, do not make `qa-read-only` the process-wide `defaultPreset` merely for this plugin, because that would also change the default permissions of ordinary operator-created sessions. Instead, the QA bootstrap path must select/pin `qa-read-only` on the freshly created blank QA session **before the first prompt is accepted**, then verify the effective sandbox/approval facts.
712
+
713
+ On a **dedicated QA DSH instance**, setting `defaultPreset: qa-read-only` is recommended as an additional fail-safe.
714
+
715
+ The exact surrounding Cordis row/config syntax must be adapted to the target deployment.
716
+
717
+ Important: DSH's shipped permission preset table normally contains `workspace-write` and `danger-full-access`; `qa-read-only` is an explicit custom table entry for this deployment. The underlying knobs are the authoritative enforcement facts.
718
+
719
+ `approval=never` is required because it deterministically rejects operations that request approval instead of presenting an approval UI. The QA surface must never implement an auto-approve path.
720
+
721
+ References:
722
+
723
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/permission-presets.md
724
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/approval.md
725
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/sandbox/sandbox-policy/README.md
726
+
727
+ #### Layer 3 — tool allow-list, not a deny-list
728
+
729
+ `read-only` is **not equivalent to “no side effects.”** It governs file effects enforced by the DSH file sandbox; network and process visibility are outside that sandbox vocabulary. A shell command can still make network requests or trigger external side effects even when filesystem writes are denied.
730
+
731
+ Therefore the default QA preset must use an explicit **allow-list of known read-only tools**.
732
+
733
+ Conceptual policy:
734
+
735
+ ```yaml
736
+ lockdown:
737
+ toolPolicy:
738
+ mode: allow-list
739
+ allow:
740
+ - knowledge.search
741
+ - knowledge.read
742
+ - web.search
743
+ - web.fetch
744
+ ```
745
+
746
+ The actual names must come from the composed DSH deployment. Do not copy these illustrative names blindly.
747
+
748
+ Prefer allow-list semantics because newly installed global tools are then excluded automatically. DSH has a scoped `ToolRestriction.allow` mechanism for inherited global tools. Note that tools registered directly inside the agent's own scope are intentionally exempt from inherited restrictions, so **the QA agent preset itself must also be reviewed and must not register write-capable tools**.
749
+
750
+ References:
751
+
752
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/tools.md
753
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/tools/src/index.ts
754
+
755
+ Default deny category examples:
756
+
757
+ | Capability | Default QA policy | Reason |
758
+ |---|---|---|
759
+ | generic shell / bash / terminal | deny | can create non-filesystem side effects and broad host visibility |
760
+ | generic code runtime | deny | same reason unless a separately constrained pure-compute runtime exists |
761
+ | fs write/edit/move/delete | deny | direct mutation |
762
+ | git commit/push/checkout/reset | deny | repository mutation / remote side effects |
763
+ | Jira create/update/comment/transition | deny | external mutation |
764
+ | Confluence create/update/delete | deny | external mutation |
765
+ | Slack/email send | deny | external mutation |
766
+ | browser/computer-use click/type/submit | deny | arbitrary external side effects |
767
+ | settings/plugin management | deny | can expand capabilities |
768
+ | permission/preset management | deny | can expand capabilities |
769
+ | agent-preset management | deny | can change composition |
770
+ | read-only KB/search/fetch tools | allow only after review | intended QA capability |
771
+
772
+ For MCP, allow-list at the **tool level**, not merely at the server name. A server may expose both read and write actions.
773
+
774
+ #### Layer 4 — no mutation commands or alternate control paths
775
+
776
+ The QA composer must call the plain public prompt path directly and must not expose the general DSH command dispatcher.
777
+
778
+ Default:
779
+
780
+ ```yaml
781
+ lockdown:
782
+ allowSlashCommands: false
783
+ ```
784
+
785
+ At minimum prevent QA-side use of control paths equivalent to:
786
+
787
+ ```text
788
+ /permission ...
789
+ /permissionPresets ...
790
+ agent/preset selectors
791
+ model selectors
792
+ workspace selectors
793
+ settings mutation
794
+ plugin management
795
+ ```
796
+
797
+ Do not rely only on filtering strings that start with `/` if the DSH prompt API already provides a command-free prompt submission method. Prefer bypassing command adjudication entirely.
798
+
799
+ #### Layer 5 — controller-side invariant checks
800
+
801
+ `QaSessionController` must verify its effective session state before enabling Send.
802
+
803
+ Conceptual check:
804
+
805
+ ```ts
806
+ interface QaLockdownInvariant {
807
+ agentPresetMatches: boolean
808
+ workspaceMatches: boolean
809
+ modelMatches: boolean
810
+ sandboxIsReadOnly: boolean
811
+ approvalIsNever: boolean
812
+ toolPolicyLoaded: boolean
813
+ }
814
+ ```
815
+
816
+ If a required invariant cannot be proven, fail closed:
817
+
818
+ ```text
819
+ Assistant configuration is unavailable.
820
+ ```
821
+
822
+ and disable the composer. Do not continue under a wider or unknown policy.
823
+
824
+ A mismatch should be operator-visible in logs with the expected and actual policy identifiers, but the QA page should not expose sensitive details.
825
+
826
+ #### Layer 6 — host-side guard where public DSH seams allow it
827
+
828
+ UI/controller checks protect normal use but are not enough against another client using the same authenticated DSH APIs. During implementation, use public Host-side DSH enforcement seams where available to bind QA-marked sessions to the locked policy.
829
+
830
+ Preferred mechanisms, in order:
831
+
832
+ 1. compose the QA preset so forbidden tools are not mounted at all;
833
+ 2. apply scoped tool restrictions to inherited tools;
834
+ 3. pin sandbox and approval facts before the first QA prompt;
835
+ 4. prefer a Host-side QA session bootstrap operation that creates/binds the session and pins the QA permission facts in one controlled flow, if DSH exposes a clean public seam;
836
+ 5. otherwise create a blank session, immediately select `qa-read-only`, verify it, and keep Send disabled until verification succeeds;
837
+ 6. add a monotonic `tools/pre-execute` / execution guard for QA sessions if a second server-side side-effect check is needed;
838
+ 7. never implement capability expansion through the QA plugin.
839
+
840
+ Do not monkey-patch private RPC handlers merely to create a guard. If DSH has no public seam for a particular mutation path, document that limitation and move the boundary to deployment isolation rather than pretending the browser UI makes it secure.
841
+
842
+ #### Layer 7 — deployment isolation for strong guarantees
843
+
844
+ A `/qa` overlay on the same fully privileged DSH Host is **not a security boundary by itself**. If the same authenticated user can navigate to normal `/`, call privileged DSH APIs, or use another DSH client, hiding controls on `/qa` does not revoke those Host capabilities.
845
+
846
+ For an internal convenience UI, the layered session/tool policy above may be enough.
847
+
848
+ For untrusted or semi-trusted QA users, prefer one of:
849
+
850
+ ```text
851
+ Best isolation:
852
+ dedicated QA DSH instance/container
853
+ + only QA preset/tools mounted
854
+ + read-only corpus mounts
855
+ + no developer credentials
856
+ + restricted network egress
857
+
858
+ Good isolation:
859
+ separate QA auth role/gateway
860
+ + Host-side API authorization for mutable capabilities
861
+ + locked QA preset/tool registry
862
+
863
+ Weakest:
864
+ same privileged DSH Host/browser identity
865
+ + UI controls merely hidden
866
+ ```
867
+
868
+ DSH upstream explicitly warns that it is developer-preview software and sandbox/approval controls are risk reducers, not a complete isolation boundary.
869
+
870
+ Reference:
871
+
872
+ - https://github.com/deepseek-ai/deepseek-harness/blob/master/SAFETY.md
873
+
874
+ #### Read isolation caveat
875
+
876
+ Current `read-only` means “no filesystem mutation,” not “the agent can only read its workspace.” Upstream discussion notes that read isolation outside the workspace is not currently guaranteed. Therefore a high-assurance QA deployment should avoid exposing generic shell/fs inspection tools and should mount only the data the QA runtime is intended to read.
877
+
878
+ Reference:
879
+
880
+ - https://github.com/deepseek-ai/deepseek-harness/discussions/492
584
881
 
585
882
  ---
586
883
 
@@ -601,12 +898,12 @@ Conceptual Cordis inject list:
601
898
 
602
899
  ```ts
603
900
  export const inject = [
604
- "slots",
605
- "sessions",
606
- "connection",
607
- "theme",
901
+ 'slots',
902
+ 'sessions',
903
+ 'connection',
904
+ 'theme',
608
905
  // settings service used for qa-surface config
609
- ];
906
+ ]
610
907
  ```
611
908
 
612
909
  Exact service names must be resolved against the target DSH release.
@@ -622,15 +919,15 @@ shell.overlay
622
919
  Conceptually:
623
920
 
624
921
  ```ts
625
- ctx.slots.inject("shell.overlay", () =>
922
+ ctx.slots.inject('shell.overlay', () =>
626
923
  ctx.slots.register(
627
924
  {
628
- name: "shell.overlay",
629
- key: "dsh-qa-surface",
925
+ name: 'shell.overlay',
926
+ key: 'dsh-qa-surface'
630
927
  },
631
928
  QaSurfaceEntry,
632
929
  ),
633
- );
930
+ )
634
931
  ```
635
932
 
636
933
  The entry remains mounted but returns `null` whenever the route is not active.
@@ -725,27 +1022,27 @@ UI components consume a plugin-owned interface such as:
725
1022
  ```ts
726
1023
  interface QaSessionState {
727
1024
  phase:
728
- | "idle"
729
- | "creating"
730
- | "ready"
731
- | "running"
732
- | "reconnecting"
733
- | "blocked"
734
- | "error";
735
-
736
- sessionId: string | null;
737
- messages: readonly QaMessage[];
738
- error: string | null;
739
- canSend: boolean;
740
- canStop: boolean;
1025
+ | 'idle'
1026
+ | 'creating'
1027
+ | 'ready'
1028
+ | 'running'
1029
+ | 'reconnecting'
1030
+ | 'blocked'
1031
+ | 'error'
1032
+
1033
+ sessionId: string | null
1034
+ messages: readonly QaMessage[]
1035
+ error: string | null
1036
+ canSend: boolean
1037
+ canStop: boolean
741
1038
  }
742
1039
 
743
1040
  interface QaSessionActions {
744
- ensureSession(): Promise<void>;
745
- send(text: string): Promise<void>;
746
- stop(): Promise<void>;
747
- reset(): Promise<void>;
748
- retryConnection(): void;
1041
+ ensureSession(): Promise<void>
1042
+ send(text: string): Promise<void>
1043
+ stop(): Promise<void>
1044
+ reset(): Promise<void>
1045
+ retryConnection(): void
749
1046
  }
750
1047
  ```
751
1048
 
@@ -758,6 +1055,27 @@ Benefits:
758
1055
 
759
1056
  ---
760
1057
 
1058
+ ### 15.1 Optional chat-history sidebar
1059
+
1060
+ `ui.showSessionList` (default `false`) renders a minimal sidebar beside the
1061
+ conversation. Contract:
1062
+
1063
+ - the index is per browser: localStorage `<storageKey>:v1:<route>:chats`,
1064
+ session ids only, most recently used first, capped at 50;
1065
+ - rows are the index intersected with the Host session list, so shared-host
1066
+ users never see each other's chats;
1067
+ - switching goes through the same bind + `secureSession` attestation path as
1068
+ restore; a missing id is pruned and reported; an attestation failure keeps
1069
+ the index entry and surfaces the generic configuration error;
1070
+ - `New chat` in the sidebar follows the same `allowSessionReset` gate as the
1071
+ header control; the previous chat stays in the index;
1072
+ - each row carries a two-click delete control: the first click arms it, the
1073
+ second removes the chat from this browser's index (the Host session stays —
1074
+ DSH 0.1.x has no session-deletion seam); deleting the open chat continues
1075
+ in a fresh attested session when resets are allowed;
1076
+ - the sidebar hides below 600px viewports and under `fixed` policy it renders
1077
+ without the new-chat control and never switches.
1078
+
761
1079
  ## 16. Session creation / restore algorithm
762
1080
 
763
1081
  ### 16.1 `browser-persistent`
@@ -809,10 +1127,22 @@ The DSH Session is the authoritative transcript store.
809
1127
 
810
1128
  ### 16.3 Reset
811
1129
 
812
- Reset means:
1130
+ In default locked-down mode:
1131
+
1132
+ ```yaml
1133
+ lockdown:
1134
+ allowSessionReset: false
1135
+ ui:
1136
+ showReset: false
1137
+ ```
1138
+
1139
+ so the QA user gets one persistent conversation and cannot create arbitrary additional sessions from the surface.
1140
+
1141
+ If an operator explicitly enables Reset, it means:
813
1142
 
814
1143
  - stop active generation if needed;
815
1144
  - create a fresh DSH Session using the configured workspace/preset;
1145
+ - apply and verify the locked QA permission policy before enabling Send;
816
1146
  - switch controller binding to it;
817
1147
  - replace stored session id;
818
1148
  - do not delete the old DSH Session automatically.
@@ -902,23 +1232,23 @@ Output:
902
1232
  ```ts
903
1233
  type QaMessage =
904
1234
  | {
905
- id: string;
906
- role: "user";
907
- text: string;
908
- status: "committed";
1235
+ id: string
1236
+ role: 'user'
1237
+ text: string
1238
+ status: 'committed'
909
1239
  }
910
1240
  | {
911
- id: string;
912
- role: "assistant";
913
- text: string;
914
- status: "streaming" | "committed" | "failed";
1241
+ id: string
1242
+ role: 'assistant'
1243
+ text: string
1244
+ status: 'streaming' | 'committed' | 'failed'
915
1245
  }
916
1246
  | {
917
- id: string;
918
- role: "system";
919
- text: string;
920
- status: "info" | "error";
921
- };
1247
+ id: string
1248
+ role: 'system'
1249
+ text: string
1250
+ status: 'info' | 'error'
1251
+ }
922
1252
  ```
923
1253
 
924
1254
  ### 18.1 Visible by default
@@ -932,7 +1262,8 @@ Render:
932
1262
 
933
1263
  ### 18.2 Hidden by default
934
1264
 
935
- Do not render:
1265
+ Unless the corresponding operator-controlled work-detail option is enabled, do
1266
+ not render:
936
1267
 
937
1268
  - reasoning chunks;
938
1269
  - tool call arguments;
@@ -953,16 +1284,22 @@ Optional config:
953
1284
  ```yaml
954
1285
  ui:
955
1286
  showToolActivity: true
1287
+ showReasoning: true
956
1288
  ```
957
1289
 
958
- may display only generic state such as:
1290
+ projects one work disclosure for each DSH turn. While the turn is running the
1291
+ disclosure is expanded and streams available reasoning/tool state. Once the
1292
+ turn completes it collapses before the final answer to a duration summary such
1293
+ as:
959
1294
 
960
1295
  ```text
961
- Searching knowledge…
962
- Working…
1296
+ Worked for 1m 24s
963
1297
  ```
964
1298
 
965
- Do not expose raw tool names/arguments unless a later explicit feature is designed.
1299
+ Expanding it shows reasoning, intermediate assistant progress, and tool rows.
1300
+ Tool rows may independently reveal their formatted arguments and results.
1301
+ These flags are presentation-only: they must not add tools, weaken
1302
+ `lockdown.toolPolicy`, or expose system/context events and raw Host failures.
966
1303
 
967
1304
  ---
968
1305
 
@@ -1029,9 +1366,13 @@ interaction:
1029
1366
 
1030
1367
  Recommended deployment architecture:
1031
1368
 
1032
- - choose an agent/permission preset appropriate for the QA assistant;
1033
- - avoid tools that unexpectedly require developer approval;
1034
- - grant only the minimum non-interactive permissions intentionally configured by the operator.
1369
+ - force the QA session to the dedicated `qa-read-only` permission bundle;
1370
+ - force `approval=never`;
1371
+ - remove tools that require escalation rather than allowing an approval flow;
1372
+ - use an allow-list of reviewed read-only tools;
1373
+ - grant no mutable external-service permissions to the QA agent.
1374
+
1375
+ In locked-down mode, an approval request is treated as a policy/configuration failure or an unsupported operation, not as something the QA user may approve.
1035
1376
 
1036
1377
  If the Session enters an unsupported pending interaction state:
1037
1378
 
@@ -1100,14 +1441,14 @@ Map internal failures to QA-safe UI errors.
1100
1441
 
1101
1442
  Examples:
1102
1443
 
1103
- | Internal class | QA message |
1104
- | ------------------------------ | --------------------------------------------- |
1105
- | session creation rejected | "Unable to start a chat." |
1106
- | prompt rejected | "Your message could not be sent." |
1107
- | model route unavailable | "The assistant is temporarily unavailable." |
1108
- | connection lost | "Connection lost. Reconnecting…" |
1109
- | unsupported approval | "This request requires operator interaction." |
1110
- | malformed persisted session id | silently clear and create a new session |
1444
+ | Internal class | QA message |
1445
+ |---|---|
1446
+ | session creation rejected | "Unable to start a chat." |
1447
+ | prompt rejected | "Your message could not be sent." |
1448
+ | model route unavailable | "The assistant is temporarily unavailable." |
1449
+ | connection lost | "Connection lost. Reconnecting…" |
1450
+ | unsupported approval | "This request requires operator interaction." |
1451
+ | malformed persisted session id | silently clear and create a new session |
1111
1452
 
1112
1453
  Never render:
1113
1454
 
@@ -1216,6 +1557,35 @@ If iframe embedding is later enabled:
1216
1557
 
1217
1558
  ---
1218
1559
 
1560
+ ### 26.5 Capability-change endpoints
1561
+
1562
+ The QA browser bundle must not intentionally call mutable control-plane APIs for:
1563
+
1564
+ - settings writes;
1565
+ - plugin enable/disable/config changes;
1566
+ - agent preset authoring/deletion/default changes;
1567
+ - permission preset switching;
1568
+ - sandbox mode changes;
1569
+ - approval policy changes;
1570
+ - workspace rebinding;
1571
+ - model/provider switching;
1572
+ - arbitrary session deletion/rename/import.
1573
+
1574
+ If a public read API is needed to verify the effective state, use it read-only and project only the minimum information required by the QA controller.
1575
+
1576
+ ### 26.6 UI disabled state is defense-in-depth only
1577
+
1578
+ For every forbidden control that might accidentally appear because of future DSH composition changes:
1579
+
1580
+ - do not render it in the QA component;
1581
+ - if inherited UI becomes reachable, make it inaccessible/inert;
1582
+ - keep controller actions absent, not merely disabled;
1583
+ - add E2E tests proving no selector/menu/command can alter the locked policy.
1584
+
1585
+ The absence of a button is not considered enforcement.
1586
+
1587
+ ---
1588
+
1219
1589
  ## 27. Operator vs QA-user separation
1220
1590
 
1221
1591
  Desired deployment:
@@ -1311,8 +1681,27 @@ Illustrative only; exact Loader syntax must match the deployment.
1311
1681
  policy: browser-persistent
1312
1682
  workspaceId: company-knowledge
1313
1683
  agentPreset: qa-assistant
1684
+ provider: null
1685
+ model: null
1686
+ lockdown:
1687
+ enabled: true
1688
+ enforceFixedAgentPreset: true
1689
+ enforceFixedWorkspace: true
1690
+ enforceFixedModel: true
1691
+ sandboxMode: read-only
1692
+ approvalPolicy: never
1693
+ permissionPreset: qa-read-only
1694
+ allowPermissionChanges: false
1695
+ allowSlashCommands: false
1696
+ allowSettingsMutation: false
1697
+ allowSessionReset: false
1698
+ allowArbitrarySessionOpen: false
1699
+ toolPolicy:
1700
+ mode: allow-list
1701
+ allow:
1702
+ - <reviewed-read-only-tool>
1314
1703
  ui:
1315
- showReset: true
1704
+ showReset: false
1316
1705
  showStop: true
1317
1706
  showToolActivity: false
1318
1707
  showReasoning: false
@@ -1331,11 +1720,13 @@ Prefer a DSH agent preset conceptually like:
1331
1720
  ```text
1332
1721
  qa-assistant
1333
1722
  ├── system prompt / persona
1334
- ├── knowledge search tool
1335
- ├── Jira/Confluence MCP if allowed
1723
+ ├── reviewed read-only knowledge/search tools only
1724
+ ├── read-only Jira/Confluence MCP actions only, if needed
1336
1725
  ├── selected skills
1337
- ├── safe permission preset
1338
- └── model policy
1726
+ ├── no shell / terminal / generic code runtime by default
1727
+ ├── no mutating external-service actions
1728
+ ├── qa-read-only permission policy
1729
+ └── fixed model policy, if deployment requires it
1339
1730
  ```
1340
1731
 
1341
1732
  This makes the same assistant reusable from:
@@ -1486,7 +1877,10 @@ Use an existing external browser UI plugin as a packaging reference if necessary
1486
1877
  - invalid `/api` route rejected;
1487
1878
  - invalid session policy rejected;
1488
1879
  - fixed policy requires session id;
1489
- - empty title/placeholder behavior defined.
1880
+ - empty title/placeholder behavior defined;
1881
+ - lockdown defaults to enabled;
1882
+ - locked-down default disables session reset;
1883
+ - `qa-read-only` selection is required before first prompt when lockdown is enabled.
1490
1884
 
1491
1885
  #### Route controller
1492
1886
 
@@ -1507,7 +1901,11 @@ Use an existing external browser UI plugin as a packaging reference if necessary
1507
1901
  - accepted send clears draft at correct time;
1508
1902
  - rejected send preserves draft;
1509
1903
  - stop calls public DSH cancellation path;
1510
- - unsupported pending interaction blocks composer.
1904
+ - unsupported pending interaction blocks composer;
1905
+ - agent/workspace/model mismatch blocks composer;
1906
+ - non-`read-only` sandbox state blocks composer;
1907
+ - approval policy other than `never` blocks composer;
1908
+ - lockdown config cannot be weakened through browser-local state.
1511
1909
 
1512
1910
  #### Transcript adapter
1513
1911
 
@@ -1543,17 +1941,55 @@ Verify:
1543
1941
  7. tool execution can occur without tool details becoming visible;
1544
1942
  8. reload restores the same session under browser-persistent policy;
1545
1943
  9. normal DSH UI can inspect the QA-created session;
1546
- 10. `/qa` does not open a second server/connection stack.
1944
+ 10. `/qa` does not open a second server/connection stack;
1945
+ 11. every QA session starts with the configured agent preset;
1946
+ 12. every QA session starts/effectively runs as `read-only`;
1947
+ 13. approval policy is `never`;
1948
+ 14. model/workspace selectors are absent;
1949
+ 15. permission selectors/commands are absent;
1950
+ 16. a denied write attempt cannot escalate;
1951
+ 17. a newly installed global tool not present in the allow-list is not visible to the QA agent.
1547
1952
 
1548
- ### 37.4 Security tests
1953
+ ### 37.4 Security / lockdown tests
1549
1954
 
1550
1955
  - route does not weaken `/api` trust checks;
1551
1956
  - config endpoint/settings do not leak secrets;
1552
1957
  - raw tool result not visible;
1553
1958
  - stack traces not visible;
1554
1959
  - unsupported approval not auto-approved;
1960
+ - `approval=never` rejects escalation deterministically;
1961
+ - sandbox effective mode remains `read-only`;
1962
+ - QA user cannot invoke `/permission` or equivalent command dispatch;
1963
+ - QA user cannot change agent preset, workspace, provider, model, or reasoning mode;
1964
+ - QA user cannot mutate DSH settings/plugin config;
1965
+ - QA user cannot reset/start additional sessions when `allowSessionReset=false`;
1966
+ - QA user cannot open arbitrary existing sessions when `allowArbitrarySessionOpen=false`;
1967
+ - write/edit/delete filesystem tools are absent or denied;
1968
+ - shell/terminal/code-runtime are absent by default;
1969
+ - mutable MCP actions are absent;
1970
+ - allow-listed MCP read actions still work;
1971
+ - adding a new global write-capable tool does not implicitly expose it to QA;
1972
+ - a preset-local tool is reviewed separately because scoped registrations may bypass inherited `ToolRestriction`;
1973
+ - direct malformed/browser-local attempts to weaken lockdown fail closed;
1555
1974
  - cross-origin behavior remains blocked unless deployment explicitly changes it.
1556
1975
 
1976
+ ### 37.5 Side-effect regression suite
1977
+
1978
+ Maintain a table of every tool visible to `qa-assistant` and classify it:
1979
+
1980
+ ```text
1981
+ read-only / pure
1982
+ external-read
1983
+ filesystem-write
1984
+ external-write
1985
+ privilege/control-plane
1986
+ unknown
1987
+ ```
1988
+
1989
+ CI must fail when a visible QA tool is `filesystem-write`, `external-write`, `privilege/control-plane`, or `unknown` unless the spec/config is intentionally updated and reviewed.
1990
+
1991
+ This guards against future plugin installs silently expanding the QA agent's capability surface.
1992
+
1557
1993
  ### 37.5 Mobile tests
1558
1994
 
1559
1995
  At minimum test:
@@ -1804,17 +2240,61 @@ Mitigation:
1804
2240
 
1805
2241
  ---
1806
2242
 
2243
+ ### Risk: `read-only` is mistaken for “no side effects”
2244
+
2245
+ DSH's read-only sandbox covers file mutation, not all process/network effects.
2246
+
2247
+ Mitigation:
2248
+
2249
+ - deny generic shell/terminal/code runtime by default;
2250
+ - explicit tool allow-list;
2251
+ - tool-by-tool MCP review;
2252
+ - `approval=never`;
2253
+ - optional network egress restrictions at container/proxy level.
2254
+
2255
+ ### Risk: QA user can reach normal DSH Web
2256
+
2257
+ If `/` and `/qa` share the same privileged identity, the user may bypass UI lockdown by opening the normal operator surface.
2258
+
2259
+ Mitigation:
2260
+
2261
+ - do not claim `/qa` alone is an authorization boundary;
2262
+ - for untrusted users use separate auth roles/gateway or a dedicated QA DSH instance;
2263
+ - preferably expose only the QA surface to the QA audience.
2264
+
2265
+ ### Risk: global plugin installation expands tool surface
2266
+
2267
+ Mitigation:
2268
+
2269
+ - allow-list inherited tools;
2270
+ - inspect preset-local registrations separately;
2271
+ - CI capability inventory;
2272
+ - fail closed on unknown tools where practical.
2273
+
2274
+ ### Risk: read access is broader than intended
2275
+
2276
+ Current DSH read-only mode does not imply workspace-only read isolation.
2277
+
2278
+ Mitigation:
2279
+
2280
+ - no generic shell/fs browsing in the default QA preset;
2281
+ - dedicated read-only corpus/workspace;
2282
+ - minimal container mounts;
2283
+ - dedicated QA container for high-assurance deployments.
2284
+
2285
+ ---
2286
+
1807
2287
  ## 43. Future evolution
1808
2288
 
1809
2289
  Potential roadmap:
1810
2290
 
1811
2291
  ```text
1812
- 0.1 full-screen /qa overlay + one DSH session
1813
- 0.2 settings + branding + suggested questions
1814
- 0.3 user questions / approval UI
1815
- 0.4 attachments + citations + feedback
1816
- 0.5 iframe/embed SDK
1817
- 1.0 dedicated qa profile/bundle option
2292
+ 0.1 full-screen /qa overlay + one DSH session + hard lockdown
2293
+ 0.2 settings + branding + suggested questions + capability inventory
2294
+ 0.3 read-only sources/citations + optional supported questions UI
2295
+ 0.4 attachments + feedback (without capability escalation)
2296
+ 0.5 iframe/embed SDK + auth-role integration
2297
+ 1.0 dedicated qa profile/bundle / isolated QA deployment option
1818
2298
  ```
1819
2299
 
1820
2300
  Long term the project can become a reusable **DSH-powered assistant frontend** rather than merely an alternative theme.
@@ -1837,43 +2317,65 @@ Current architecture was checked against upstream DeepSeek Harness sources/docs
1837
2317
 
1838
2318
  Key references:
1839
2319
 
1840
- - DeepSeek Harness repository / developer-preview status
2320
+ - DeepSeek Harness repository / developer-preview status
1841
2321
  https://github.com/deepseek-ai/deepseek-harness
1842
2322
 
1843
- - Web app bundle composition (`dsh-base` + browser roster)
2323
+ - Web app bundle composition (`dsh-base` + browser roster)
1844
2324
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/web-app/cordis.patch.yml
1845
2325
 
1846
- - Web app bundle README
2326
+ - Web app bundle README
1847
2327
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/web-app/README.md
1848
2328
 
1849
- - Client modules / `dsh.client` / `window.__DSH_BOOT__`
2329
+ - Client modules / `dsh.client` / `window.__DSH_BOOT__`
1850
2330
  https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/client-modules.md
1851
2331
 
1852
- - Host webserver route/fallback model
2332
+ - Host webserver route/fallback model
1853
2333
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/host/webserver/README.md
1854
2334
 
1855
- - SPA frontend-static fallback
2335
+ - SPA frontend-static fallback
1856
2336
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/host/frontend-static/README.md
1857
2337
 
1858
- - Client runtime / Host-born sessions
2338
+ - Client runtime / Host-born sessions
1859
2339
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/runtime/README.md
1860
2340
 
1861
- - Session Controller
2341
+ - Session Controller
1862
2342
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/api/session-controller/README.md
1863
2343
 
1864
- - Web client slots
2344
+ - Sandbox policy / `read-only` semantics
2345
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/sandbox/sandbox-policy/README.md
2346
+
2347
+ - Approval policy / `never` fail-closed behavior
2348
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/approval.md
2349
+
2350
+ - Permission presets (`sandbox` + `approval`)
2351
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/permission-presets.md
2352
+
2353
+ - Tool restrictions / allow-list semantics
2354
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/tools.md
2355
+
2356
+ - Agent preset session semantics
2357
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-agent-preset/README.md
2358
+
2359
+ - DSH safety limitations
2360
+ https://github.com/deepseek-ai/deepseek-harness/blob/master/SAFETY.md
2361
+
2362
+ - Current read-isolation limitation discussion
2363
+ https://github.com/deepseek-ai/deepseek-harness/discussions/492
2364
+
2365
+
2366
+ - Web client slots
1865
2367
  https://deepseek-harness.github.io/deepseek-harness/en/reference/subsystems/slots
1866
2368
 
1867
- - Conversation slot contract / `conversation.session`
2369
+ - Conversation slot contract / `conversation.session`
1868
2370
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-conversation/src/client/contract/slots.ts
1869
2371
 
1870
- - UI layout shell / `shell.overlay`
2372
+ - UI layout shell / `shell.overlay`
1871
2373
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-layout/README.md
1872
2374
 
1873
- - Web client package rules / new UI plugin checklist
2375
+ - Web client package rules / new UI plugin checklist
1874
2376
  https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/AGENTS.md
1875
2377
 
1876
- - Settings-card cookbook / external dual-face plugin pattern
2378
+ - Settings-card cookbook / external dual-face plugin pattern
1877
2379
  https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cookbook/adding-a-settings-card.md
1878
2380
 
1879
2381
  ### Upstream limitation worth tracking
@@ -1914,8 +2416,10 @@ At the time of writing there is no generic first-class global-page extension con
1914
2416
  textarea / Send
1915
2417
  ```
1916
2418
 
1917
- The core architectural principle is:
2419
+ The core architectural principles are:
1918
2420
 
1919
2421
  > **Replace the presentation, not the harness.**
1920
2422
 
1921
- `dsh-qa-surface` should be a thin QA/browser surface over the existing DSH runtime, with `/qa` acting as a dedicated end-user presentation while `/` remains the full operator/developer interface.
2423
+ > **Remove capabilities at the runtime/tool-policy layer; hiding buttons is only defense in depth.**
2424
+
2425
+ `dsh-qa-surface` should be a thin QA/browser surface over the existing DSH runtime, with `/qa` acting as a dedicated end-user presentation while `/` remains the full operator/developer interface. In locked-down deployments, QA sessions must be bound to a fixed composition and a fail-closed read-only capability set.