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,227 @@
1
+ # Security Model (V1)
2
+
3
+ Status: normative for V1.
4
+
5
+ The plugin manages internal network topology and may describe production systems. This document defines the security boundaries and the controls required by V1.
6
+
7
+ Related: [AGENTS.md](../../AGENTS.md) §3, [security-policy.md](../06-operations/security-policy.md), [design.md](./design.md) §12, [data-model.md](./data-model.md).
8
+
9
+ ---
10
+
11
+ ## 1. Assets
12
+
13
+ | Asset | Sensitivity | Where it lives |
14
+ |---|---|---|
15
+ | Hostnames, IPs, internal URLs | medium | registry YAML |
16
+ | Service Git URLs | low–medium | registry YAML |
17
+ | Deployment topology | medium | registry YAML |
18
+ | Credential references | low | registry YAML |
19
+ | Credential values | high | DSH credential service / external vault — **never here** |
20
+ | Operation history | medium | memory plugin (P1) |
21
+ | Skill bindings and policies | high (execution routing) | registry YAML |
22
+
23
+ ---
24
+
25
+ ## 2. Trust boundaries
26
+
27
+ ```text
28
+ Browser (Client half) ──HTTP──▶ Host transport ──▶ app ──files──▶ plugin data dir
29
+ │
30
+ Agent/model ──tools──▶ Host tools ────┤
31
+ ├──describe──▶ DSH credential service
32
+ ├──request───▶ DSH approval service
33
+ └──lookup────▶ DSH Skill registry
34
+ ```
35
+
36
+ Two untrusted input paths exist: the model through tools, and the browser page through
37
+ the local HTTP route. Registry content is user-maintained and semi-trusted. The DSH
38
+ credential service is the only component allowed to hold secret values.
39
+
40
+ ---
41
+
42
+ ## 3. Credential handling
43
+
44
+ ### 3.1 Rules
45
+
46
+ 1. Registry records store only `credentialRef` strings.
47
+ 2. The Host calls `ctx.credentials.describe(ref)` and keeps only `{ configured, source?, writable }`.
48
+ 3. The Host never calls `ctx.credentials.resolve(ref)` and never forwards a resolved value to a tool result, diagnostic or log.
49
+ 4. Tool and HTTP outputs contain `{ ref, configured, source }` only.
50
+ 5. `credentialRef` values are validated against the credential reference pattern if one exists; otherwise a conservative pattern (`^[A-Za-z0-9._:/-]{1,200}$`) is applied.
51
+ 6. Host accounts and login records follow the same rule: `Host.accounts[].credentialRef` is a
52
+ reference, and `LoginRecord` stores username, time, source and result only. A web login
53
+ `loginUrl` is configuration, not a secret; it must be an absolute `http(s)` URL. Password,
54
+ private-key, token and session-recording fields are not part of the schema.
55
+ 7. The guided environment form may collect a password transiently. It sends the value
56
+ once to `credential`; the Host calls `ctx.credentials.set(ref, value)` and returns only
57
+ `{ ref, configured }`. The value is never written to YAML, never logged and never
58
+ returned. The Host still never calls `resolve`.
59
+ 8. Credential writes are restricted to refs matching
60
+ `env-manager/<environmentId>/<accountId>` so the form cannot overwrite an ambient
61
+ credential such as `OPENAI_API_KEY`.
62
+
63
+ ### 3.2 Threat: exfiltration through tool output
64
+
65
+ Mitigation: a single formatting layer for tool results strips any field named `value`, `secret`, `password`, `token`, `privateKey` and similar, even if a future module accidentally adds one. This is defense in depth, not a replacement for rule 3.
66
+
67
+ ### 3.3 Threat: credential reference confusion
68
+
69
+ A user may point a production deployment at a test credential. V1 cannot detect this; the result reports `configured: true` without revealing which secret it is. Operational review is required; this is documented as a residual risk.
70
+
71
+ ### 3.4 Threat: password exposure while adding or editing a node or account
72
+
73
+ The add/edit node form and the add/edit account form optionally hold a login password in the
74
+ browser input and send it in the same-origin JSON request. Shell creation and batch import
75
+ collect no password.
76
+ Mitigations:
77
+
78
+ - the route is localhost-only, requires the custom request header, validates origin and
79
+ sends `cache-control: no-store`;
80
+ - the Host forwards the value directly to `ctx.credentials.set` and does not log the
81
+ request body or the value;
82
+ - the response contains only the ref and `configured`, never the value;
83
+ - the generated YAML is checked by tests to contain the ref only.
84
+
85
+ A compromised same-origin page could still submit a credential write while the user is
86
+ present; this is accepted for a local single-user tool and documented as a residual risk.
87
+ Credential deletion is restricted to the same `env-manager/` namespace.
88
+
89
+ ### 3.5 Command access is metadata, not authorization
90
+
91
+ `Host.access` records that `ssh`, `kubectl` or `docker` exists on a machine; it does not
92
+ grant permission to run anything. V1 has no remote execution path, so a declared method
93
+ cannot be used to open a shell, call Kubernetes or run `docker exec` through this plugin.
94
+ A future execution adapter must still apply the operation binding's approval policy,
95
+ production checks and credential resolution. `Host.kind` and `Cluster.topology` are also
96
+ descriptive: they do not prove cluster membership or command availability. Do not treat a
97
+ `pod-host` badge as permission to run `docker exec` on a Pod.
98
+
99
+ ---
100
+
101
+ ## 4. Approval and production policy
102
+
103
+ ### 4.1 Operation routing
104
+
105
+ - `policy.requiresApproval` defaults to `true` for write operations.
106
+ - `policy.allowProduction` defaults to `false`.
107
+ - When `environmentCategory === 'production'` and `allowProduction !== true`, the router fails with `POLICY_DENIED`.
108
+ - The router does not execute anything; it returns the policy for the executing Skill.
109
+
110
+ ### 4.2 Management writes
111
+
112
+ Tool writes (`env_save` / `env_delete`) request approval through `ctx.approval` when the write affects a production environment:
113
+
114
+ - creating/updating an environment with `category: production`;
115
+ - creating/updating a deployment or operation binding whose environment is production;
116
+ - deleting any environment, deployment or binding that resolves to production.
117
+
118
+ Denied approval returns `POLICY_DENIED`; nothing is written. An approval outcome other
119
+ than `allowed-once` (`rejected`, `cancelled`, `unavailable`) denies.
120
+
121
+ UI writes (`record.save` / `record.remove`) run with actor `'ui'`:
122
+
123
+ - production saves are denied unless `writePolicy.allowUiProductionWrites` is true;
124
+ - delete requires `confirm: true` and a second, explicit confirmation in the panel;
125
+ - the UI never sends an approval request (it has no agent); it is policy-gated instead.
126
+
127
+ ### 4.3 Local HTTP route
128
+
129
+ `POST /env-manager/api` is reachable only with:
130
+
131
+ - `x-env-manager-request: 1`;
132
+ - `Content-Type: application/json`;
133
+ - an `Origin` equal to `ctx.webServer.getOrigin()` when the header is present;
134
+ - a body no larger than 2 MiB.
135
+
136
+ No CORS headers are sent, so a cross-origin page cannot preflight the custom header.
137
+ The route reads no cookie and no credential. It returns only registry data that a
138
+ `env_get`/`env_list` call could also return, plus derived context.
139
+
140
+ ### 4.4 Threat: approval bypass
141
+
142
+ A caller could edit YAML files directly, outside the tools. V1 cannot prevent this; file access is governed by the DSH sandbox and OS permissions. The design documents this as a deliberate boundary: the plugin is a context manager, not a policy enforcement point for the filesystem.
143
+
144
+ ---
145
+
146
+ ## 5. Rule template safety
147
+
148
+ Controls:
149
+
150
+ 1. Interpolation only — no function calls, operators, conditionals or shell.
151
+ 2. No environment variable access unless a future ADR explicitly allows named variables.
152
+ 3. Undefined references are errors.
153
+ 4. Cyclic references are errors with the cycle path.
154
+ 5. Resolved paths are rejected if any segment is `..`.
155
+ 6. Resolved values are not executed, only returned.
156
+ 7. Template syntax is restricted to `${identifier(.identifier)*}` with a 2048-character
157
+ cap; anything else is `RULE_TEMPLATE_INVALID`.
158
+
159
+ Threat: a malicious rule profile could point at sensitive directories to make the Agent read them later. Mitigation: the plugin never reads the resolved paths itself in V1; it only returns them. Future adapters that read paths must add their own path allowlist.
160
+
161
+ ---
162
+
163
+ ## 6. Data minimization and redaction
164
+
165
+ Before emitting diagnostics, logs or P1 operation records:
166
+
167
+ - Replace values matching common secret patterns: `password=`, `token=`, `Authorization:`, private key blocks, `kubeconfig` blocks.
168
+ - Truncate command output and error text to a bounded length.
169
+ - Keep only opaque evidence references, not raw evidence.
170
+
171
+ The redaction function is shared and unit-tested.
172
+
173
+ ---
174
+
175
+ ## 7. Scope isolation
176
+
177
+ - Registry data is profile-global and read-only for sessions.
178
+ - Session default environment is per session; one session cannot change another's.
179
+ - Tools accept explicit ids and do not implicitly broaden scope.
180
+ - P1 memory queries always carry environment/service/deployment scope; a single-environment result must not be presented as a global rule.
181
+
182
+ ---
183
+
184
+ ## 8. Fail-safe behavior
185
+
186
+ | Condition | Behavior |
187
+ |---|---|
188
+ | Two same-level bindings | `BINDING_CONFLICT`, no selection |
189
+ | Skill missing or mismatched | `SKILL_UNAVAILABLE`, no fallback |
190
+ | Skill registry unreachable | resolve with `available: 'unknown'` + warning |
191
+ | Production without `allowProduction` | `POLICY_DENIED` |
192
+ | Missing credential | `configured: false`, warning; operation still resolves |
193
+ | Broken reference | error from `env_validate`, skipped on read |
194
+ | Invalid record | skipped on read with diagnostic |
195
+ | Approval denied | `POLICY_DENIED`, no write |
196
+ | UI production write without `allowUiProductionWrites` | `POLICY_DENIED`, no write |
197
+ | Missing transport header or foreign origin | `FORBIDDEN`, no routing |
198
+ | Body over 2 MiB | 413, no parsing |
199
+ | Record cap exceeded | `RECORD_LIMIT_EXCEEDED`, no write |
200
+
201
+ ---
202
+
203
+ ## 9. Audit
204
+
205
+ V1 does not maintain an audit log of its own. P1 operation records become the audit trail through the memory bridge.
206
+
207
+ Requirements for P1:
208
+
209
+ - every write-class operation record includes `sessionId`, target ids, outcome and evidence refs;
210
+ - summaries are redacted;
211
+ - records are append-only from the plugin's perspective;
212
+ - evidence retention is bounded.
213
+
214
+ ---
215
+
216
+ ## 10. Residual risks
217
+
218
+ 1. YAML editing outside the plugin bypasses approval.
219
+ 2. Credential reference swaps are undetectable without a vault policy.
220
+ 3. A correct Skill binding can still be used for an unsafe action; policy is necessary but not sufficient.
221
+ 4. Internal hostnames/IPs in registry files may leak if the repository or data directory is shared carelessly. Registry data must not be committed to Git.
222
+ 5. Operation history in P1 may capture sensitive command text if redaction is incomplete.
223
+ 6. The local HTTP route trusts the page origin and a custom header; a script running in
224
+ the DSH page (extension, devtools) can call it. It can read registry data the user
225
+ can already see, and write only as the `ui` actor under §4.2.
226
+ 7. `ctx.fs.watch` is not used in V1, so an external edit is picked up on the next
227
+ request rather than immediately; a stale resolve cannot outlive one request.
@@ -0,0 +1,224 @@
1
+ # Storage Contract
2
+
3
+ Status: normative for V1.
4
+
5
+ Durable registry data is a directory of YAML files. Nothing else is authoritative.
6
+
7
+ Related: [data-model.md](./data-model.md), [design.md](./design.md) §5.
8
+
9
+ ---
10
+
11
+ ## 1. Location
12
+
13
+ ```text
14
+ <dataDir>/
15
+ ├── environments/{environmentId}.yaml
16
+ ├── services/{serviceId}.yaml
17
+ ├── deployments/{deploymentId}.yaml
18
+ ├── rules/{ruleProfileId}.yaml
19
+ ├── operations/{bindingId}.yaml
20
+ ├── sessions/{sessionId}.json
21
+ └── .backups/
22
+ ```
23
+
24
+ ### 1.1 Resolving `dataDir`
25
+
26
+ Precedence:
27
+
28
+ 1. Plugin config `dataDir`, when set (absolute path or path relative to the profile directory).
29
+ 2. `profileContext.dir` → `<dir>/data/dsh-env-manager`.
30
+ 3. `$DSH_PROFILE_DIR/data/dsh-env-manager`.
31
+ 4. `<working directory>/data/dsh-env-manager` as a last resort for development only.
32
+
33
+ The chosen path is memoized, reported once at startup, and shown in the UI overview. It
34
+ is never derived from model input. Resolution order and fallbacks are implemented in
35
+ `src/storage/paths.ts` (detailed-design §4.1).
36
+
37
+ ### 1.2 Creation
38
+
39
+ - The directory tree is created on first use.
40
+ - A missing directory is not an error; an unwritable directory is an error with `STORAGE_UNWRITABLE`.
41
+
42
+ ---
43
+
44
+ ## 2. File rules
45
+
46
+ | Rule | Value |
47
+ |---|---|
48
+ | Encoding | UTF-8, no BOM |
49
+ | Line endings | LF on write; CRLF tolerated on read |
50
+ | Format | YAML 1.2 |
51
+ | Extension | `.yaml` for records, `.json` for session state |
52
+ | Name | `{id}.yaml`, where `id` matches `^[a-z0-9][a-z0-9-]{0,62}$` |
53
+ | Extra files | Ignored unless they match the extension; ignored files are not deleted |
54
+
55
+ A record file whose `id` differs from its filename is reported by `env_validate` and treated as invalid on load.
56
+
57
+ ---
58
+
59
+ ## 3. Record envelope
60
+
61
+ Every record carries:
62
+
63
+ ```yaml
64
+ id: string
65
+ schemaVersion: 1
66
+ createdAt: 2026-11-01T03:00:00.000Z
67
+ updatedAt: 2026-11-01T03:00:00.000Z
68
+ ```
69
+
70
+ - `createdAt` is set by storage on create.
71
+ - `updatedAt` is set by storage on every successful write.
72
+ - A write input may include both fields; storage overrides them. `createdAt` is
73
+ preserved from the existing file on update, so a caller cannot rewrite history.
74
+ - The stored record always contains both fields.
75
+
76
+ ---
77
+
78
+ ## 4. Atomic write protocol
79
+
80
+ ```text
81
+ 1. Serialize the record to YAML.
82
+ 2. Validate the serialized document by parsing it back and running schema validation.
83
+ 3. Open `.${id}.${pid}.${randomUUID()}.tmp` in the target directory.
84
+ 4. Write UTF-8, fsync, close.
85
+ 5. Copy the previous file into `.backups/<kind>/<id>/<timestamp>.yaml`, prune to 5.
86
+ 6. Rename over the target (atomic on the same volume).
87
+ 7. On a caught failure before rename, delete the temp file and leave the target intact.
88
+ ```
89
+
90
+ Constraints:
91
+
92
+ - Never write directly to the target file.
93
+ - Never leave a temp file behind on success or on a caught failure. A crash can leave
94
+ one; startup removes `*.tmp` older than one hour.
95
+ - A backup failure does not fail the write; it is reported as a warning.
96
+ - Never delete a record file that failed to parse; surface a diagnostic instead.
97
+ - Storage does not reload the registry; the application layer does after a successful
98
+ write.
99
+
100
+ ---
101
+
102
+ ## 5. Read behavior
103
+
104
+ ```text
105
+ 1. Enumerate `*.yaml` in the directory.
106
+ 2. Parse each file.
107
+ 3. Schema-validate.
108
+ 4. On parse error: record `RECORD_INVALID` diagnostic, skip the file.
109
+ 5. On unknown schemaVersion: record `SCHEMA_UNSUPPORTED` warning, skip the file.
110
+ 6. Build an in-memory map keyed by id.
111
+ ```
112
+
113
+ Reads never modify files. A failed file stays on disk for the user to fix.
114
+
115
+ If a directory is unreadable, the loader returns an empty map plus an error diagnostic; tools still serve whatever other directories loaded.
116
+
117
+ ---
118
+
119
+ ## 6. Registry lifecycle
120
+
121
+ - V1 loads the registry at the start of every application request.
122
+ - Concurrent requests share one in-flight load.
123
+ - A successful write triggers a reload before the request returns.
124
+ - The registry is an in-memory snapshot; it is never persisted.
125
+ - V1 has no index files and no watcher. A scan of a few hundred small YAML files is
126
+ fast enough; an mtime-keyed cache is added only with measured need.
127
+
128
+ ---
129
+
130
+ ## 7. Session state
131
+
132
+ ```text
133
+ sessions/{sessionId}.json
134
+ ```
135
+
136
+ ```json
137
+ {
138
+ "schemaVersion": 1,
139
+ "sessionId": "session-abc",
140
+ "defaultEnvironmentId": "test-a",
141
+ "updatedAt": "2026-11-01T03:00:00.000Z"
142
+ }
143
+ ```
144
+
145
+ Rules:
146
+
147
+ - Written atomically with the same protocol as records.
148
+ - A missing file means "no default".
149
+ - A corrupt file is treated as "no default" and reported as `persisted: false`; it is
150
+ not deleted, so the user can inspect it.
151
+ - A process-local cache mirrors the last value so a failed write still works until
152
+ restart (detailed-design §9).
153
+ - Session files are not registry records and are never scanned into the registry.
154
+ - V1 uses this file store; a projection over the durable session log is a later
155
+ refinement (detailed-design §9).
156
+
157
+ ---
158
+
159
+ ## 8. Referential integrity
160
+
161
+ Storage itself does not enforce cross-record references; the registry does, on load and on write:
162
+
163
+ - Broken references are reported by `env_validate`.
164
+ - On load, an invalid reference produces a warning; the record stays usable for name lookups.
165
+ - On write, an invalid reference is rejected (`RECORD_INVALID` with details).
166
+
167
+ This split keeps a single bad file from making the whole registry unusable.
168
+
169
+ ---
170
+
171
+ ## 9. Backups
172
+
173
+ V1 keeps one rolling backup per write:
174
+
175
+ ```text
176
+ .backups/{kind}/{id}/{timestamp}.yaml
177
+ ```
178
+
179
+ - At most 5 backups per record; older ones are pruned.
180
+ - Backups are best-effort: a backup failure does not fail the write, but emits a warning.
181
+ - Backups are not a history feature. They exist to recover from an accidental overwrite.
182
+
183
+ ---
184
+
185
+ ## 10. Migration
186
+
187
+ - `schemaVersion` gates compatibility.
188
+ - V1 accepts only `1`.
189
+ - A future migration must:
190
+ 1. read old files,
191
+ 2. write new files atomically,
192
+ 3. keep a backup,
193
+ 4. report counts,
194
+ 5. never delete the old file until the new one is verified.
195
+
196
+ V1 implements no migration.
197
+
198
+ ---
199
+
200
+ ## 11. What must never be stored
201
+
202
+ - Passwords, tokens, private keys, kubeconfigs.
203
+ - Raw command output or logs.
204
+ - Copy of credential values.
205
+ - Session conversation content.
206
+
207
+ A violation found in validation is reported as `RECORD_INVALID` with the field path but without echoing the value.
208
+
209
+ ---
210
+
211
+ ## 12. Concurrency
212
+
213
+ - Writers are serialized in-process; the plugin is one Host process per profile.
214
+ - Multi-process writers are last-write-wins; no file locks in V1.
215
+ - Readers never block writers.
216
+ - A write that fails validation leaves the previous file untouched.
217
+
218
+ ---
219
+
220
+ ## 13. Repository hygiene
221
+
222
+ - `data/` is git-ignored.
223
+ - The repository ships no registry data, only schemas, examples in docs, and tests fixtures.
224
+ - Test fixtures live under `tests/fixtures/` and contain no real hostnames, IPs or URLs.