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,416 @@
1
+ # API Verification
2
+
3
+ > Status: verified against the DSH runtime shipped as `DeepSeek Harness 0.2.1-rc.5`.
4
+ > Method: `cordis_inspect_*` providers plus package documentation and bundled source
5
+ > extracted from the installed `app.asar`. This document records what the V1 design
6
+ > may rely on, and what it must verify again at implementation time.
7
+
8
+ ## 1. Purpose
9
+
10
+ The first design draft left eight open questions about DSH integration. This document
11
+ answers them from the runtime itself so that `detailed-design.md`,
12
+ `tool-contract.md`, `storage-contract.md` and `ui-design.md` describe APIs that exist,
13
+ not APIs that are assumed.
14
+
15
+ Every finding has an ID (`F1`…`F21`). The design documents reference these IDs.
16
+
17
+ ## 2. Findings
18
+
19
+ ### F1 — Host plugin entry
20
+
21
+ A Host package is an ES module exporting any of:
22
+
23
+ ```ts
24
+ export const name = 'dsh-env-manager'
25
+ export const inject = ['tools']
26
+ export const Config = z.object({ dataDir: z.string() })
27
+ export function apply(ctx, config) { /* ... */ }
28
+ ```
29
+
30
+ Evidence: `@deepseek-ai/dsh-shell-env` (`lib/index.js`) and `@deepseek-ai/dsh-tool-todo`.
31
+ `Config` is a `@deepseek-ai/schemastery` schema, not Zod. `inject` is a plain string
32
+ array. The loader supplies the parsed config as the second `apply` argument.
33
+
34
+ Design consequence: the Host entry keeps `apply(ctx, config)` and declares `inject`
35
+ explicitly; `Config` is authored with schemastery, while record validation uses Zod.
36
+
37
+ ### F2 — Tool registration
38
+
39
+ `ctx.tools.register(definition)` registers globally or in the calling Agent scope and
40
+ returns an exact disposer. Duplicate names within one layer throw.
41
+
42
+ Tool authors should use `defineTool` from `@deepseek-ai/dsh-tools`:
43
+
44
+ ```ts
45
+ ctx.tools.register(defineTool({
46
+ name: 'env_list',
47
+ description: '...',
48
+ parameters: { kind: { type: 'string', enum: [...] } },
49
+ output: { schema: { ... }, render: (args, value) => [{ type: 'text', text: '...' }] },
50
+ async execute(args, exec) { /* ... */ },
51
+ }))
52
+ ```
53
+
54
+ Evidence: `dsh-tools` README §"Register a tool"; `dsh-tool-todo` source.
55
+
56
+ Design consequence: all ten V1 tools are `defineTool` definitions, registered inside
57
+ `apply` and disposed with the plugin fiber.
58
+
59
+ ### F3 — Parameter schema DSL
60
+
61
+ `defineTool.parameters` is DSH's own value-schema DSL, not raw JSON Schema. Supported
62
+ node types: `string`, `number`, `integer`, `boolean`, `null`, `array`, `object`,
63
+ author-only `json`, and exact-one `oneOf`. Object property `required: true` marks
64
+ membership; `additionalProperties` must be explicitly `true` or `false`.
65
+
66
+ Model arguments are validated before `execute` runs; invalid input becomes a normal
67
+ error result (`ToolArgsError`).
68
+
69
+ Design consequence: tool parameter tables in `tool-contract.md` use this DSL verbatim.
70
+
71
+ ### F4 — Tool output contract
72
+
73
+ `output.schema` is a `JsonSchemaNode` restricted to:
74
+ `type`, `oneOf`, `properties`, `required`, `additionalProperties`, `items`, `enum`,
75
+ `const`, plus annotations (`description`, `title`, `default`, `examples`).
76
+ An **annotation-only schema** (`{ description: '…' }`) is the supported way to declare
77
+ unconstrained JSON.
78
+
79
+ The registry validates the returned value against `output.schema` after execution and
80
+ throws `ToolOutputError` on mismatch. `render(args, value)` returns `ContentBlock[]`;
81
+ one `{ type: 'text', text }` block is enough for a JSON-shaped tool.
82
+
83
+ Evidence: `dsh-tools` `assertSupportedJsonSchema`, `checkSchemaNode`,
84
+ `validateJsonSchemaValue`, and the output check at dispatch.
85
+
86
+ Design consequence: every tool output is a precise object schema where practical, and
87
+ uses `additionalProperties: true` for open record payloads. `render` emits a short
88
+ human summary, because the canonical value is what the model and programmatic callers
89
+ receive.
90
+
91
+ ### F5 — Structured tool errors
92
+
93
+ Only a thrown `HarnessError` produces structured `{ name, code }` failure info;
94
+ plain `Error` produces a message only. Constructor:
95
+
96
+ ```ts
97
+ new HarnessError(message, code, options?)
98
+ ```
99
+
100
+ imported from `@deepseek-ai/dsh-llm`. `name` defaults to the subclass constructor name.
101
+
102
+ Evidence: `dsh-tools` `errorInfo()` and `@deepseek-ai/dsh-llm` `HarnessError`.
103
+
104
+ Design consequence: all expected failures throw a single `EnvManagerError extends
105
+ HarnessError` carrying a stable code from the diagnostics catalogue. Success values
106
+ carry no `ok` flag; failure is the error path.
107
+
108
+ ### F6 — Tool execution context
109
+
110
+ `execute(args, exec)` receives `ToolRunContext` extending `ToolExecution`:
111
+
112
+ | Member | Use |
113
+ |---|---|
114
+ | `exec.signal` | cooperative cancellation; must be observed |
115
+ | `exec.callId`, `exec.rootCallId` | correlation ids |
116
+ | `exec.agent` | optional `Agent`; `exec.agent.session.header.id` is the session id |
117
+ | `exec.deferContext(msg)` | attach extra model context |
118
+ | `exec.concludeTurn()` | end the turn (not used in V1) |
119
+
120
+ Evidence: Service contract `tools`, referenced type `ToolRunContext`; `dsh-shell-env`
121
+ reads `execution.agent.session.header.id`.
122
+
123
+ Design consequence: session-scoped tools require `exec.agent`; without it they fail
124
+ with `SESSION_CONTEXT_UNAVAILABLE` instead of guessing a session.
125
+
126
+ ### F7 — Approval
127
+
128
+ `ctx.approval.request(req)` returns
129
+ `'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'`.
130
+ Only `'allowed-once'` is a grant; every other outcome must be treated as denial.
131
+ The request needs `agent`, `toolName`, optional `callId`, `reason`, `displayReason`
132
+ (localized), and a `signal`. It requires an open turn.
133
+
134
+ Evidence: Service contract `approval`.
135
+
136
+ Design consequence: `env_delete` and production `env_save` call
137
+ `ctx.approval.request` with a localized `displayReason`, and fail closed on any
138
+ non-grant. Approval is per call; nothing is cached.
139
+
140
+ ### F8 — Credentials
141
+
142
+ `ctx.credentials.describe(ref)` returns `{ configured, source?, writable }` without
143
+ the value. `set(ref, value)` writes to a provider-managed writable source and
144
+ `unset(ref)` removes it. `resolve(ref)` returns the value and must not be called by this
145
+ plugin. `CredentialRef` is a branded string; at runtime an ordinary string is accepted.
146
+
147
+ Evidence: Service contract `credentials`; live `POST /env-manager/api` with
148
+ `op: "credential"` returned `{ ref, configured: true }`, and `op: "credentialUnset"`
149
+ returned `{ ref, removed: true }`. The panel used the same path to create an environment,
150
+ then the record was read back and contained only the ref.
151
+
152
+ Design consequence: `env_resolve` reports each `credentialRef` with
153
+ `configured`/`source` only. Credential values may appear in the request body of
154
+ `credential`, but never in a tool result, HTTP response, log line, diagnostic or registry
155
+ file.
156
+
157
+ ### F9 — Filesystem capability gap
158
+
159
+ `ctx.fs` exposes `resolve`, `stat`, `listDir`, `readText`, `writeText` (atomic),
160
+ `editText`, `readBytes`, `readByteRange`, `watch`, `contains` — but **no** `mkdir`,
161
+ `unlink`, `rename`, or `copy`. `resolve` follows real paths, so it cannot target a
162
+ file whose parent directory does not exist yet.
163
+
164
+ Evidence: Service contract `fs`.
165
+
166
+ Design consequence: registry storage uses `node:fs/promises` directly, confined to
167
+ the resolved `dataDir`. The plugin is trusted Host code, not a sandboxed tool. Every
168
+ path is built from a validated id and checked with `path.resolve` containment; no
169
+ model-supplied path reaches the filesystem.
170
+
171
+ ### F10 — Data directory
172
+
173
+ `ctx.get('profileContext')` exposes at least `name` and `dir` at runtime
174
+ (`dsh-shell-env` reads both), although the inspect contract lists only four other
175
+ members. `dir` is the profile directory that owns `node_modules`, patches and data.
176
+
177
+ Evidence: `dsh-shell-env` `collect()`; Service contract `profileContext`.
178
+
179
+ Design consequence: `dataDir` resolution order:
180
+
181
+ 1. `config.dataDir` (absolute, or relative to the profile directory)
182
+ 2. `<profileContext.dir>/data/dsh-env-manager`
183
+ 3. `$DSH_PROFILE_DIR/data/dsh-env-manager`
184
+ 4. `<cwd>/data`
185
+
186
+ The resolved path is logged once and exposed read-only through `env_get_context` and
187
+ the UI overview. Task T1 verifies `profileContext.dir` on a live profile.
188
+
189
+ ### F11 — Session identity and state
190
+
191
+ `exec.agent.session` is a live `Session` with `header.id`, `append(type, data)` and
192
+ `snapshotEvents()`. The session id is available for every agent-initiated tool call.
193
+
194
+ There is a `sessionProjections` service (used by `todo_write`) for replayable plugin
195
+ state, but it is designed around declared session event types and projection schemas;
196
+ `SessionEventMap` is a closed TypeScript interface.
197
+
198
+ Design consequence: V1 stores the default environment in
199
+ `<dataDir>/sessions/<sessionId>.json` (small, explicit, testable, no session-log
200
+ schema changes). A projection-based implementation is recorded as a P2 alternative.
201
+
202
+ ### F12 — HTTP carrier
203
+
204
+ `ctx.webServer.register({ kind: 'prefix' | 'exact', path, handler })` registers a
205
+ route and returns a disposer; duplicate `(kind, path)` throws.
206
+ `getOrigin()` returns the local origin. `tapIndex(transform)` can inject markup.
207
+
208
+ Evidence: Service contract `webServer`.
209
+
210
+ Design consequence: the Client panel talks to the Host through
211
+ `POST /env-manager/api` registered by this plugin, authenticated by a required custom
212
+ header plus an `Origin` check. See ADR-0009 and `ui-design.md` §4.
213
+
214
+ ### F13 — Client module
215
+
216
+ A bundle's `client.js` is served as `dsh.client`'s `./client` export and loaded by
217
+ `window.__ModuleLoader__.load({ id, factory(require) { ... } })`. The factory may
218
+ `require('react')`. The returned plugin declares `inject` services (`slots`,
219
+ `locale`, …) and `apply(ctx)`.
220
+
221
+ The dynamic-Cordis builtins (`React`, `host.call`, `styles`, `console`) belong to
222
+ runtime-defined packages, **not** to installed bundle client halves.
223
+
224
+ Evidence: `client.js` skeleton; `Slots`/`Service` client catalog; `dsh-cordis-client-runner`.
225
+
226
+ Design consequence: the Client uses `slots` and `locale` only, plus browser `fetch`
227
+ for the plugin's own HTTP route.
228
+
229
+ ### F14 — Client-to-Host Remote is not usable here
230
+
231
+ DSH's native Client↔Host RPC is the Typert Remote system:
232
+ `ctx.remote.$mount()` mounts a **generated** Host-for-Client contribution; strict
233
+ descriptors come from `@Remote`/`@RemoteScope` decorators processed by the build-time
234
+ Typert loader into `ctx.typert.local` (`/remote` artifacts imported by the Client).
235
+ Client-supplied fields without strict generated codecs fail before methods become
236
+ callable. SRC mode is a development fallback for endpoints without a strict
237
+ definition and does not give a standalone bundle a typed namespace.
238
+
239
+ Evidence: `dsh-api-gateway` README (Host and Client sections), `dsh-api-remotes`
240
+ README ("capability set is fixed by explicit build-time value imports"), client
241
+ Service catalog (no `remote` service until the gateway Client face mounts one).
242
+
243
+ Design consequence: a standalone plugin bundle cannot add a Remote namespace without
244
+ adopting DSH's build toolchain. V1 therefore uses the plugin-owned HTTP route
245
+ (ADR-0009), which is same-origin, token-free, header-gated, and reuses the exact same
246
+ application handlers as the tools.
247
+
248
+ ### F15 — Slots
249
+
250
+ Live topology confirms:
251
+
252
+ | Slot | Kind | Registration |
253
+ |---|---|---|
254
+ | `sidebar.panellist` | list | `{ id, order?, label? }` — one sidebar icon |
255
+ | `main` | keyed | `{ key }` — the selected panel body |
256
+ | `plugins.bundle.config` | keyed | `{ key: '<package name>' }`, `view: 'page'` only |
257
+ | `conversation.composer.dock` | list | `{ id, order?, label? }` — ambient composer entry |
258
+
259
+ `ctx.slots.inject(slotName, () => ctx.slots.register({ name, ... }, Component))` waits
260
+ for the slot to exist and disposes with the fiber.
261
+
262
+ Design consequence: the manager registers a `sidebar.panellist` icon
263
+ (id `env-manager`), a `main` panel keyed `env-manager`, and optionally a bundle
264
+ config page. The composer dock is not used for the main UI.
265
+
266
+ ### F16 — Client services
267
+
268
+ Available to the Client half: `layout`, `locale`, `sessions`, `slots`, `theme`,
269
+ `timer`, `uiWorkspace`, `workspaces`.
270
+
271
+ - `ctx.locale.register(ns, { en, zh })` and `ctx.locale.bind(ns)` provide copy.
272
+ - `ctx.theme` owns theme selection; the plugin only consumes CSS variables.
273
+ - `ctx.layout.selectPanel(id)` can select the panel programmatically.
274
+
275
+ Design consequence: UI copy is registered as namespace `envManager`; no string is
276
+ hardcoded twice.
277
+
278
+ ### F17 — Theme tokens
279
+
280
+ The current theme exposes `--dsw-alias-*` tokens:
281
+ `bg-base`, `bg-layer-1`, `bg-layer-2`, `bg-overlay`, `border-l1`, `border-l2`,
282
+ `brand-primary`, `label-primary`, `label-secondary`, `state-error-primary`,
283
+ `state-idle-primary`, `state-success-primary`, `state-warn-primary`, and
284
+ `--dsw-specific-sidebar-fill`.
285
+
286
+ Design consequence: the panel styles itself only with these variables; no hardcoded
287
+ palette, including the sidebar icon (except its own brand mark color).
288
+
289
+ ### F18 — Config schema shape
290
+
291
+ `Config` is `@deepseek-ai/schemastery` (`z.object({...})`), and the loader rejects
292
+ invalid config before `apply`. There is no runtime config *write* API for a plugin;
293
+ users edit the profile patch. The Client may show the effective config read-only.
294
+
295
+ Design consequence: the bundle config page is informational; `dataDir` is changed in
296
+ the profile patch or `cordis.patch.yml`, not through the UI.
297
+
298
+ ### F19 — Package layout and bundling
299
+
300
+ The bundle needs `index.js` (Host entry), `client.js` (Client entry),
301
+ `cordis.patch.yml`, `package.json` with `dsh.bundle.patch` and `dsh.client`.
302
+ DSH itself uses prebuilt ESM in `lib/` and ships no bundler requirement for the Host.
303
+ The Client half is a single plain-JS module.
304
+
305
+ Design consequence: TypeScript Host compiled to `lib/`, `index.js` as a thin
306
+ re-export shim, no client bundler. This matches ADR-0004.
307
+
308
+ ### F20 — Skill catalog
309
+
310
+ The Host exposes a `skills` service:
311
+
312
+ ```ts
313
+ ctx.skills.list({ scope?, cwd?, signal? }): Promise<SkillSummary[]>
314
+ ctx.skills.snapshot({ scope?, cwd?, signal? }): Promise<{ skills: SkillSummary[]; complete: boolean }>
315
+ ctx.skills.get(name, { scope?, cwd? }): Promise<SkillSummary | undefined>
316
+ ctx.skills.register(...), ctx.skills.registerProvider(...)
317
+ ```
318
+
319
+ `SkillSummary` fields: `name`, `description`, `whenToUse?`, `invocation`, `source`,
320
+ `provider`, `path?`, `resourceBase?`. There is **no digest field**, and no API returns
321
+ Skill file contents or a content hash.
322
+
323
+ `snapshot.complete === false` means the catalog is incomplete for the requested scope;
324
+ absence of a name then proves nothing.
325
+
326
+ Design consequence:
327
+
328
+ - Availability checks use `snapshot`, not a name-only `get`.
329
+ - A missing/service-unavailable/incomplete catalog yields `available: 'unknown'` with a
330
+ warning, never a silent pass.
331
+ - Digest pinning is unsupported. `SkillReference` uses `expectedSource` and
332
+ `expectedProvider`; `expectedDigest` was removed.
333
+
334
+ ### F21 — Runtime Skill registration from a plugin
335
+
336
+ `ctx.skills.register(skill)` is the official Host API for a plugin-owned runtime Skill:
337
+
338
+ ```ts
339
+ const dispose = ctx.skills.register({
340
+ name: 'dsh-env-manager',
341
+ description: '...',
342
+ whenToUse: '...',
343
+ content: '# ...',
344
+ source: 'runtime',
345
+ invocation: { modelInvocable: true, userInvocable: true },
346
+ })
347
+ ```
348
+
349
+ Evidence: Service contract `skills` (methods `register`, `registerProvider`, `list`,
350
+ `snapshot`, `get`; types `SkillRegistration`, `SkillDefinition`, `SkillSummary`,
351
+ `SkillInvocationPolicy`). The service description states that an unscoped registration
352
+ files into the **global layer**, while a registration made in a scoped context (for
353
+ example an agent preset) files into that scope. The disposer unregisters the provider
354
+ and invalidates catalog caches.
355
+
356
+ A live registration was verified in the `desktop` profile: after rebuilding and
357
+ reloading `dsh-env-manager`, the Skill catalog added `dsh-env-manager`, and the `skill`
358
+ tool returned the registered content. This is runtime registration, not a filesystem
359
+ `SKILL.md`; no workspace file or separate Skill installation is involved.
360
+
361
+ Design consequence:
362
+
363
+ - The bundled `dsh-env-manager` discovery Skill is registered in `apply()` with
364
+ `ctx.get('skills')`, inside the plugin effect so it is disposed with the fiber.
365
+ - The Skill is visible in the same profile's other workspaces; it remains a catalog
366
+ summary until invoked, so it does not put the full workflow in every system prompt.
367
+ - `config.registerSkill` defaults to `true`; setting it to `false` suppresses the entry.
368
+ - `ctx.skills` is optional. If the service is absent or lacks `register`, the plugin
369
+ keeps its tools and panel and skips the Skill.
370
+ - Operation execution still requires an `operationBinding`; the discovery Skill does
371
+ not execute the routed Skill.
372
+
373
+ ## 3. Consequences for the V1 design
374
+
375
+ 1. **Tool contract is concrete.** Ten `defineTool` definitions with a parameter DSL,
376
+ output schema, render function and `HarnessError` codes.
377
+ 2. **Transport is plugin-owned HTTP.** No Typert codegen; one same-origin route; the
378
+ same application service answers tools and UI.
379
+ 3. **Storage uses Node fs.** `ctx.fs` cannot create or delete directories/files, so
380
+ the plugin owns `<dataDir>` with `node:fs/promises` and its own atomic writes.
381
+ 4. **Approval is per tool call.** Delete and production writes ask the human, and any
382
+ outcome other than `allowed-once` denies.
383
+ 5. **Session context is file-backed.** No session-log schema changes in V1.
384
+ 6. **UI is a real panel.** Sidebar entry + `main` panel; no composer-dock widget.
385
+
386
+ ## 4. Remaining verification tasks (implementation phase)
387
+
388
+ | ID | Verify | Fallback | Status |
389
+ |---|---|---|---|
390
+ | V1 | `profileContext.dir` present and writable | `$DSH_PROFILE_DIR`, then `<cwd>/data` | Verified: live `meta` reports `C:\Users\18811\.dsh\profiles\desktop\data` via the env fallback |
391
+ | V2 | `slots.register` accepts `main` with `key` | register under `main.conversation` | Verified live: `sidebar.panellist` occupant `dsh-env-manager` and `main` key `dsh-env-manager` are active |
392
+ | V3 | `fetch('/env-manager/api')` works from the bundle | fall back to Remote when DSH exposes runtime descriptors | Verified: live `POST /env-manager/api` returns `200` and a save/list/remove round-trip succeeds |
393
+ | V4 | `approval.request` from a tool body resolves | show a UI confirmation and deny without approval | Implemented fail-closed; not exercised live |
394
+ | V5 | Output schema accepts `additionalProperties: true` objects | annotation-only schema | Verified locally: all ten schemas pass `assertObjectJsonSchema` |
395
+ | V6 | `CredentialRef` accepts a plain string at runtime | import the branded factory | Implemented with string refs; live `describe` not exercised |
396
+ | V7 | Directory watcher parity on Windows | reload per request (already the V1 plan) | Verified: each request reloads the registry |
397
+ | V8 | `webServer` route prefix does not collide with shell routes | choose a more specific prefix | Verified live: `/env-manager/api` is served beside the GUI |
398
+
399
+ ## 5. Evidence index
400
+
401
+ | Source | What it proves |
402
+ |---|---|
403
+ | `cordis_inspect_query(host, Service, tools)` | `register` signature, `ToolDefinition`, `ToolRunContext` |
404
+ | `cordis_inspect_query(host, Service, approval)` | approval outcomes, request fields |
405
+ | `cordis_inspect_query(host, Service, credentials)` | describe/resolve split |
406
+ | `cordis_inspect_query(host, Service, fs)` | capability gap (no mkdir/unlink) |
407
+ | `cordis_inspect_query(host, Service, skills)` | `snapshot`/`list`/`get`/`register`, `SkillRegistration`, no digest |
408
+ | Live `dsh-env-manager` registration | Skill catalog gained `dsh-env-manager`; `skill` tool loaded its content |
409
+ | `cordis_inspect_query(host, Service, webServer)` | route registration, origin |
410
+ | `cordis_inspect_query(client, Slots, listSubTree)` | slot topology and registration fields |
411
+ | `cordis_inspect_query(client, Builtin, listBuiltins)` | dynamic-half builtins (`host.call`) |
412
+ | `cordis_inspect_query(client, Theme, listTokens)` | `--dsw-alias-*` tokens |
413
+ | `dsh-tools` README + `lib/index.js` | `defineTool`, schema subset, output validation, `HarnessError` |
414
+ | `dsh-shell-env` `lib/index.js` | `Config` schemastery, `profileContext.dir`, session id |
415
+ | `dsh-api-gateway` / `dsh-api-remotes` README | Typert Remote requires generated descriptors |
416
+ | `dsh-tool-todo` `lib/index.js` | a complete tool definition in practice |
@@ -0,0 +1,222 @@
1
+ # Architecture Overview
2
+
3
+ Status: V1.
4
+
5
+ Related: [design.md](./design.md), [detailed-design.md](./detailed-design.md), [module-boundaries.md](./module-boundaries.md), [data-model.md](./data-model.md).
6
+
7
+ ---
8
+
9
+ ## 1. Layers
10
+
11
+ ```text
12
+ DSH Web / Desktop UI
13
+ └── Client module (client.js): panels, context preview, YAML editor
14
+ │
15
+ ▼ POST /env-manager/api
16
+ DSH Host process
17
+ ├── transport header/origin/size checks, op dispatch
18
+ ├── tools Agent-facing schemas, approval gates
19
+ ├── app use cases shared by tools and transport
20
+ ├── session per-session default environment
21
+ ├── router operation → Skill matching
22
+ ├── resolver context + rule resolution with provenance
23
+ ├── registry validated in-memory snapshot
24
+ ├── storage atomic YAML persistence
25
+ └── memory-bridge (P1 contract, null adapter in V1)
26
+ │
27
+ ▼
28
+ Plugin data directory (YAML records + session JSON)
29
+ ```
30
+
31
+ `tools` and `transport` are sibling adapters over `app`; neither contains business
32
+ logic. This is the core boundary that keeps the Agent path and the UI path consistent.
33
+
34
+ ---
35
+
36
+ ## 2. Major components
37
+
38
+ ### 2.1 Client module
39
+
40
+ Runs inside DSH Web/Desktop as a `sidebar.panellist` entry plus the `main` panel keyed
41
+ `env-manager`.
42
+
43
+ Responsibilities:
44
+
45
+ - Environment, service, deployment, rule and binding lists and YAML editors.
46
+ - Context preview with provenance badges.
47
+ - Operation preview with skill availability and conflict display.
48
+ - Validation and diagnostics display.
49
+ - Locale-aware text (`en`, `zh`).
50
+
51
+ It calls the plugin HTTP API and renders the results. It holds no business logic and no
52
+ durable state.
53
+
54
+ ### 2.2 Tools
55
+
56
+ The Agent-facing surface: ten `env_*` tools.
57
+
58
+ Read tools answer questions about the registry. Management tools validate and write
59
+ records, with approval gates for production.
60
+
61
+ Tool handlers are thin; they resolve ids, call `app`, and map results to the declared
62
+ JSON value and `render` text. Expected failures throw `EnvManagerError`.
63
+
64
+ ### 2.3 Transport
65
+
66
+ The Client-facing surface: one same-origin route, `POST /env-manager/api`.
67
+
68
+ - Requires `x-env-manager-request: 1`, JSON content type and a same-origin `Origin`.
69
+ - Body limit 2 MiB; no CORS, no cookies.
70
+ - Dispatches `{ op, payload }` to `app` and maps codes to HTTP statuses.
71
+
72
+ ### 2.4 App
73
+
74
+ The use-case layer shared by both adapters.
75
+
76
+ - Composes storage, registry, resolver, router and session.
77
+ - Applies the record cap, timestamps, actor policy and dry-run.
78
+ - Reloads the registry after a write.
79
+ - Returns domain results or throws `EnvManagerError`.
80
+
81
+ ### 2.5 Registry
82
+
83
+ Loads all record files into an in-memory snapshot and validates them.
84
+
85
+ - Ids are unique per record type.
86
+ - References resolve.
87
+ - Invalid files are skipped with diagnostics, never deleted.
88
+ - V1 loads at the start of every application request; a write triggers a reload before
89
+ returning.
90
+
91
+ ### 2.6 Resolver
92
+
93
+ Pure functions that combine records into a `ServiceContext`.
94
+
95
+ - Merges rule layers field by field.
96
+ - Resolves `${...}` templates with cycle and undefined-variable detection.
97
+ - Rejects path traversal.
98
+ - Attaches provenance to every returned field.
99
+
100
+ ### 2.7 Router
101
+
102
+ Pure functions that map an operation to a bound Skill.
103
+
104
+ - Groups bindings by match level.
105
+ - Fails on same-level conflicts.
106
+ - Checks Skill availability through an injected interface (`ctx.skills.snapshot`).
107
+ - Evaluates approval and production policy.
108
+ - Maps parameters and returns diagnostics.
109
+
110
+ The router never executes anything.
111
+
112
+ ### 2.8 Session
113
+
114
+ Stores the current session's default environment.
115
+
116
+ - Per-session isolation.
117
+ - V1 persists `sessions/<sessionId>.json`; a projection is a later refinement.
118
+ - Never affects other sessions or registry files.
119
+
120
+ ### 2.9 Memory bridge
121
+
122
+ P1 integration with the memory plugin.
123
+
124
+ - V1 defines types and a null adapter.
125
+ - Emits redacted operation records.
126
+ - Proposes rule/skill changes for review.
127
+ - Never applies changes.
128
+
129
+ ### 2.10 Storage
130
+
131
+ Atomic YAML persistence.
132
+
133
+ - Directory per record type.
134
+ - Temp file + fsync + rename.
135
+ - Rolling backups (5 per record).
136
+ - Bounded and validated.
137
+
138
+ ---
139
+
140
+ ## 3. Request flows
141
+
142
+ ### 3.1 Resolve a service context
143
+
144
+ ```text
145
+ env_resolve(environmentId?, serviceId, deploymentId?)
146
+ → tools: validate input, resolve session default if needed
147
+ → app: fetch env, service, deployments from the registry
148
+ → resolver: select deployment, merge rules, resolve templates
149
+ → app: attach credential status (describe only)
150
+ → tools: return ServiceContext
151
+ ```
152
+
153
+ ### 3.2 Route an operation
154
+
155
+ ```text
156
+ env_resolve_operation(operation, environmentId?, serviceId?, deploymentId?)
157
+ → resolve context (above)
158
+ → router: classify bindings, detect conflict, snapshot skills, evaluate policy
159
+ → app: resolve parameters
160
+ → tools: return OperationResolution
161
+ ```
162
+
163
+ ### 3.3 Save a record
164
+
165
+ ```text
166
+ env_save(kind, record, dryRun?)
167
+ → tools: parse the JSON parameter
168
+ → app: validate with Zod, stamp timestamps, enforce the record cap
169
+ → approval gate when production is affected
170
+ → storage: atomic write + backup
171
+ → app: reload registry, re-run integrity checks
172
+ → tools: return id, created flag, warnings
173
+ ```
174
+
175
+ ### 3.4 Validate the registry
176
+
177
+ ```text
178
+ env_validate
179
+ → app: load snapshot and integrity diagnostics
180
+ → tools: summarize counts and diagnostics
181
+ ```
182
+
183
+ ### 3.5 UI read and write
184
+
185
+ ```text
186
+ client: call('record.get', { kind, id })
187
+ → transport: header + origin + size checks
188
+ → app.get(kind, id)
189
+ → transport: { ok: true, data: { record, yaml } }
190
+ → client: render the editor
191
+ ```
192
+
193
+ Writes follow the same path with actor `'ui'` and the production policy in
194
+ `writePolicy.allowUiProductionWrites`.
195
+
196
+ ---
197
+
198
+ ## 4. Cross-cutting concerns
199
+
200
+ | Concern | Approach |
201
+ |---|---|
202
+ | Credentials | references only; status via `describe` |
203
+ | Approval | policy in router; `ctx.approval` for management writes |
204
+ | Rule safety | interpolation only, cycle/traversal detection |
205
+ | Redaction | shared function before diagnostics and P1 records |
206
+ | Persistence | atomic write, bounded backups, no hidden state |
207
+ | Performance | small YAML scans; no indexes in V1 |
208
+ | Compatibility | `schemaVersion` gate; unknown versions skipped |
209
+ | Client security | same-origin route, custom header, no CORS, actor policy |
210
+
211
+ ---
212
+
213
+ ## 5. What is deliberately absent
214
+
215
+ - No network calls.
216
+ - No remote execution.
217
+ - No database.
218
+ - No vector search.
219
+ - No background jobs.
220
+ - No telemetry.
221
+
222
+ Each absence is a V1 scope decision, not an oversight.