dsh-env-manager-plugin 0.0.0-stage → 0.1.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -2
  3. package/client.js +2581 -0
  4. package/cordis.patch.yml +4 -0
  5. package/docs/00-project/current-state.md +246 -0
  6. package/docs/00-project/glossary.md +215 -0
  7. package/docs/00-project/project-brief.md +114 -0
  8. package/docs/00-project/roadmap.md +89 -0
  9. package/docs/01-architecture/DSH_Environment_Service_Memory_Skill_Brainstorm.md +493 -0
  10. package/docs/01-architecture/api-verification.md +416 -0
  11. package/docs/01-architecture/architecture-overview.md +222 -0
  12. package/docs/01-architecture/data-model.md +930 -0
  13. package/docs/01-architecture/decisions/ADR-0001-plugin-as-bundle.md +29 -0
  14. package/docs/01-architecture/decisions/ADR-0002-file-based-storage.md +25 -0
  15. package/docs/01-architecture/decisions/ADR-0003-skill-router-over-search.md +25 -0
  16. package/docs/01-architecture/decisions/ADR-0004-typescript-host-js-client.md +31 -0
  17. package/docs/01-architecture/decisions/ADR-0005-layered-rule-resolution.md +36 -0
  18. package/docs/01-architecture/decisions/ADR-0006-credentials-as-references.md +30 -0
  19. package/docs/01-architecture/decisions/ADR-0007-session-scoped-default-environment.md +28 -0
  20. package/docs/01-architecture/decisions/ADR-0008-memory-bridge-not-second-store.md +31 -0
  21. package/docs/01-architecture/decisions/ADR-0009-client-host-http-transport.md +87 -0
  22. package/docs/01-architecture/decisions/ADR-0010-environment-plane-model.md +87 -0
  23. package/docs/01-architecture/decisions/ADR-0011-k8s-node-execution-model.md +92 -0
  24. package/docs/01-architecture/design.md +666 -0
  25. package/docs/01-architecture/detailed-design.md +961 -0
  26. package/docs/01-architecture/implementation-plan.md +323 -0
  27. package/docs/01-architecture/module-boundaries.md +306 -0
  28. package/docs/01-architecture/security-model.md +227 -0
  29. package/docs/01-architecture/storage-contract.md +224 -0
  30. package/docs/01-architecture/tool-contract.md +438 -0
  31. package/docs/01-architecture/ui-design.md +587 -0
  32. package/docs/02-harness/agent-behavior.md +36 -0
  33. package/docs/02-harness/change-snapshot-policy.md +29 -0
  34. package/docs/02-harness/client-ui-integration.md +110 -0
  35. package/docs/02-harness/context-policy.md +43 -0
  36. package/docs/02-harness/harness-standard.md +91 -0
  37. package/docs/02-harness/task-execution-policy.md +26 -0
  38. package/docs/02-harness/verification-policy.md +97 -0
  39. package/docs/03-openspec/archive-rule.md +25 -0
  40. package/docs/03-openspec/bugfix-rule.md +30 -0
  41. package/docs/03-openspec/design-rule.md +33 -0
  42. package/docs/03-openspec/openspec-usage-guide.md +31 -0
  43. package/docs/03-openspec/proposal-rule.md +70 -0
  44. package/docs/03-openspec/spec-rule.md +33 -0
  45. package/docs/03-openspec/tasks-rule.md +31 -0
  46. package/docs/04-development/coding-style.md +67 -0
  47. package/docs/04-development/dependency-policy.md +43 -0
  48. package/docs/04-development/development-guide.md +85 -0
  49. package/docs/04-development/directory-convention.md +105 -0
  50. package/docs/04-development/local-run-guide.md +55 -0
  51. package/docs/04-development/testing-guide.md +102 -0
  52. package/docs/05-domain/deployment-template.md +76 -0
  53. package/docs/05-domain/environment-template.md +165 -0
  54. package/docs/05-domain/operation-binding-template.md +55 -0
  55. package/docs/05-domain/operation-record-template.md +55 -0
  56. package/docs/05-domain/prompt-template.md +90 -0
  57. package/docs/05-domain/rule-template.md +73 -0
  58. package/docs/05-domain/service-template.md +39 -0
  59. package/docs/06-operations/deployment-guide.md +54 -0
  60. package/docs/06-operations/release.md +188 -0
  61. package/docs/06-operations/security-policy.md +53 -0
  62. package/docs/06-operations/troubleshooting.md +91 -0
  63. package/docs/07-templates/adr-template.md +21 -0
  64. package/docs/07-templates/bugfix-template.md +11 -0
  65. package/docs/07-templates/case-template.md +15 -0
  66. package/docs/07-templates/design-template.md +29 -0
  67. package/docs/07-templates/proposal-template.md +19 -0
  68. package/docs/07-templates/spec-template.md +15 -0
  69. package/docs/07-templates/tasks-template.md +12 -0
  70. package/docs/99-archive/README.md +13 -0
  71. package/docs/README.md +142 -0
  72. package/icon.svg +4 -0
  73. package/index.js +8 -0
  74. package/lib/app/app.d.ts +67 -0
  75. package/lib/app/app.js +494 -0
  76. package/lib/app/app.js.map +1 -0
  77. package/lib/config.d.ts +55 -0
  78. package/lib/config.js +42 -0
  79. package/lib/config.js.map +1 -0
  80. package/lib/domain/constants.d.ts +21 -0
  81. package/lib/domain/constants.js +59 -0
  82. package/lib/domain/constants.js.map +1 -0
  83. package/lib/domain/errors.d.ts +20 -0
  84. package/lib/domain/errors.js +22 -0
  85. package/lib/domain/errors.js.map +1 -0
  86. package/lib/domain/schemas.d.ts +28 -0
  87. package/lib/domain/schemas.js +445 -0
  88. package/lib/domain/schemas.js.map +1 -0
  89. package/lib/domain/types.d.ts +412 -0
  90. package/lib/domain/types.js +2 -0
  91. package/lib/domain/types.js.map +1 -0
  92. package/lib/index.d.ts +18 -0
  93. package/lib/index.js +89 -0
  94. package/lib/index.js.map +1 -0
  95. package/lib/logger.d.ts +8 -0
  96. package/lib/logger.js +13 -0
  97. package/lib/logger.js.map +1 -0
  98. package/lib/memory/bridge.d.ts +18 -0
  99. package/lib/memory/bridge.js +13 -0
  100. package/lib/memory/bridge.js.map +1 -0
  101. package/lib/registry/integrity.d.ts +10 -0
  102. package/lib/registry/integrity.js +204 -0
  103. package/lib/registry/integrity.js.map +1 -0
  104. package/lib/registry/registry.d.ts +14 -0
  105. package/lib/registry/registry.js +182 -0
  106. package/lib/registry/registry.js.map +1 -0
  107. package/lib/resolver/merge.d.ts +23 -0
  108. package/lib/resolver/merge.js +90 -0
  109. package/lib/resolver/merge.js.map +1 -0
  110. package/lib/resolver/resolve.d.ts +18 -0
  111. package/lib/resolver/resolve.js +288 -0
  112. package/lib/resolver/resolve.js.map +1 -0
  113. package/lib/resolver/template.d.ts +10 -0
  114. package/lib/resolver/template.js +42 -0
  115. package/lib/resolver/template.js.map +1 -0
  116. package/lib/router/params.d.ts +3 -0
  117. package/lib/router/params.js +83 -0
  118. package/lib/router/params.js.map +1 -0
  119. package/lib/router/router.d.ts +11 -0
  120. package/lib/router/router.js +204 -0
  121. package/lib/router/router.js.map +1 -0
  122. package/lib/session/session.d.ts +16 -0
  123. package/lib/session/session.js +64 -0
  124. package/lib/session/session.js.map +1 -0
  125. package/lib/skill.d.ts +17 -0
  126. package/lib/skill.js +56 -0
  127. package/lib/skill.js.map +1 -0
  128. package/lib/storage/atomic.d.ts +10 -0
  129. package/lib/storage/atomic.js +107 -0
  130. package/lib/storage/atomic.js.map +1 -0
  131. package/lib/storage/paths.d.ts +10 -0
  132. package/lib/storage/paths.js +50 -0
  133. package/lib/storage/paths.js.map +1 -0
  134. package/lib/storage/records.d.ts +16 -0
  135. package/lib/storage/records.js +112 -0
  136. package/lib/storage/records.js.map +1 -0
  137. package/lib/storage/sessions.d.ts +17 -0
  138. package/lib/storage/sessions.js +65 -0
  139. package/lib/storage/sessions.js.map +1 -0
  140. package/lib/tools/tools.d.ts +14 -0
  141. package/lib/tools/tools.js +472 -0
  142. package/lib/tools/tools.js.map +1 -0
  143. package/lib/transport/http.d.ts +3 -0
  144. package/lib/transport/http.js +217 -0
  145. package/lib/transport/http.js.map +1 -0
  146. package/locale/en.json +6 -0
  147. package/locale/zh.json +6 -0
  148. package/package.json +97 -4
@@ -0,0 +1,587 @@
1
+ # UI Design (V1)
2
+
3
+ > Status: normative for the Client half. Host behavior is in
4
+ > [`detailed-design.md`](detailed-design.md); transport and authentication are
5
+ > §4 here and §12 there.
6
+
7
+ ## 1. Goals
8
+
9
+ The V1 UI makes the registry **inspectable and editable** without asking the agent to
10
+ run a tool, and makes rule resolution **explainable** rather than magical.
11
+
12
+ It is not a CMDB dashboard: no charts, no topology graph, no Skill execution button.
13
+ Bulk import is limited to the node line format below. Every other screen answers one
14
+ question and offers focused write actions.
15
+
16
+ ## 2. Constraints
17
+
18
+ - Plain JavaScript `client.js`; no bundler, no JSX, no npm imports (F13).
19
+ - React comes from `require('react')`; elements via `React.createElement`.
20
+ - Only `slots` and `locale` services (F16).
21
+ - All data through `POST /env-manager/api` (F14, ADR-0009).
22
+ - All colors from `--dsw-alias-*` tokens (F17).
23
+ - No credential value is rendered or persisted. Environment-shell creation collects no
24
+ password. The add/edit node form may collect a login password transiently and send it
25
+ once to the Host, which forwards it to `ctx.credentials.set`; the registry keeps only
26
+ `credentialRef`. Editing a node with both login fields empty never touches existing
27
+ accounts or credentials.
28
+
29
+ ## 3. Information architecture
30
+
31
+ ```text
32
+ Sidebar
33
+ └── Environment Manager (id: dsh-env-manager, order 40)
34
+ └── main panel (key: dsh-env-manager)
35
+ ├── Environments (default; list, detail, New environment)
36
+ ├── Overview (counts, runtime, warnings, getting started, validation)
37
+ ├── Records (Services, Deployments, Rule Profiles, Operation Bindings)
38
+ ├── Context Preview
39
+ └── Operation Preview
40
+ ```
41
+
42
+ Sections are a horizontal tab strip inside the panel, not separate sidebar entries. The
43
+ sidebar entry is the only global surface; the panel fills the same region as
44
+ `conversation`. **Environments is the default section** because host and account
45
+ management is the primary job; the panel header also carries a persistent
46
+ `New environment` action. Records is for the remaining configuration kinds and no longer
47
+ offers environment creation.
48
+
49
+ ### 3.1 Sidebar slot contract
50
+
51
+ The sidebar is a host-owned list slot, not a free-form navigation area. The entry must
52
+ follow the installed `sidebar.panellist` contract (verified against
53
+ `@deepseek-ai/dsh-client-ui-sidebar` in this runtime). The full reusable contract is in
54
+ [`client-ui-integration.md`](../02-harness/client-ui-integration.md); the project-specific
55
+ rules are:
56
+
57
+ 1. Register a list entry on the root context:
58
+
59
+ ```js
60
+ ctx.slots.inject('sidebar.panellist', () =>
61
+ ctx.slots.register({
62
+ name: 'sidebar.panellist',
63
+ id: 'dsh-env-manager',
64
+ order: 40,
65
+ label: () => t('title'), // thunk: re-read on every locale projection
66
+ }, SidebarGlyph),
67
+ )
68
+ ```
69
+
70
+ `name` names the slot for the registration; `id` is the row key and must equal the
71
+ `main` key. `label` is the row text and tooltip; use a thunk so a language switch does
72
+ not require re-registration. Do not register a second entry for panel sections.
73
+
74
+ 2. The owner renders the second registration argument inside its own
75
+ `span.panelGlyph` and passes exactly `{ size, active }`:
76
+ - `size` is `16` when the sidebar is wide and `18` in the collapsed rail;
77
+ - `active` is the selected-page state.
78
+
79
+ The glyph must render an inline `<svg>` with `viewBox="0 0 20 20"`,
80
+ `width={size}`, `height={size}`, `fill="none"`, and `stroke="currentColor"`.
81
+ Use `opacity: 0.72` when `active === false`; do not set a fixed pixel size,
82
+ background, border, radius, margin, or padding. The host row already supplies the
83
+ button, hit area, hover, focus, tooltip, and `aria-current`.
84
+
85
+ 3. The glyph is decorative: set `aria-hidden="true"` and do not put an `aria-label` or
86
+ tooltip on it. The row button owns the accessible name from `label`.
87
+
88
+ 4. Register the page under `main` on the root context with the same key:
89
+
90
+ ```js
91
+ ctx.slots.inject('main', () =>
92
+ ctx.slots.register({ name: 'main', key: 'dsh-env-manager', inject: () => ({}) }, Panel),
93
+ )
94
+ ```
95
+
96
+ A sidebar id without a matching `main` key opens an empty page; a `main` key without
97
+ the sidebar id is unreachable.
98
+
99
+ 5. Keep the glyph monochrome. Brand color belongs to `icon.svg` (the plugin-manager
100
+ card), not to the sidebar row; the sidebar uses host `currentColor` so light and dark
101
+ themes stay consistent with shipped icons.
102
+
103
+ The bundle page (`plugins.bundle.config`, keyed `dsh-env-manager`) shows a read-only
104
+ summary: version, `dataDir`, record counts, last load time, warning count. It links to
105
+ the panel's Diagnostics section.
106
+
107
+ ## 4. Client ↔ Host transport
108
+
109
+ ### 4.1 Request
110
+
111
+ ```js
112
+ const API = '/env-manager/api'
113
+
114
+ async function call(op, payload, signal) {
115
+ const res = await fetch(API, {
116
+ method: 'POST',
117
+ headers: { 'content-type': 'application/json', 'x-env-manager-request': '1' },
118
+ body: JSON.stringify(payload === undefined ? { op } : { op, payload }),
119
+ signal,
120
+ })
121
+ const body = await res.json().catch(() => null)
122
+ if (!body) throw { code: 'NETWORK', message: `HTTP ${res.status}` }
123
+ if (!body.ok) throw body.error
124
+ return body.data
125
+ }
126
+ ```
127
+
128
+ - One helper, one place that knows the URL and header.
129
+ - `signal` is always passed; every screen creates an `AbortController` on mount and
130
+ aborts it on unmount or section switch.
131
+ - Errors are surfaced as `{ code, message, details }`; the UI never renders a stack.
132
+
133
+ ### 4.2 Operations used by the UI
134
+
135
+ `meta`, `record.list`, `record.get`, `record.find`, `record.save`, `record.remove`
136
+ (with `confirm`), `record.validate`, `context.resolve`, `operation.resolve`,
137
+ `diagnostics`. No session-context operation.
138
+
139
+ ### 4.3 Failure presentation
140
+
141
+ | HTTP | UI |
142
+ |---|---|
143
+ | 400/422 | inline field banner with `code` and message |
144
+ | 403 | banner: "This action is not allowed by the current policy" + code |
145
+ | 404 | banner + refresh button |
146
+ | 409 | conflict dialog listing the conflicting ids from `details` |
147
+ | 500 | banner: "Unexpected error", `code`, copy button for the message |
148
+ | network | banner with retry |
149
+
150
+ ## 5. Layout
151
+
152
+ The implemented V1 panel is a centered content column, not a second navigation
153
+ sidebar:
154
+
155
+ ```text
156
+ ┌──────────────────────────────────────────────────────────────┐
157
+ │ [glyph] Environment Manager [New environment]│
158
+ │ Environments, services, deployments, routing. │
159
+ │ │
160
+ │ Environments Overview Records Context Operations │
161
+ │ ──────────── │
162
+ │ Environments [Refresh] │
163
+ │ ┌────────────────────┬───────────────────────────────────┐ │
164
+ │ │ Oracle 1 │ Select an environment, │ │
165
+ │ │ test · 1 host ... │ or add a new one. [New environment]│ │
166
+ │ └────────────────────┴───────────────────────────────────┘ │
167
+ └──────────────────────────────────────────────────────────────┘
168
+ ```
169
+
170
+ - The root is a plugin-owned `div.dem-root` with `height:100%; overflow:auto`; the
171
+ content column is capped at 1080 px and centered.
172
+ - Title 22 px / 650; subtitle 13 px secondary. The header carries identity **and** the
173
+ persistent `New environment` primary action; each view still supplies its own toolbar.
174
+ - Tabs are a horizontal underline strip (`role="tablist"`), never host `nav` styling.
175
+ Environments / Overview / Records / Context / Operations are views. Environments is
176
+ the default; record kinds are a second, segmented control inside Records.
177
+ - Panel background: `--dsw-alias-bg-base`; cards `--dsw-alias-bg-layer-1`; borders
178
+ `--dsw-alias-border-l1`; secondary text `--dsw-alias-label-secondary`.
179
+ - The primary button is filled with `--dsw-alias-brand-primary` and uses
180
+ `--dsw-alias-bg-base` for its label; this stays legible in both themes because the
181
+ brand color and the base background are a contrast pair.
182
+ - Content scrolls with the root. Minimum practical width is 720 px; below 820 px the
183
+ two-column blocks and the record split stack into one column.
184
+ - Avoid bare `<nav>`/`<section>` layout containers: host global styles can impose
185
+ height on them. Use prefixed class names and plugin-owned elements.
186
+
187
+ ## 6. Screens
188
+
189
+ ### 6.1 Overview
190
+
191
+ - Five compact stat cards: Environments, Services, Deployments, Rule Profiles,
192
+ Operation Bindings. Each shows the count only; no per-card diagnostics.
193
+ - Two side-by-side cards: Runtime (`dataDir`, total records, version pill) and Warnings
194
+ (count pill, diagnostic list, or an empty state).
195
+ - `Refresh` reloads `meta`; `Validate` calls `validate` and renders a result card with
196
+ a pass/fail pill and diagnostics.
197
+ - When the registry is empty, a "Get started" card shows the registration order
198
+ (environment → service → deployment → operation binding) and opens the guided
199
+ `New environment` form directly.
200
+
201
+ ### 6.2 Environments
202
+
203
+ - The default section. A toolbar shows the section title, a one-line purpose and
204
+ `Refresh`; the persistent header action `New environment` is always available.
205
+ - Environments use a master/detail flow: the left card lists environments (name, id,
206
+ `category · hosts · accounts · login records`); selecting one opens the grouped detail
207
+ view (§6.3).
208
+ - `New environment` creates only the **environment shell**: name, optional id, category and
209
+ description. It seeds the four standard planes (`management`, `business`, `database`,
210
+ `extension`) and leaves `hosts`, `clusters`, `endpoints` and `loginRecords` empty. Nodes
211
+ are added inside the environment, from the plane they belong to. The shell form never
212
+ asks for a password.
213
+ - Empty lists show a dashed empty state with `New environment` and `Refresh`.
214
+ - There is no global search across environments in V1; the `record.find` HTTP operation
215
+ remains available to future screens.
216
+
217
+ ### 6.2a Records
218
+
219
+ - A kind segmented control for Services, Deployments, Rule Profiles and Operation
220
+ Bindings only. Environments are not listed here.
221
+ - `New record` loads the selected kind's YAML template into the editor; `Refresh`
222
+ reloads the list. This is the advanced path for non-environment records.
223
+ - Empty lists show a dashed empty state with a short hint, not a blank card.
224
+
225
+ ### 6.3 Environment detail
226
+
227
+ ```text
228
+ ← Back to environments
229
+ [glyph] Oracle 1 [Edit YAML] [Delete]
230
+ oracle1 [test] ● 4 hosts · 1 cluster · 5 accounts
231
+ Environment overview | All nodes | Management | Business | Database |
232
+ Expansion / other | Login records | Kubernetes / clusters | Endpoints | YAML
233
+ ┌────────────────────────────────────┬──────────────────────────┐
234
+ │ Basic information │ Access summary │
235
+ │ ID / category / name / aliases │ 4 hosts 5 accounts │
236
+ │ description / created / updated │ 1 endpoint 0 failed │
237
+ │ hosts / accounts │ latest login │
238
+ │ Plane distribution │ Roles │
239
+ │ [Management 2] [Business 2] ... │ [management] [business] │
240
+ │ All nodes │ │
241
+ │ [Oracle1] [db-01] [db-02] ... │ │
242
+ └────────────────────────────────────┴──────────────────────────┘
243
+ ```
244
+
245
+ - Tabs are `Environment overview`, `All nodes`, one tab per declared role (`Management`,
246
+ `Business`, `Database`, `Expansion / other`), `Login records`, `Kubernetes / clusters`,
247
+ `Endpoints`, `YAML`. The identity row and the back link stay visible above the tabs; tabs
248
+ wrap rather than hide behind a horizontal scrollbar.
249
+ - The environment is **one network and one YAML document**. Plane tabs are filtered views
250
+ over `hosts[]`: the same node appears in every plane it serves, and no record is copied
251
+ per plane. A plane with zero nodes is valid and shows a plane-specific empty state with
252
+ `Add node`.
253
+ - Overview basic information is a two-column key/value grid. `Edit info` opens an inline
254
+ form for name, aliases, category and description; the ID is immutable. Saving sends the
255
+ full structured record through `save` (`record`, not `yaml`) and then re-reads it.
256
+ - Access summary counts hosts, accounts, endpoints, clusters, Kubernetes/Pod nodes and
257
+ failed logins; the latest login time and declared roles sit in the right rail. The role
258
+ card shows node counts per plane and the four standard roles even when the environment
259
+ declares only some of them.
260
+ - `All nodes` and every plane tab share one node view:
261
+ - toolbar: search box (id, name, IP, platform, admin user, role, kind, access
262
+ method, cluster name/reference, account username, purpose or web login URL),
263
+ `Add node`, `Batch import`;
264
+ - one compact card per host: name, active state, a kind badge, command-access badges,
265
+ IP/port, platform, admin user, account count, cluster reference, credential reference
266
+ and role chips;
267
+ - `Edit node` opens the same form prefilled; `Accounts (n)` expands the account table;
268
+ `Add account` opens the account form for that host; `Add web login` opens the same form
269
+ with protocol `web` preselected.
270
+ - `Add node` / `Edit node` form fields: id (auto-slug when blank), name, node kind,
271
+ Kubernetes cluster, IP, SSH port, platform, admin user, credential reference, role
272
+ checkboxes (at least one), command-access checkboxes (`ssh`, `kubectl`, `docker`) and an
273
+ optional login username + password. Changing the kind pre-fills that kind's default
274
+ access set; the operator can override it. `k8s-all-in-one` and `k8s-worker` require an
275
+ existing cluster; `pod-host` may omit one for a Docker-only machine. Username and password
276
+ must be filled together; when present the password is written to `ctx.credentials.set`
277
+ and the account/credential ref is `env-manager/<environmentId>/<hostId>-<username>`.
278
+ Editing a node preserves its existing accounts; filling username + password again adds a
279
+ new password account while leaving the existing accounts and host credential reference
280
+ intact. Opening the form from a plane tab preselects that role.
281
+ - `Add account` / `Edit account` form fields: id (auto-slug when blank, immutable after
282
+ creation), username, purpose, protocol, web login URL, auth type, password, credential
283
+ reference, latest login and source. A web login URL must start with `http://` or
284
+ `https://`; the Host schema also rejects non-http(s) values. It is shown as a link under
285
+ the account username. A non-empty password is
286
+ written to the DSH credential service and the account keeps only the generated
287
+ `credentialRef`; leaving the password blank preserves the existing reference. Accounts
288
+ can be edited in place, so a web URL or password can be changed without adding a
289
+ duplicate account.
290
+ - `Batch import` accepts one node per line:
291
+ `IP,name,roles,port,user`; multiple roles use `|` or `/`, for example
292
+ `10.20.30.53,app-01,business|management,22,root`. Import creates generic `vm` nodes with
293
+ `access: [ssh]` and optional accounts but never passwords; Kubernetes/Pod-host nodes are
294
+ edited after import. The batch parser rejects unknown roles, duplicate IPs, duplicate host
295
+ ids and invalid ports, and reports the first offending line without saving a partial batch.
296
+ - `Login records` sorts newest-first and shows time, result (success/failure), user, host,
297
+ source IP, protocol and note. `Add login record` opens an inline form with host/account
298
+ pickers; picking an account fills its username. A successful login also updates that
299
+ account's `lastLoginAt`/`lastLoginFrom` when it is newer than the stored value. A
300
+ failure renders with the error state color.
301
+ - `Kubernetes / clusters` lists cluster id, name, kind, topology, API endpoint, namespaces,
302
+ attached node count and credential reference. `Add cluster` / `Edit cluster` opens an
303
+ inline form for id (auto-slug when blank, immutable after creation), name, kind
304
+ (`kubernetes` or `docker`), topology (`all-in-one`, `separate`, `external`, or unset),
305
+ API endpoint, comma-separated namespaces, credential reference and note. Node counts are
306
+ derived from `Host.clusterRef`; cluster deletion remains a YAML operation so referenced
307
+ clusters cannot disappear silently.
308
+ - `Endpoints` lists id, kind, target, URL, port, credential reference and note.
309
+ - `YAML` exposes the whole environment document with `Save` and `Delete`; it is the
310
+ escape hatch, not the primary path. Account and login fields never contain secrets.
311
+ - Forms validate required fields locally (name, at least one role, IP format/duplicates,
312
+ port range, username/password pairing and `http(s)` web login URLs) and rely on the Host
313
+ for schema and referential-integrity failures. A Host rejection stays visible above the
314
+ node form and the form remains open; a failed save also removes any credential written
315
+ for that attempt.
316
+
317
+ ### 6.4 YAML record editor
318
+
319
+ Services, deployments, rule profiles and operation bindings. Environments are created as
320
+ shells through the dedicated shell form; an existing environment's YAML is editable from
321
+ its detail tab.
322
+
323
+ ```text
324
+ test-a [Save] [Delete]
325
+ environment / test-a
326
+ ┌─────────────────────────────────────────────────────┐
327
+ │ id: test-a │
328
+ │ schemaVersion: 1 │
329
+ │ ... │
330
+ └─────────────────────────────────────────────────────┘
331
+ ```
332
+
333
+ - The textarea is the source of truth for this editor; environment detail forms are the
334
+ structured alternative.
335
+ - `Save` sends the whole YAML through `save`; schema failures are shown in an alert with
336
+ the Host message and code. There is no per-record dry-run button in V1; `Validate`
337
+ on Overview checks the whole registry.
338
+ - `Delete` asks for confirmation, then calls `remove`. The Host keeps a backup.
339
+ - Production records are still protected by the Host write policy; the panel surfaces
340
+ `POLICY_DENIED` instead of retrying.
341
+ - Empty editor state offers `New record`, so the user does not have to go back to the
342
+ toolbar.
343
+ - Keyboard shortcuts (Ctrl/Cmd+S, Esc) and dirty-state prompts are not implemented in
344
+ V1; they remain on the roadmap.
345
+
346
+ Production records show a persistent banner:
347
+ "Production environment. Changes affect a live system." with
348
+ `--dsw-alias-state-warn-primary`.
349
+
350
+ ### 6.5 Context Preview
351
+
352
+ Inputs: environment id, service id, deployment id (optional), entered as free text with
353
+ example placeholders (`test-a`, `aicoservice`, `deploy-001`).
354
+
355
+ Output in V1: the resolved context as formatted JSON in a scrollable `<pre>`. The Host
356
+ already returns provenance and structured `paths`/`variables`/`logSources`; a table view
357
+ with expandable provenance is a later refinement, not part of the current panel.
358
+
359
+ If resolution fails, the Host error (code and message) appears in the alert above the
360
+ form.
361
+
362
+ ### 6.6 Operation Preview
363
+
364
+ Inputs: operation id, environment id, service id, deployment id (optional).
365
+
366
+ Output in V1: the routing result as formatted JSON, including the selected binding,
367
+ Skill, policy and parameters. Conflicts and missing Skills surface as the Host error
368
+ alert. A per-field policy card and an "also matched" list are later refinements.
369
+
370
+ ### 6.7 Validation
371
+
372
+ There is no separate Diagnostics tab in V1. `Validate` on Overview calls the whole
373
+ registry check and renders its result card: pass/fail pill plus diagnostics with
374
+ severity, code and message. `Refresh` reloads `meta` and its warnings.
375
+
376
+ ### 6.8 Bundle config page
377
+
378
+ Read-only rows: version, `dataDir`, record counts, warning count, "Open manager".
379
+
380
+ ## 7. State and data flow
381
+
382
+ ```js
383
+ const [section, setSection] = useState('overview')
384
+ const [kind, setKind] = useState('environment')
385
+ const [items, setItems] = useState([])
386
+ const [selected, setSelected] = useState(null) // record id, not an object
387
+ const [record, setRecord] = useState(null) // parsed environment detail
388
+ const [yaml, setYaml] = useState('')
389
+ const [error, setError] = useState(null)
390
+ const [message, setMessage] = useState(null) // transient save/delete toast
391
+ ```
392
+
393
+ - Requests use the shared `call(op, payload)` helper and surface Host errors as `ErrorBox`.
394
+ - After a successful save/delete: refresh the list, re-read the selected record for
395
+ structured saves, and clear detail state when the record is removed.
396
+ - Environment detail forms save with `{ kind: 'environment', record }`; the YAML tab and
397
+ the other record kinds save with `{ kind, yaml }`.
398
+ - No global state, no context provider, no polling. "Refresh" is explicit.
399
+ - A client reload or HMR resets the selected section and open detail; this is accepted in V1.
400
+
401
+ ## 8. Internationalization
402
+
403
+ Namespace `envManager`; dictionaries registered for `en` and `zh`.
404
+
405
+ | Key | en | zh |
406
+ |---|---|---|
407
+ | `title` | Environment Manager | 环境管理器 |
408
+ | `section.overview` | Overview | 概览 |
409
+ | `section.environments` | Environments | 环境 |
410
+ | `section.services` | Services | 服务 |
411
+ | `section.deployments` | Deployments | 部署 |
412
+ | `section.rules` | Rule Profiles | 规则档案 |
413
+ | `section.bindings` | Operation Bindings | 操作绑定 |
414
+ | `section.context` | Context Preview | 上下文预览 |
415
+ | `section.operation` | Operation Preview | 操作预览 |
416
+ | `section.diagnostics` | Diagnostics | 诊断 |
417
+ | `action.new` | New | 新建 |
418
+ | `action.save` | Save | 保存 |
419
+ | `action.validate` | Validate | 校验 |
420
+ | `action.delete` | Delete | 删除 |
421
+ | `action.refresh` | Refresh | 刷新 |
422
+ | `action.back` | Back | 返回 |
423
+ | `action.cancel` | Cancel | 取消 |
424
+ | `state.empty` | No records yet | 暂无记录 |
425
+ | `state.loading` | Loading… | 加载中… |
426
+ | `state.dirty` | Unsaved changes | 有未保存的修改 |
427
+ | `state.production` | Production environment. Changes affect a live system. | 生产环境,修改会影响线上系统。 |
428
+ | `confirm.delete.title` | Delete {id}? | 删除 {id}? |
429
+ | `confirm.delete.body` | This cannot be undone. A backup copy is kept. | 此操作不可撤销,但会保留一份备份。 |
430
+ | `confirm.delete.type` | Type {id} to confirm | 输入 {id} 以确认 |
431
+ | `error.policy` | Not allowed by the current policy | 当前策略不允许此操作 |
432
+ | `error.network` | Cannot reach the Host | 无法连接 Host |
433
+ | `error.conflict` | Conflicting bindings: {ids} | 冲突的绑定:{ids} |
434
+ | `context.definedBy` | Defined by | 定义来源 |
435
+ | `context.inputs` | Inputs | 输入 |
436
+ | `context.shadowed` | Shadowed | 被覆盖 |
437
+ | `context.noValue` | Not defined | 未定义 |
438
+ | `operation.requiresApproval` | Requires approval | 需要审批 |
439
+ | `operation.allowProduction` | Allows production | 允许生产 |
440
+ | `skill.available` | Available | 可用 |
441
+ | `skill.unavailable` | Unavailable | 不可用 |
442
+ | `skill.unknown` | Unknown | 未知 |
443
+
444
+ Rules:
445
+
446
+ - `t(key, params)` from `ctx.locale.bind('envManager')`; no string concatenation.
447
+ - Dates render with `Intl.DateTimeFormat` from the active locale id.
448
+ - A missing key falls back to English and logs once.
449
+
450
+ ## 9. Styling
451
+
452
+ ```js
453
+ const S = {
454
+ panel: { display: 'flex', height: '100%', background: 'var(--dsw-alias-bg-base)' },
455
+ strip: { width: 180, borderRight: '1px solid var(--dsw-alias-border-l1)',
456
+ background: 'var(--dsw-alias-bg-layer-1)', padding: 8 },
457
+ card: { background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l1)',
458
+ borderRadius: 8, padding: 16 },
459
+ text: { color: 'var(--dsw-alias-label-primary)' },
460
+ muted: { color: 'var(--dsw-alias-label-secondary)' },
461
+ danger: { color: 'var(--dsw-alias-state-error-primary)' },
462
+ warn: { color: 'var(--dsw-alias-state-warn-primary)' },
463
+ ok: { color: 'var(--dsw-alias-state-success-primary)' },
464
+ mono: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 },
465
+ }
466
+ ```
467
+
468
+ - No inline hex colors except the plugin-manager `icon.svg` brand mark. The sidebar
469
+ glyph uses `currentColor` only, so it follows the host theme.
470
+ - Radius 8, spacing multiples of 4, body 13 px, mono 12 px.
471
+ - Hover uses `--dsw-alias-bg-layer-2`; focus ring uses `--dsw-alias-brand-primary`.
472
+
473
+ ## 10. Accessibility
474
+
475
+ - Section strip is a `role="tablist"` with arrow-key navigation.
476
+ - Every icon-only button has `aria-label`.
477
+ - Errors use `role="alert"`; success uses `role="status"`.
478
+ - The YAML textarea has a label and a described-by diagnostics id.
479
+ - Focus returns to the row that opened the editor after Save/Back.
480
+ - No action is reachable only by hover.
481
+
482
+ ## 11. Component tree
483
+
484
+ ```text
485
+ EnvManagerPanel
486
+ ├── PanelHeader (glyph, title, subtitle)
487
+ ├── HeaderActions (New environment, persistent)
488
+ ├── SectionTabs (Environments / Overview / Records / Context / Operations)
489
+ ├── SectionContent
490
+ │ ├── OverviewView (stat cards, runtime, warnings, onboarding)
491
+ │ ├── EnvironmentsView
492
+ │ │ ├── EnvironmentList ── EnvironmentRow
493
+ │ │ ├── EnvironmentDetail
494
+ │ │ │ ├── DetailHeader (back, identity, actions)
495
+ │ │ │ ├── DetailTabs (overview/all nodes/plane tabs/logins/clusters/endpoints/yaml)
496
+ │ │ │ ├── BasicInfo + BasicInfoForm
497
+ │ │ │ ├── AccessSummary + PlaneDistribution + RoleChips
498
+ │ │ │ ├── NodeToolbar (search/add/batch)
499
+ │ │ │ ├── NodeCard ── AccountTable + AccountForm
500
+ │ │ │ ├── NodeForm (kind/cluster/access/multi-role/optional credential)
501
+ │ │ │ ├── BatchNodeForm
502
+ │ │ │ ├── LoginTable + LoginForm
503
+ │ │ │ ├── ClusterTable + ClusterForm
504
+ │ │ │ ├── EndpointTable
505
+ │ │ │ └── YamlEditor
506
+ │ │ └── EnvironmentWizard (shell create: name/id/category/description + default planes)
507
+ │ ├── RecordsView
508
+ │ │ └── YamlRecordEditor (service/deployment/rule/binding)
509
+ │ ├── ContextView ── ResultBlock
510
+ │ └── OperationsView ── ResultBlock
511
+ └── Toast / ErrorBox
512
+ ```
513
+
514
+ Each component is a function in `client.js`; shared helpers are `h()`, `t()`, `call()`,
515
+ `formatTime()`, `categoryLabel()`, `authLabel()`, `roleName()`, `countByRole()`,
516
+ `hostKindLabel()`, `hostAccessLabel()`, `topologyLabel()`, `stableSlug()` and the form
517
+ primitives `Field`, `Select` and `TextArea`.
518
+
519
+ ## 12. Manual QA checklist
520
+
521
+ - [ ] Sidebar icon appears once, at 16 px wide / 18 px collapsed, in `currentColor`,
522
+ with no filled background, and selects the panel.
523
+ - [ ] The sidebar row shows one label, aligned with the shipped rows, and the active
524
+ row uses the host highlight rather than plugin-provided styling.
525
+ - [ ] Panel opens on **Environments**, not Overview or Records.
526
+ - [ ] The header `New environment` action opens the guided shell form from any tab.
527
+ - [ ] `Records` excludes Environments and keeps Services/Deployments/Rule Profiles/
528
+ Operation Bindings YAML editing.
529
+ - [ ] Panel renders with an empty `dataDir` and with several environments.
530
+ - [ ] `New environment` creates one environment shell with the four standard roles and
531
+ zero hosts; no password is requested and no credential is written.
532
+ - [ ] Environment list summary shows hosts, accounts and login records.
533
+ - [ ] Detail back link returns to the list and keeps the Records tab selected.
534
+ - [ ] Basic info edit round-trips name, aliases, category and description.
535
+ - [ ] Detail tabs wrap without clipping and show Overview, All nodes, each declared
536
+ plane, Login records, Kubernetes / clusters, Endpoints and YAML.
537
+ - [ ] A plane tab shows only nodes with that role; a multi-role node appears in every
538
+ plane it serves without duplicating the host record.
539
+ - [ ] A plane with zero nodes shows its empty state and `Add node` preselects that role.
540
+ - [ ] Add node requires a name and at least one role; optional username + password writes
541
+ a credential reference and never a plaintext secret.
542
+ - [ ] Add node offers VM, physical, K8s all-in-one, K8s worker and Pod host; selecting a
543
+ K8s kind requires a cluster and pre-fills its default access methods.
544
+ - [ ] A Pod host records `ssh` + `docker`, not a Pod name; the node remains one machine.
545
+ - [ ] Edit node preserves existing accounts and can add/remove roles, kind, access and
546
+ cluster reference.
547
+ - [ ] Batch import parses `IP,name,roles,port,user`, supports `|` role separators and
548
+ rejects unknown roles/duplicate IPs without saving a partial batch.
549
+ - [ ] Node search filters by id, name, IP, platform, admin user, role, kind, access method,
550
+ cluster name/reference, account username and web login URL.
551
+ - [ ] An expanded node card shows every account with auth type and last login source.
552
+ - [ ] Add web login stores the URL and username, shows the URL as a link, and keeps only a
553
+ credential reference after the password is written; edit account can update the URL
554
+ or rotate the password.
555
+ - [ ] Kubernetes / clusters lists node counts, topology and API endpoint; add/edit cluster
556
+ round-trips and the new cluster appears in the node form's cluster picker.
557
+ - [ ] Add node / add account / add login record / add cluster persist and re-render without
558
+ a reload.
559
+ - [ ] Login records are ordered newest-first and failures use the error color.
560
+ - [ ] Endpoints show target name plus id and preserve URL/credential reference.
561
+ - [ ] YAML tab saves the same document and clears stale detail state after delete.
562
+ - [ ] Invalid YAML shows a diagnostic and does not write.
563
+ - [ ] Delete asks for confirmation and reports the Host error if removal fails.
564
+ - [ ] Production save is denied when `allowUiProductionWrites` is false.
565
+ - [ ] Context preview returns a structured context.
566
+ - [ ] Operation preview resolves a binding or a clear diagnostic.
567
+ - [ ] Language switch between `en` and `zh` updates every visible string.
568
+ - [ ] Light and dark themes both keep contrast on cards and badges.
569
+ - [ ] Keyboard-only navigation reaches every action.
570
+ - [ ] Reload of the page keeps the selected section out of scope (state resets).
571
+
572
+ ## 13. Out of scope for V1
573
+
574
+ - Full CRUD forms for services, deployments, rules and bindings.
575
+ - Deleting individual hosts/accounts/logins from rows; account and node edit exist, deletion
576
+ stays in YAML in V1.
577
+ - Per-Pod or per-container inventory, container ids and restart counts. V1 records the
578
+ Pod-hosting machine as a `pod-host` and leaves Pods in the deployment model.
579
+ - Deleting clusters from a row; cluster deletion stays in YAML so references are deliberate.
580
+ - Database primary/replica/sharding topology; V1 models the database plane as a role and
581
+ keeps topology detail in node notes.
582
+ - YAML syntax highlighting, folding, or diff view.
583
+ - Topology graphs and dependency visualizations.
584
+ - Creating or editing the session default environment.
585
+ - Executing a Skill or showing an operation timeline.
586
+ - Multi-user presence, comments, or audit UI.
587
+ - JumpServer/PAM API synchronization; login data is entered or imported explicitly.
@@ -0,0 +1,36 @@
1
+ # Agent Behavior
2
+
3
+ ## Required Behavior
4
+
5
+ Agents working on this project must:
6
+
7
+ - preserve the file-system-first architecture
8
+ - keep changes small and reviewable
9
+ - write tests for core logic
10
+ - update current-state.md after completion
11
+ - avoid introducing infrastructure not requested by the user
12
+ - keep tool functions thin
13
+ - keep business logic in resolver/router modules
14
+ - treat skill references as discovery, not execution authorization
15
+
16
+ ## Forbidden Behavior
17
+
18
+ Agents must not:
19
+
20
+ - introduce plain-text credentials into registry files
21
+ - execute arbitrary JavaScript or shell in rule templates
22
+ - auto-apply rule or skill changes without review
23
+ - write hidden durable state outside documented directories
24
+ - silently discard resolver/router failures
25
+ - treat skill reference resolution as execution approval
26
+ - perform unrelated refactoring
27
+ - change storage contracts without documentation
28
+
29
+ ## When Unclear
30
+
31
+ If the task is unclear:
32
+
33
+ 1. inspect relevant docs
34
+ 2. inspect source files
35
+ 3. propose a small plan
36
+ 4. avoid broad implementation
@@ -0,0 +1,29 @@
1
+ # Change Snapshot Policy
2
+
3
+ ## Purpose
4
+
5
+ Preserve project continuity by documenting what changed and why after each meaningful change.
6
+
7
+ ## When to Update
8
+
9
+ Update `docs/00-project/current-state.md` when:
10
+
11
+ - a new feature is implemented
12
+ - a bug is fixed
13
+ - a storage contract changes
14
+ - a module boundary changes
15
+ - a known gap is closed
16
+
17
+ ## What to Record
18
+
19
+ - What changed (high-level)
20
+ - Which files were added/removed/modified
21
+ - Any new constraints or dependencies
22
+ - Remaining known gaps
23
+ - Recommended next work
24
+
25
+ ## What Not to Record
26
+
27
+ - Every single commit detail (git history covers that)
28
+ - Temporary workarounds without context
29
+ - Opinion or speculation