dsh-env-manager-plugin 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +273 -250
  3. package/client.js +2581 -2581
  4. package/cordis.patch.yml +4 -4
  5. package/docs/00-project/current-state.md +32 -6
  6. package/docs/00-project/glossary.md +215 -215
  7. package/docs/00-project/project-brief.md +114 -114
  8. package/docs/00-project/roadmap.md +89 -89
  9. package/docs/01-architecture/DSH_Environment_Service_Memory_Skill_Brainstorm.md +493 -493
  10. package/docs/01-architecture/api-verification.md +416 -416
  11. package/docs/01-architecture/architecture-overview.md +222 -222
  12. package/docs/01-architecture/data-model.md +930 -930
  13. package/docs/01-architecture/decisions/ADR-0001-plugin-as-bundle.md +29 -29
  14. package/docs/01-architecture/decisions/ADR-0002-file-based-storage.md +25 -25
  15. package/docs/01-architecture/decisions/ADR-0003-skill-router-over-search.md +25 -25
  16. package/docs/01-architecture/decisions/ADR-0004-typescript-host-js-client.md +31 -31
  17. package/docs/01-architecture/decisions/ADR-0005-layered-rule-resolution.md +36 -36
  18. package/docs/01-architecture/decisions/ADR-0006-credentials-as-references.md +30 -30
  19. package/docs/01-architecture/decisions/ADR-0007-session-scoped-default-environment.md +28 -28
  20. package/docs/01-architecture/decisions/ADR-0008-memory-bridge-not-second-store.md +31 -31
  21. package/docs/01-architecture/decisions/ADR-0009-client-host-http-transport.md +87 -87
  22. package/docs/01-architecture/decisions/ADR-0010-environment-plane-model.md +87 -87
  23. package/docs/01-architecture/decisions/ADR-0011-k8s-node-execution-model.md +92 -92
  24. package/docs/01-architecture/design.md +666 -666
  25. package/docs/01-architecture/detailed-design.md +961 -961
  26. package/docs/01-architecture/implementation-plan.md +323 -323
  27. package/docs/01-architecture/module-boundaries.md +306 -306
  28. package/docs/01-architecture/security-model.md +227 -227
  29. package/docs/01-architecture/storage-contract.md +224 -224
  30. package/docs/01-architecture/tool-contract.md +438 -438
  31. package/docs/01-architecture/ui-design.md +587 -587
  32. package/docs/02-harness/agent-behavior.md +36 -36
  33. package/docs/02-harness/change-snapshot-policy.md +29 -29
  34. package/docs/02-harness/client-ui-integration.md +110 -110
  35. package/docs/02-harness/context-policy.md +43 -43
  36. package/docs/02-harness/harness-standard.md +91 -91
  37. package/docs/02-harness/task-execution-policy.md +26 -26
  38. package/docs/02-harness/verification-policy.md +97 -97
  39. package/docs/03-openspec/archive-rule.md +25 -25
  40. package/docs/03-openspec/bugfix-rule.md +30 -30
  41. package/docs/03-openspec/design-rule.md +33 -33
  42. package/docs/03-openspec/openspec-usage-guide.md +31 -31
  43. package/docs/03-openspec/proposal-rule.md +70 -70
  44. package/docs/03-openspec/spec-rule.md +33 -33
  45. package/docs/03-openspec/tasks-rule.md +31 -31
  46. package/docs/04-development/coding-style.md +67 -67
  47. package/docs/04-development/dependency-policy.md +43 -43
  48. package/docs/04-development/development-guide.md +85 -85
  49. package/docs/04-development/directory-convention.md +105 -105
  50. package/docs/04-development/local-run-guide.md +61 -55
  51. package/docs/04-development/testing-guide.md +102 -102
  52. package/docs/05-domain/deployment-template.md +76 -76
  53. package/docs/05-domain/environment-template.md +165 -165
  54. package/docs/05-domain/operation-binding-template.md +55 -55
  55. package/docs/05-domain/operation-record-template.md +55 -55
  56. package/docs/05-domain/prompt-template.md +90 -90
  57. package/docs/05-domain/rule-template.md +73 -73
  58. package/docs/05-domain/service-template.md +39 -39
  59. package/docs/06-operations/deployment-guide.md +54 -54
  60. package/docs/06-operations/release.md +198 -188
  61. package/docs/06-operations/security-policy.md +53 -53
  62. package/docs/06-operations/troubleshooting.md +141 -91
  63. package/docs/07-templates/adr-template.md +21 -21
  64. package/docs/07-templates/bugfix-template.md +11 -11
  65. package/docs/07-templates/case-template.md +15 -15
  66. package/docs/07-templates/design-template.md +29 -29
  67. package/docs/07-templates/proposal-template.md +19 -19
  68. package/docs/07-templates/spec-template.md +15 -15
  69. package/docs/07-templates/tasks-template.md +12 -12
  70. package/docs/99-archive/README.md +13 -13
  71. package/docs/README.md +142 -142
  72. package/icon.svg +4 -4
  73. package/index.js +31 -8
  74. package/lib/skill.js +37 -37
  75. package/locale/en.json +6 -6
  76. package/locale/zh.json +6 -6
  77. package/package.json +101 -99
  78. package/scripts/repair-profile.mjs +141 -0
@@ -1,930 +1,930 @@
1
- # Data Model (V1)
2
-
3
- Status: normative for V1 implementation.
4
-
5
- Schema version: `1`.
6
-
7
- This document defines the records stored by DSH Environment Manager, the validation rules applied to them, and the structured results returned by tools.
8
-
9
- TypeScript declarations below are the source of truth for the Host implementation. YAML examples illustrate the on-disk form.
10
-
11
- ---
12
-
13
- ## 1. Conventions
14
-
15
- ### 1.1 Identifiers
16
-
17
- - Pattern: `^[a-z0-9][a-z0-9-]{0,62}$` (1–63 characters).
18
- - One namespace per record type: environment ids, service ids, deployment ids, rule profile ids and operation binding ids are unique within their own type.
19
- - A record's `id` must equal its file name without extension.
20
-
21
- ### 1.2 Timestamps
22
-
23
- - ISO 8601 with timezone, stored as UTC (`2026-11-01T03:00:00.000Z`).
24
- - `createdAt` is set once; `updatedAt` changes on every write.
25
-
26
- ### 1.3 Record envelope
27
-
28
- ```ts
29
- interface RecordMeta {
30
- id: string;
31
- schemaVersion: 1;
32
- createdAt: string;
33
- updatedAt: string;
34
- }
35
- ```
36
-
37
- ### 1.4 Validation policy
38
-
39
- - Unknown fields are rejected on write (`RECORD_INVALID`).
40
- - On read, unknown fields are preserved and reported as `UNKNOWN_FIELD` warnings, so a
41
- hand-edited file is inspectable before it is fixed.
42
- - Records with an unknown `schemaVersion` are skipped on read with a warning diagnostic.
43
- - Records failing schema validation are skipped on read with a warning; they are not deleted.
44
- - No field may contain a plain-text secret. `credentialRef` is a reference only.
45
-
46
- ---
47
-
48
- ## 2. Enumerations
49
-
50
- ```ts
51
- type EnvironmentCategory = 'dev' | 'test' | 'staging' | 'production' | 'custom';
52
-
53
- type RuntimeMode = 'native' | 'docker' | 'kubernetes';
54
-
55
- type HostKind = 'vm' | 'physical' | 'k8s-all-in-one' | 'k8s-worker' | 'pod-host';
56
-
57
- type HostAccessMethod = 'ssh' | 'kubectl' | 'docker';
58
-
59
- type ClusterTopology = 'all-in-one' | 'separate' | 'external';
60
-
61
- type Status = 'configured-not-verified' | 'partially-verified' | 'verified';
62
-
63
- type MatchLevel =
64
- | 'deployment'
65
- | 'environment-service'
66
- | 'service'
67
- | 'category-mode'
68
- | 'global';
69
-
70
- type Severity = 'info' | 'warning' | 'error';
71
- ```
72
-
73
- `EnvironmentCategory` drives the production policy check. `custom` never implies production.
74
-
75
- ---
76
-
77
- ## 3. Environment
78
-
79
- An environment is the unit of topology: roles, hosts, clusters and access endpoints travel together in one file. Hosts and clusters are embedded entities rather than top-level records; this keeps co-location edits atomic. Cross-environment host sharing is out of V1 scope (see §14).
80
-
81
- ```ts
82
- interface LogicalRole {
83
- id: string; // e.g. management, business, extension
84
- name: string;
85
- description?: string;
86
- }
87
-
88
- interface Host {
89
- id: string;
90
- name: string; // hostname
91
- kind?: HostKind; // defaults to vm; a machine, not a Pod object
92
- os?: string;
93
- platform?: string; // e.g. Linux / x86_64
94
- ip?: string;
95
- publicIp?: string;
96
- sshPort?: number; // default 22
97
- roles: string[]; // at least one declared LogicalRole id; many-to-many is expected
98
- access?: HostAccessMethod[]; // command surfaces available on this machine
99
- clusterRef?: string; // Cluster.id in this environment
100
- adminUser?: string;
101
- isActive?: boolean;
102
- credentialRef?: string;
103
- accounts?: HostAccount[]; // max 50 per host
104
- note?: string;
105
- }
106
-
107
- type LoginProtocol = 'ssh' | 'rdp' | 'web' | 'k8s-api' | 'docker' | 'custom';
108
- type LoginAuthType = 'password' | 'private-key' | 'agent' | 'token' | 'other';
109
-
110
- interface HostAccount {
111
- id: string;
112
- username: string;
113
- purpose?: string;
114
- protocol?: LoginProtocol; // defaults to ssh
115
- loginUrl?: string; // web console entry URL; never a secret
116
- port?: number;
117
- authType?: LoginAuthType;
118
- credentialRef?: string; // reference only; never a secret value
119
- lastLoginAt?: string; // ISO-8601
120
- lastLoginFrom?: string; // source IP or hostname
121
- note?: string;
122
- }
123
-
124
- interface Cluster {
125
- id: string;
126
- name: string;
127
- kind: 'kubernetes' | 'docker';
128
- topology?: ClusterTopology; // descriptive hint; per-host kind is authoritative
129
- apiEndpoint?: string; // https://...
130
- namespaces?: string[];
131
- credentialRef?: string;
132
- note?: string;
133
- }
134
-
135
- interface AccessEndpoint {
136
- id: string;
137
- kind: 'ssh' | 'web' | 'k8s-api' | 'docker' | 'custom';
138
- hostRef?: string; // Host.id inside this environment
139
- clusterRef?: string; // Cluster.id inside this environment
140
- url?: string;
141
- port?: number;
142
- credentialRef?: string;
143
- note?: string;
144
- }
145
-
146
- interface LoginRecord {
147
- id: string;
148
- at: string; // ISO-8601 login time
149
- username: string;
150
- hostRef?: string; // Host.id inside this environment
151
- accountId?: string; // HostAccount.id on hostRef
152
- protocol?: LoginProtocol;
153
- sourceIp?: string; // client / jump-host address
154
- result: 'success' | 'failure';
155
- note?: string;
156
- }
157
-
158
- interface Environment extends RecordMeta {
159
- name: string;
160
- aliases: string[];
161
- category: EnvironmentCategory;
162
- description?: string;
163
- roles: LogicalRole[]; // at least one; standard four are seeded on create
164
- hosts: Host[];
165
- clusters: Cluster[];
166
- endpoints: AccessEndpoint[];
167
- loginRecords?: LoginRecord[]; // max 200; latest-first is a UI convention, not a schema rule
168
- ruleOverrides?: RuleOverride; // see §7
169
- }
170
- ```
171
-
172
- ### 3.1 Environment, plane and node
173
-
174
- An **environment** is one complete network and one configuration page. Its management, business
175
- and database planes are **role views over the same `hosts[]`**, not separate environments and not
176
- duplicated host records. An environment may also have an expansion / other plane.
177
-
178
- ```text
179
- Environment (one network, one YAML document)
180
- ├── LogicalRole: management
181
- ├── LogicalRole: business
182
- ├── LogicalRole: database
183
- ├── LogicalRole: extension
184
- ├── Host: management-plane-01 roles: [management]
185
- ├── Host: app-db-01 roles: [business, database]
186
- └── Host: app-01 roles: [business]
187
- ```
188
-
189
- Consequences:
190
-
191
- - A host that serves both the management plane and the business plane is **one** `Host` with
192
- `roles: [management, business]`.
193
- - A host may belong to any number of declared roles; co-location is represented once.
194
- - The UI renders one tab per role by filtering `hosts[]`; the same node therefore appears in every
195
- plane it serves. No role-specific copy of the node exists.
196
- - A plane with no nodes is valid (for example, a network without a database tier is valid). A node
197
- with no role is **not** valid.
198
-
199
- Standard role ids are `management`, `business`, `database` and `extension`. They are seeded on
200
- environment creation so the four plane pages exist before the first node is added. Custom role ids
201
- are allowed, but `roles` must contain at least one entry and every host must reference at least one
202
- declared role.
203
-
204
- ### 3.2 Node kind, command surface and cluster topology
205
-
206
- `Host.kind` classifies the **machine**; it is not a logical plane. `Host.access` records which
207
- command surfaces are available on that machine; it is capability metadata, not authorization.
208
- `Host.clusterRef` links a Kubernetes or Pod-hosting machine to a `Cluster` in the same
209
- environment. A Pod is a workload, not a `Host`; the `pod-host` kind is the machine that runs
210
- Pods and is operated with `docker exec`.
211
-
212
- | `Host.kind` | Meaning | `clusterRef` | Form default access |
213
- | --- | --- | --- | --- |
214
- | `vm` | Generic virtual machine | optional | `[ssh]` |
215
- | `physical` | Generic physical machine | optional | `[ssh]` |
216
- | `k8s-all-in-one` | Control plane and worker on the same machine | required | `[ssh, kubectl, docker]` |
217
- | `k8s-worker` | Worker node in a Kubernetes cluster | required | `[ssh, kubectl]` |
218
- | `pod-host` | Machine running Pods, operated through `docker exec` | optional but recommended | `[ssh, docker]` |
219
-
220
- Rules:
221
-
222
- - `Environment.hosts[].kind` defaults to `vm` when omitted, so existing records remain readable.
223
- - `Environment.hosts[].access` may be omitted for legacy records; the UI then shows no command
224
- surface. The form pre-fills the defaults above, but the operator can change them.
225
- - A `k8s-all-in-one` or `k8s-worker` host must reference an existing `Cluster.id`; a missing or
226
- unknown `clusterRef` is a validation error.
227
- - A `pod-host` may omit `clusterRef` when it is a Docker-only machine outside this environment's
228
- Kubernetes cluster. When it does reference a cluster, the reference must exist.
229
- - `access` values are `ssh`, `kubectl` and `docker`. The kind does not restrict access: a worker
230
- may also have Docker, and a VM may have `kubectl`. Duplicate access values are rejected.
231
- - `Cluster.topology` is optional and descriptive: `all-in-one`, `separate` or `external`. It does
232
- not replace per-host `kind`; a legacy cluster without topology is shown as `—`. The schema does
233
- not enforce a particular mix of node kinds, because environments are often migrated incrementally.
234
- - The UI can add and edit clusters, but cluster deletion remains a YAML operation so a referenced
235
- cluster cannot disappear by accident.
236
-
237
- ```yaml
238
- hosts:
239
- - id: k8s-aio-01
240
- name: k8s-aio-01
241
- kind: k8s-all-in-one
242
- clusterRef: k8s-prod
243
- access: [ssh, kubectl, docker]
244
- roles: [management, business]
245
- ip: 10.0.1.20
246
- - id: pod-host-01
247
- name: pod-host-01
248
- kind: pod-host
249
- clusterRef: k8s-prod
250
- access: [ssh, docker]
251
- roles: [business]
252
- ip: 10.0.1.21
253
- - id: worker-01
254
- name: worker-01
255
- kind: k8s-worker
256
- clusterRef: k8s-prod
257
- access: [ssh, kubectl]
258
- roles: [business]
259
- ip: 10.0.1.22
260
-
261
- clusters:
262
- - id: k8s-prod
263
- name: production-k8s
264
- kind: kubernetes
265
- topology: all-in-one
266
- apiEndpoint: https://10.0.1.20:6443
267
- ```
268
-
269
- ### 3.3 Co-location rule
270
-
271
- A host that serves both the management plane and the business plane is **one** `Host` with `roles: [management, business]`.
272
-
273
- Forbidden: two host entries with the same hostname or IP that differ only by role.
274
-
275
- Validation must reject duplicate host ids within an environment and duplicate `ip` values within
276
- an environment. A host must declare at least one role, and every declared role id must exist in
277
- `Environment.roles`.
278
-
279
- ### 3.4 Accounts and login audit
280
-
281
- `Host.accounts` records the identities used to access a host. Each account carries a purpose,
282
- protocol, authentication type, credential reference, and the last observed login time/source.
283
- For a web console, `loginUrl` stores the entry URL and `username` stores the login name; a
284
- web account is still attached to the machine or business host that exposes the console.
285
- `loginUrl` is optional but must be an absolute `http(s)` URL.
286
- Accounts never carry password, key, token, or kubeconfig material; only `credentialRef` is stored.
287
- When the node form or account form receives a password, the Client writes it through the DSH
288
- credential service and stores the generated reference; the password is not written to YAML.
289
-
290
- `Environment.loginRecords` records login observations, including failed attempts. Records are
291
- bounded (200 per environment) and may reference a host and one of its accounts. Entries are
292
- stored in the environment YAML rather than a separate record kind so a topology change and its
293
- audit context remain one atomic write. The UI sorts them newest-first.
294
-
295
- These fields are intentionally lightweight and optional. They are not a replacement for a
296
- privileged-access-management (PAM) or SIEM system; they are context for DSH sessions.
297
-
298
- ### 3.5 YAML example
299
-
300
- ```yaml
301
- id: test-a
302
- schemaVersion: 1
303
- name: Test Environment A
304
- aliases: [ta, test-a]
305
- category: test
306
- description: Main test environment for aicoservice
307
- roles:
308
- - id: management
309
- name: Management Plane
310
- - id: business
311
- name: Business Plane
312
- hosts:
313
- - id: host-001
314
- name: test-a-node-01
315
- kind: vm
316
- os: ubuntu-22.04
317
- platform: Linux / x86_64
318
- ip: 10.0.1.10
319
- sshPort: 22
320
- roles: [management, business]
321
- access: [ssh]
322
- adminUser: root
323
- isActive: true
324
- credentialRef: cred-test-a-root
325
- accounts:
326
- - id: root
327
- username: root
328
- purpose: system administration
329
- protocol: ssh
330
- port: 22
331
- authType: private-key
332
- credentialRef: cred-test-a-root
333
- lastLoginAt: 2026-10-31T08:12:00.000Z
334
- lastLoginFrom: 10.0.8.21
335
- - id: console
336
- username: ops
337
- purpose: business console
338
- protocol: web
339
- loginUrl: https://test-a.example.internal/login
340
- authType: password
341
- credentialRef: env-manager/test-a/console
342
- clusters:
343
- - id: k8s-01
344
- name: test-a-k8s
345
- kind: kubernetes
346
- topology: separate
347
- apiEndpoint: https://10.0.1.20:6443
348
- namespaces: [dv-sop]
349
- credentialRef: cred-test-a-kubeconfig
350
- endpoints:
351
- - id: ep-ssh-01
352
- kind: ssh
353
- hostRef: host-001
354
- port: 22
355
- credentialRef: cred-test-a-root
356
- - id: ep-web-01
357
- kind: web
358
- url: https://test-a.example.internal
359
- loginRecords:
360
- - id: login-0001
361
- at: 2026-10-31T08:12:00.000Z
362
- username: root
363
- hostRef: host-001
364
- accountId: root
365
- protocol: ssh
366
- sourceIp: 10.0.8.21
367
- result: success
368
- note: jump-host session
369
- createdAt: 2026-11-01T03:00:00.000Z
370
- updatedAt: 2026-11-01T03:00:00.000Z
371
- ```
372
-
373
- ---
374
-
375
- ## 4. Service
376
-
377
- A service is environment-independent. Deployment placement lives in §5.
378
-
379
- ```ts
380
- interface GitInfo {
381
- url: string;
382
- defaultBranch: string; // default main
383
- modulePath?: string; // monorepo subpath
384
- }
385
-
386
- interface BuildInfo {
387
- tool: 'maven' | 'gradle' | 'npm' | 'custom';
388
- command?: string;
389
- }
390
-
391
- interface ArtifactInfo {
392
- name: string; // e.g. aicoservice.jar
393
- path?: string; // template allowed
394
- }
395
-
396
- interface Service extends RecordMeta {
397
- name: string;
398
- aliases: string[];
399
- category: string; // free-form service category, e.g. backend
400
- description?: string;
401
- git: GitInfo;
402
- build?: BuildInfo;
403
- artifact?: ArtifactInfo;
404
- deployModes: RuntimeMode[]; // at least one
405
- ruleProfileRefs: string[]; // ordered; later profiles win on field conflicts
406
- }
407
- ```
408
-
409
- ### 4.1 YAML example
410
-
411
- ```yaml
412
- id: aicoservice
413
- schemaVersion: 1
414
- name: Aico Service
415
- aliases: [aicoservice]
416
- category: backend
417
- description: Core AI coordination service
418
- git:
419
- url: https://git.example.internal/team/aicoservice.git
420
- defaultBranch: main
421
- build:
422
- tool: maven
423
- artifact:
424
- name: aicoservice.jar
425
- deployModes: [native, docker, kubernetes]
426
- ruleProfileRefs: [java-common, company-default]
427
- createdAt: 2026-11-01T03:00:00.000Z
428
- updatedAt: 2026-11-01T03:00:00.000Z
429
- ```
430
-
431
- ---
432
-
433
- ## 5. Deployment Binding
434
-
435
- A deployment binding states which service runs where. A service may have several bindings in the same environment; `(environmentId, serviceId)` is therefore not unique.
436
-
437
- ```ts
438
- interface NativeLocation {
439
- kind: 'native';
440
- hostRef: string; // Host.id in the referenced environment
441
- installDir?: string;
442
- serviceName?: string; // systemd unit or Windows service name
443
- processMatch?: string; // process command-line fragment
444
- }
445
-
446
- interface DockerLocation {
447
- kind: 'docker';
448
- hostRef: string;
449
- containerName?: string;
450
- image?: string;
451
- volumes?: string[];
452
- }
453
-
454
- interface KubernetesLocation {
455
- kind: 'kubernetes';
456
- clusterRef: string; // Cluster.id in the referenced environment
457
- namespace: string;
458
- workload: {
459
- kind: 'Deployment' | 'StatefulSet' | 'DaemonSet' | 'CronJob' | 'Pod';
460
- name: string;
461
- };
462
- container?: string;
463
- }
464
-
465
- type DeploymentLocation =
466
- | NativeLocation
467
- | DockerLocation
468
- | KubernetesLocation;
469
-
470
- interface DeploymentBinding extends RecordMeta {
471
- name?: string;
472
- environmentId: string;
473
- serviceId: string;
474
- runtimeMode: RuntimeMode;
475
- location: DeploymentLocation;
476
- variables?: Record<string, string | number | boolean>;
477
- ruleOverrides?: RuleOverride;
478
- }
479
- ```
480
-
481
- Validation: `runtimeMode` must equal `location.kind`; the referenced host/cluster must exist in the referenced environment; the service must list `runtimeMode` in `deployModes`.
482
-
483
- ### 5.1 YAML example
484
-
485
- ```yaml
486
- id: deploy-001
487
- schemaVersion: 1
488
- name: aicoservice-test-a
489
- environmentId: test-a
490
- serviceId: aicoservice
491
- runtimeMode: kubernetes
492
- location:
493
- kind: kubernetes
494
- clusterRef: k8s-01
495
- namespace: dv-sop
496
- workload:
497
- kind: Deployment
498
- name: aicoservice
499
- container: aicoservice
500
- variables:
501
- replicaCount: 2
502
- createdAt: 2026-11-01T03:00:00.000Z
503
- updatedAt: 2026-11-01T03:00:00.000Z
504
- ```
505
-
506
- ---
507
-
508
- ## 6. Rule Profile
509
-
510
- A rule profile is a shared template bundle. Overrides live on the environment (§3) and the deployment (§5).
511
-
512
- ```ts
513
- interface LogSourceFile {
514
- kind: 'file';
515
- path: string; // template allowed
516
- }
517
-
518
- interface LogSourceContainer {
519
- kind: 'container-stdout';
520
- container: string; // template allowed
521
- }
522
-
523
- interface LogSourcePod {
524
- kind: 'pod-logs';
525
- namespace: string; // template allowed
526
- workload: string; // template allowed
527
- }
528
-
529
- type LogSource = LogSourceFile | LogSourceContainer | LogSourcePod;
530
-
531
- interface ClassLocation {
532
- kind: 'build-output' | 'deployed-dir' | 'jar-entry';
533
- path: string; // directory, or jar path for jar-entry
534
- entryPrefix?: string; // jar-internal class prefix
535
- }
536
-
537
- interface RuleOverride {
538
- variables?: Record<string, string | number | boolean>;
539
- paths?: Record<string, string>;
540
- logSources?: Partial<Record<RuntimeMode, LogSource>>;
541
- classLocations?: Partial<Record<RuntimeMode, ClassLocation>>;
542
- }
543
-
544
- interface RuleProfile extends RecordMeta {
545
- name?: string;
546
- description?: string;
547
- variables?: Record<string, string | number | boolean>;
548
- paths?: Record<string, string>;
549
- logSources?: Partial<Record<RuntimeMode, LogSource>>;
550
- classLocations?: Partial<Record<RuntimeMode, ClassLocation>>;
551
- }
552
- ```
553
-
554
- `classLocation.kind` distinguishes the three real sources: build output (`target/classes`), a deployed directory of classes, or an entry inside a JAR. V1 never collapses these into one path.
555
-
556
- ### 6.1 YAML example
557
-
558
- ```yaml
559
- id: java-common
560
- schemaVersion: 1
561
- description: Common Java service paths and log sources
562
- variables:
563
- installRoot: /opt/company/services
564
- paths:
565
- serviceHome: ${installRoot}/${service.id}
566
- jar: ${paths.serviceHome}/lib/${service.artifact.name}
567
- logDir: ${paths.serviceHome}/logs
568
- applicationLog: ${paths.logDir}/${service.id}.log
569
- logSources:
570
- native:
571
- kind: file
572
- path: ${paths.applicationLog}
573
- docker:
574
- kind: container-stdout
575
- container: ${deployment.location.containerName}
576
- kubernetes:
577
- kind: pod-logs
578
- namespace: ${deployment.location.namespace}
579
- workload: ${deployment.location.workload.name}
580
- classLocations:
581
- native:
582
- kind: deployed-dir
583
- path: ${paths.serviceHome}/classes
584
- kubernetes:
585
- kind: jar-entry
586
- path: ${paths.jar}
587
- entryPrefix: com/company/aico
588
- createdAt: 2026-11-01T03:00:00.000Z
589
- updatedAt: 2026-11-01T03:00:00.000Z
590
- ```
591
-
592
- ---
593
-
594
- ## 7. Operation Binding
595
-
596
- ```ts
597
- interface OperationMatch {
598
- environmentId?: string;
599
- serviceId?: string;
600
- deploymentId?: string;
601
- deploymentMode?: RuntimeMode;
602
- serviceCategory?: string;
603
- }
604
-
605
- interface SkillReference {
606
- name: string;
607
- expectedSource?: string | null; // SkillSummary.source, e.g. project-dsh
608
- expectedProvider?: string | null; // SkillSummary.provider
609
- }
610
-
611
- interface OperationPolicy {
612
- requiresApproval: boolean;
613
- allowProduction: boolean;
614
- }
615
-
616
- interface OperationBinding extends RecordMeta {
617
- operation: string; // standard id or custom id
618
- match: OperationMatch; // empty object means global
619
- skill: SkillReference;
620
- parameters?: Record<string, string>;
621
- policy: OperationPolicy;
622
- enabled: boolean;
623
- }
624
- ```
625
-
626
- `match` must be one of five shapes; any other combination is `RECORD_INVALID`:
627
-
628
- | Shape | Level |
629
- |---|---|
630
- | `{ deploymentId }` plus optional `environmentId`/`serviceId` | `deployment` |
631
- | `{ environmentId, serviceId }` | `environment-service` |
632
- | `{ serviceId }` | `service` |
633
- | `{ serviceCategory, deploymentMode }` | `category-mode` |
634
- | `{}` | `global` |
635
-
636
- `environmentId` alone, `deploymentMode` alone, or `environmentId` + `deploymentMode` are
637
- invalid: they would create a sixth level with no defined precedence.
638
-
639
- ### 7.1 Standard operation ids
640
-
641
- ```text
642
- install, deploy, redeploy, replace, restart, upgrade, rollback, uninstall,
643
- collect-logs, health-check, inspect, upload, download
644
- ```
645
-
646
- Any other id is treated as a custom operation and must appear explicitly in a binding.
647
-
648
- ### 7.2 Write operations
649
-
650
- `install`, `deploy`, `redeploy`, `replace`, `restart`, `upgrade`, `rollback`, `uninstall`, `upload` are write operations. For them, `policy.requiresApproval` defaults to `true` when the field is omitted, and a binding cannot implicitly grant production access.
651
-
652
- ### 7.3 YAML example
653
-
654
- ```yaml
655
- id: binding-001
656
- schemaVersion: 1
657
- operation: redeploy
658
- match:
659
- environmentId: test-a
660
- serviceId: aicoservice
661
- skill:
662
- name: redeploy-k8s-service
663
- expectedSource: null
664
- expectedProvider: null
665
- parameters:
666
- namespace: ${deployment.location.namespace}
667
- workload: ${deployment.location.workload.name}
668
- cluster: ${deployment.location.clusterRef}
669
- policy:
670
- requiresApproval: true
671
- allowProduction: false
672
- enabled: true
673
- createdAt: 2026-11-01T03:00:00.000Z
674
- updatedAt: 2026-11-01T03:00:00.000Z
675
- ```
676
-
677
- ---
678
-
679
- ## 8. Resolved results
680
-
681
- ### 8.1 Rule source and provenance
682
-
683
- ```ts
684
- type RuleLayer = 'global' | 'service' | 'environment' | 'deployment';
685
-
686
- interface RuleSource {
687
- layer: RuleLayer;
688
- ruleProfileId?: string;
689
- recordId?: string; // environmentId / deploymentId / serviceId
690
- }
691
-
692
- interface ProvenanceInput {
693
- name: string; // e.g. paths.logDir
694
- source: RuleSource;
695
- }
696
-
697
- interface ProvenanceEntry {
698
- field: string; // e.g. paths.applicationLog, logSources.kubernetes.namespace
699
- definedBy: RuleSource; // layer that supplied the winning template
700
- inputs: ProvenanceInput[]; // referenced fields and their winning sources
701
- shadowed: RuleSource[]; // lower-priority definitions that lost
702
- value: string | number | boolean;
703
- }
704
-
705
- interface ResolvedField<T> {
706
- value: T;
707
- provenance: ProvenanceEntry;
708
- }
709
-
710
- interface ResolvedStructured<T> {
711
- value: T;
712
- provenance: ProvenanceEntry[]; // one entry per property
713
- }
714
- ```
715
-
716
- `definedBy` answers "where did this template come from". `inputs` answers "what influenced the final value". Both are required: an override of `paths.logDir` changes `paths.applicationLog` even though the latter was defined at the service layer.
717
-
718
- ### 8.2 Service Context
719
-
720
- ```ts
721
- interface CredentialStatus {
722
- ref: string;
723
- configured: boolean;
724
- source?: string; // where the value lives, e.g. profile-vault
725
- }
726
-
727
- interface ServiceContext {
728
- environmentId: string;
729
- environmentName: string;
730
- environmentCategory: EnvironmentCategory;
731
- serviceId: string;
732
- serviceName: string;
733
- serviceCategory: string;
734
- deploymentId: string;
735
- deploymentName?: string;
736
- runtimeMode: RuntimeMode;
737
- source: {
738
- gitUrl?: string;
739
- defaultBranch?: string;
740
- modulePath?: string;
741
- };
742
- artifact?: {
743
- name: string;
744
- path?: string;
745
- };
746
- location: DeploymentLocation;
747
- variables: Record<string, ResolvedField<string | number | boolean>>;
748
- paths: Record<string, ResolvedField<string>>;
749
- logSource?: ResolvedStructured<LogSource>;
750
- classLocation?: ResolvedStructured<ClassLocation>;
751
- ruleProfiles: string[];
752
- status: Status;
753
- credentials: CredentialStatus[];
754
- diagnostics: Diagnostic[];
755
- }
756
- ```
757
-
758
- `status` starts as `configured-not-verified`. Without an online check, V1 must not claim `verified`.
759
-
760
- ### 8.3 Operation Resolution
761
-
762
- ```ts
763
- interface Diagnostic {
764
- code: string;
765
- severity: Severity;
766
- message: string;
767
- file?: string; // registry-relative file name
768
- path?: string; // field path, e.g. ruleProfileRefs[0]
769
- details?: Record<string, unknown>;
770
- }
771
-
772
- interface ResolvedSkillReference {
773
- name: string;
774
- source?: string | null;
775
- provider?: string | null;
776
- available: boolean | 'unknown';
777
- }
778
-
779
- interface OperationResolution {
780
- ok: boolean;
781
- operation: string;
782
- requested: {
783
- environmentId: string;
784
- serviceId?: string;
785
- deploymentId?: string;
786
- };
787
- matchedBindingId?: string;
788
- matchLevel?: MatchLevel;
789
- skill?: ResolvedSkillReference;
790
- parameters?: Record<string, string>;
791
- policy?: OperationPolicy;
792
- diagnostics: Diagnostic[];
793
- }
794
- ```
795
-
796
- ---
797
-
798
- ## 9. Session context
799
-
800
- ```ts
801
- interface SessionContext {
802
- sessionId: string;
803
- defaultEnvironmentId: string | null;
804
- persisted: boolean;
805
- updatedAt: string;
806
- }
807
- ```
808
-
809
- Session context is per session and never mutates registry records. It is stored under `sessions/` in the plugin data directory, keyed by session id.
810
-
811
- ---
812
-
813
- ## 10. Operation record (P1 boundary)
814
-
815
- ```ts
816
- interface OperationRecord {
817
- id: string;
818
- sessionId: string;
819
- environmentId: string;
820
- serviceId?: string;
821
- deploymentId?: string;
822
- operation: string;
823
- skillRef?: string;
824
- startedAt: string;
825
- finishedAt?: string;
826
- outcome: 'success' | 'failure' | 'unknown';
827
- evidenceRefs: string[]; // opaque, sanitized references
828
- summary: string;
829
- }
830
- ```
831
-
832
- V1 defines the type and a no-op bridge; it does not persist operation records itself.
833
-
834
- ---
835
-
836
- ## 11. Diagnostics catalogue
837
-
838
- | Code | Severity | Meaning |
839
- |---|---|---|
840
- | `ENV_NOT_FOUND` | error | Environment id does not exist |
841
- | `SERVICE_NOT_FOUND` | error | Service id does not exist |
842
- | `DEPLOYMENT_NOT_FOUND` | error | Deployment id does not exist |
843
- | `HOST_NOT_FOUND` | error | Deployment host reference, or a login record's host reference, is missing |
844
- | `CLUSTER_NOT_FOUND` | error | Deployment cluster reference is missing in the environment |
845
- | `DUPLICATE_HOST_ID` | warning | Two hosts in one environment share an id |
846
- | `DUPLICATE_HOST_IP` | warning | Two hosts in one environment share an IP |
847
- | `DUPLICATE_ACCOUNT_ID` | warning | Two accounts on one host share an id |
848
- | `ACCOUNT_NOT_FOUND` | warning | A login record references an account that is not on its host |
849
- | `RULE_PROFILE_NOT_FOUND` | error | `ruleProfileRefs` names a missing profile |
850
- | `DEPLOYMENT_AMBIGUOUS` | error | No deployment id given and several bindings match |
851
- | `DUPLICATE_ID` | warning | Two files declare the same id; the later file is skipped |
852
- | `BINDING_NOT_FOUND` | error | No operation binding matches the context |
853
- | `RECORD_INVALID` | error | Record failed schema validation |
854
- | `RECORD_IN_USE` | error | Record is still referenced and cannot be deleted |
855
- | `RECORD_LIMIT_EXCEEDED` | error | Record count would exceed `maxRecords` |
856
- | `RECORD_TOO_LARGE` | error | Serialized record exceeds 1 MiB |
857
- | `INVALID_ARGUMENT` | error | Tool or request input failed validation |
858
- | `FORBIDDEN` | error | Request rejected by transport policy |
859
- | `STORAGE_UNWRITABLE` | error | Data directory cannot be written |
860
- | `SCHEMA_UNSUPPORTED` | warning | Unknown `schemaVersion`, record skipped |
861
- | `UNKNOWN_FIELD` | warning | Read-only: a field outside the schema is present |
862
- | `RULE_TEMPLATE_INVALID` | error | Malformed `${...}` or identifier |
863
- | `RULE_UNDEFINED_VARIABLE` | error | Template references an unknown field |
864
- | `RULE_CYCLE` | error | Template variable cycle |
865
- | `RULE_PATH_TRAVERSAL` | error | Resolved path contains `..` |
866
- | `RULE_TYPE_MISMATCH` | error | Template used where a non-string value is required |
867
- | `OPERATION_UNKNOWN` | error | Operation is neither standard nor bound |
868
- | `BINDING_CONFLICT` | error | Multiple bindings at the highest match level |
869
- | `SKILL_UNAVAILABLE` | error | Skill not registered, or source/provider mismatch |
870
- | `SKILL_REGISTRY_UNAVAILABLE` | warning | Skill registry cannot be queried; availability unknown |
871
- | `POLICY_DENIED` | error | Production policy or approval policy rejects the operation |
872
- | `PARAMETER_RESOLUTION_FAILED` | error | Parameter template failed to resolve |
873
- | `CREDENTIAL_MISSING` | warning | `credentialRef` has no configured value |
874
- | `MEMORY_BRIDGE_UNAVAILABLE` | warning | Memory plugin not installed |
875
- | `SESSION_CONTEXT_UNAVAILABLE` | error | A session-scoped tool ran without an owning agent |
876
- | `SESSION_CONTEXT_UNPERSISTED` | warning | Session state could not be written |
877
- | `INTERNAL_ERROR` | error | Unexpected failure, message redacted |
878
-
879
- ---
880
-
881
- ## 12. Referential integrity
882
-
883
- Validated on every write and by `env_validate`:
884
-
885
- 1. `DeploymentBinding.environmentId` exists.
886
- 2. `DeploymentBinding.serviceId` exists.
887
- 3. `DeploymentBinding.location.hostRef` exists in that environment (native/docker).
888
- 4. `DeploymentBinding.location.clusterRef` exists in that environment (kubernetes).
889
- 5. `Service.ruleProfileRefs` all exist.
890
- 6. `Environment.hosts[].roles` reference declared roles.
891
- 7. `Environment.endpoints[].hostRef` / `clusterRef` exist in that environment.
892
- 8. `Environment.hosts[].accounts[].id` are unique within their host.
893
- 9. `Environment.loginRecords[].hostRef` exists in that environment.
894
- 10. `Environment.loginRecords[].accountId` exists on the referenced host when both are set.
895
- 11. `OperationBinding` match references (when present) exist.
896
- 12. `OperationBinding.skill.name` is non-empty.
897
-
898
- Duplicate host ids and duplicate host IPs are also reported as integrity errors.
899
- Schema validation additionally rejects an environment whose `roles` is empty, and a host whose
900
- `roles` is empty. A host role that is not declared in `Environment.roles` is reported as
901
- `ROLE_NOT_FOUND` by integrity validation.
902
-
903
- Broken references are reported, never silently repaired.
904
-
905
- ---
906
-
907
- ## 13. Identity and cardinality
908
-
909
- ```text
910
- Environment 1 ── N Host
911
- Environment 1 ── N Cluster
912
- Environment 1 ── N Role
913
- Host N ── N Role (via Host.roles)
914
- Host N ── N Environment (out of V1; hosts are embedded)
915
- Service 1 ── N DeploymentBinding
916
- Environment 1 ── N DeploymentBinding
917
- Service 1 ── N RuleProfile (ordered ruleProfileRefs)
918
- OperationBinding N ── predicate over Environment/Service/Deployment
919
- ```
920
-
921
- ---
922
-
923
- ## 14. Out-of-scope model changes
924
-
925
- The following are deliberately postponed:
926
-
927
- - Extracting hosts into a top-level registry for cross-environment sharing.
928
- - Storing discovered assets separately from declared assets.
929
- - Vector or embedding-based retrieval for services.
930
- - Per-user permissions inside one DSH profile.
1
+ # Data Model (V1)
2
+
3
+ Status: normative for V1 implementation.
4
+
5
+ Schema version: `1`.
6
+
7
+ This document defines the records stored by DSH Environment Manager, the validation rules applied to them, and the structured results returned by tools.
8
+
9
+ TypeScript declarations below are the source of truth for the Host implementation. YAML examples illustrate the on-disk form.
10
+
11
+ ---
12
+
13
+ ## 1. Conventions
14
+
15
+ ### 1.1 Identifiers
16
+
17
+ - Pattern: `^[a-z0-9][a-z0-9-]{0,62}$` (1–63 characters).
18
+ - One namespace per record type: environment ids, service ids, deployment ids, rule profile ids and operation binding ids are unique within their own type.
19
+ - A record's `id` must equal its file name without extension.
20
+
21
+ ### 1.2 Timestamps
22
+
23
+ - ISO 8601 with timezone, stored as UTC (`2026-11-01T03:00:00.000Z`).
24
+ - `createdAt` is set once; `updatedAt` changes on every write.
25
+
26
+ ### 1.3 Record envelope
27
+
28
+ ```ts
29
+ interface RecordMeta {
30
+ id: string;
31
+ schemaVersion: 1;
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ }
35
+ ```
36
+
37
+ ### 1.4 Validation policy
38
+
39
+ - Unknown fields are rejected on write (`RECORD_INVALID`).
40
+ - On read, unknown fields are preserved and reported as `UNKNOWN_FIELD` warnings, so a
41
+ hand-edited file is inspectable before it is fixed.
42
+ - Records with an unknown `schemaVersion` are skipped on read with a warning diagnostic.
43
+ - Records failing schema validation are skipped on read with a warning; they are not deleted.
44
+ - No field may contain a plain-text secret. `credentialRef` is a reference only.
45
+
46
+ ---
47
+
48
+ ## 2. Enumerations
49
+
50
+ ```ts
51
+ type EnvironmentCategory = 'dev' | 'test' | 'staging' | 'production' | 'custom';
52
+
53
+ type RuntimeMode = 'native' | 'docker' | 'kubernetes';
54
+
55
+ type HostKind = 'vm' | 'physical' | 'k8s-all-in-one' | 'k8s-worker' | 'pod-host';
56
+
57
+ type HostAccessMethod = 'ssh' | 'kubectl' | 'docker';
58
+
59
+ type ClusterTopology = 'all-in-one' | 'separate' | 'external';
60
+
61
+ type Status = 'configured-not-verified' | 'partially-verified' | 'verified';
62
+
63
+ type MatchLevel =
64
+ | 'deployment'
65
+ | 'environment-service'
66
+ | 'service'
67
+ | 'category-mode'
68
+ | 'global';
69
+
70
+ type Severity = 'info' | 'warning' | 'error';
71
+ ```
72
+
73
+ `EnvironmentCategory` drives the production policy check. `custom` never implies production.
74
+
75
+ ---
76
+
77
+ ## 3. Environment
78
+
79
+ An environment is the unit of topology: roles, hosts, clusters and access endpoints travel together in one file. Hosts and clusters are embedded entities rather than top-level records; this keeps co-location edits atomic. Cross-environment host sharing is out of V1 scope (see §14).
80
+
81
+ ```ts
82
+ interface LogicalRole {
83
+ id: string; // e.g. management, business, extension
84
+ name: string;
85
+ description?: string;
86
+ }
87
+
88
+ interface Host {
89
+ id: string;
90
+ name: string; // hostname
91
+ kind?: HostKind; // defaults to vm; a machine, not a Pod object
92
+ os?: string;
93
+ platform?: string; // e.g. Linux / x86_64
94
+ ip?: string;
95
+ publicIp?: string;
96
+ sshPort?: number; // default 22
97
+ roles: string[]; // at least one declared LogicalRole id; many-to-many is expected
98
+ access?: HostAccessMethod[]; // command surfaces available on this machine
99
+ clusterRef?: string; // Cluster.id in this environment
100
+ adminUser?: string;
101
+ isActive?: boolean;
102
+ credentialRef?: string;
103
+ accounts?: HostAccount[]; // max 50 per host
104
+ note?: string;
105
+ }
106
+
107
+ type LoginProtocol = 'ssh' | 'rdp' | 'web' | 'k8s-api' | 'docker' | 'custom';
108
+ type LoginAuthType = 'password' | 'private-key' | 'agent' | 'token' | 'other';
109
+
110
+ interface HostAccount {
111
+ id: string;
112
+ username: string;
113
+ purpose?: string;
114
+ protocol?: LoginProtocol; // defaults to ssh
115
+ loginUrl?: string; // web console entry URL; never a secret
116
+ port?: number;
117
+ authType?: LoginAuthType;
118
+ credentialRef?: string; // reference only; never a secret value
119
+ lastLoginAt?: string; // ISO-8601
120
+ lastLoginFrom?: string; // source IP or hostname
121
+ note?: string;
122
+ }
123
+
124
+ interface Cluster {
125
+ id: string;
126
+ name: string;
127
+ kind: 'kubernetes' | 'docker';
128
+ topology?: ClusterTopology; // descriptive hint; per-host kind is authoritative
129
+ apiEndpoint?: string; // https://...
130
+ namespaces?: string[];
131
+ credentialRef?: string;
132
+ note?: string;
133
+ }
134
+
135
+ interface AccessEndpoint {
136
+ id: string;
137
+ kind: 'ssh' | 'web' | 'k8s-api' | 'docker' | 'custom';
138
+ hostRef?: string; // Host.id inside this environment
139
+ clusterRef?: string; // Cluster.id inside this environment
140
+ url?: string;
141
+ port?: number;
142
+ credentialRef?: string;
143
+ note?: string;
144
+ }
145
+
146
+ interface LoginRecord {
147
+ id: string;
148
+ at: string; // ISO-8601 login time
149
+ username: string;
150
+ hostRef?: string; // Host.id inside this environment
151
+ accountId?: string; // HostAccount.id on hostRef
152
+ protocol?: LoginProtocol;
153
+ sourceIp?: string; // client / jump-host address
154
+ result: 'success' | 'failure';
155
+ note?: string;
156
+ }
157
+
158
+ interface Environment extends RecordMeta {
159
+ name: string;
160
+ aliases: string[];
161
+ category: EnvironmentCategory;
162
+ description?: string;
163
+ roles: LogicalRole[]; // at least one; standard four are seeded on create
164
+ hosts: Host[];
165
+ clusters: Cluster[];
166
+ endpoints: AccessEndpoint[];
167
+ loginRecords?: LoginRecord[]; // max 200; latest-first is a UI convention, not a schema rule
168
+ ruleOverrides?: RuleOverride; // see §7
169
+ }
170
+ ```
171
+
172
+ ### 3.1 Environment, plane and node
173
+
174
+ An **environment** is one complete network and one configuration page. Its management, business
175
+ and database planes are **role views over the same `hosts[]`**, not separate environments and not
176
+ duplicated host records. An environment may also have an expansion / other plane.
177
+
178
+ ```text
179
+ Environment (one network, one YAML document)
180
+ ├── LogicalRole: management
181
+ ├── LogicalRole: business
182
+ ├── LogicalRole: database
183
+ ├── LogicalRole: extension
184
+ ├── Host: management-plane-01 roles: [management]
185
+ ├── Host: app-db-01 roles: [business, database]
186
+ └── Host: app-01 roles: [business]
187
+ ```
188
+
189
+ Consequences:
190
+
191
+ - A host that serves both the management plane and the business plane is **one** `Host` with
192
+ `roles: [management, business]`.
193
+ - A host may belong to any number of declared roles; co-location is represented once.
194
+ - The UI renders one tab per role by filtering `hosts[]`; the same node therefore appears in every
195
+ plane it serves. No role-specific copy of the node exists.
196
+ - A plane with no nodes is valid (for example, a network without a database tier is valid). A node
197
+ with no role is **not** valid.
198
+
199
+ Standard role ids are `management`, `business`, `database` and `extension`. They are seeded on
200
+ environment creation so the four plane pages exist before the first node is added. Custom role ids
201
+ are allowed, but `roles` must contain at least one entry and every host must reference at least one
202
+ declared role.
203
+
204
+ ### 3.2 Node kind, command surface and cluster topology
205
+
206
+ `Host.kind` classifies the **machine**; it is not a logical plane. `Host.access` records which
207
+ command surfaces are available on that machine; it is capability metadata, not authorization.
208
+ `Host.clusterRef` links a Kubernetes or Pod-hosting machine to a `Cluster` in the same
209
+ environment. A Pod is a workload, not a `Host`; the `pod-host` kind is the machine that runs
210
+ Pods and is operated with `docker exec`.
211
+
212
+ | `Host.kind` | Meaning | `clusterRef` | Form default access |
213
+ | --- | --- | --- | --- |
214
+ | `vm` | Generic virtual machine | optional | `[ssh]` |
215
+ | `physical` | Generic physical machine | optional | `[ssh]` |
216
+ | `k8s-all-in-one` | Control plane and worker on the same machine | required | `[ssh, kubectl, docker]` |
217
+ | `k8s-worker` | Worker node in a Kubernetes cluster | required | `[ssh, kubectl]` |
218
+ | `pod-host` | Machine running Pods, operated through `docker exec` | optional but recommended | `[ssh, docker]` |
219
+
220
+ Rules:
221
+
222
+ - `Environment.hosts[].kind` defaults to `vm` when omitted, so existing records remain readable.
223
+ - `Environment.hosts[].access` may be omitted for legacy records; the UI then shows no command
224
+ surface. The form pre-fills the defaults above, but the operator can change them.
225
+ - A `k8s-all-in-one` or `k8s-worker` host must reference an existing `Cluster.id`; a missing or
226
+ unknown `clusterRef` is a validation error.
227
+ - A `pod-host` may omit `clusterRef` when it is a Docker-only machine outside this environment's
228
+ Kubernetes cluster. When it does reference a cluster, the reference must exist.
229
+ - `access` values are `ssh`, `kubectl` and `docker`. The kind does not restrict access: a worker
230
+ may also have Docker, and a VM may have `kubectl`. Duplicate access values are rejected.
231
+ - `Cluster.topology` is optional and descriptive: `all-in-one`, `separate` or `external`. It does
232
+ not replace per-host `kind`; a legacy cluster without topology is shown as `—`. The schema does
233
+ not enforce a particular mix of node kinds, because environments are often migrated incrementally.
234
+ - The UI can add and edit clusters, but cluster deletion remains a YAML operation so a referenced
235
+ cluster cannot disappear by accident.
236
+
237
+ ```yaml
238
+ hosts:
239
+ - id: k8s-aio-01
240
+ name: k8s-aio-01
241
+ kind: k8s-all-in-one
242
+ clusterRef: k8s-prod
243
+ access: [ssh, kubectl, docker]
244
+ roles: [management, business]
245
+ ip: 10.0.1.20
246
+ - id: pod-host-01
247
+ name: pod-host-01
248
+ kind: pod-host
249
+ clusterRef: k8s-prod
250
+ access: [ssh, docker]
251
+ roles: [business]
252
+ ip: 10.0.1.21
253
+ - id: worker-01
254
+ name: worker-01
255
+ kind: k8s-worker
256
+ clusterRef: k8s-prod
257
+ access: [ssh, kubectl]
258
+ roles: [business]
259
+ ip: 10.0.1.22
260
+
261
+ clusters:
262
+ - id: k8s-prod
263
+ name: production-k8s
264
+ kind: kubernetes
265
+ topology: all-in-one
266
+ apiEndpoint: https://10.0.1.20:6443
267
+ ```
268
+
269
+ ### 3.3 Co-location rule
270
+
271
+ A host that serves both the management plane and the business plane is **one** `Host` with `roles: [management, business]`.
272
+
273
+ Forbidden: two host entries with the same hostname or IP that differ only by role.
274
+
275
+ Validation must reject duplicate host ids within an environment and duplicate `ip` values within
276
+ an environment. A host must declare at least one role, and every declared role id must exist in
277
+ `Environment.roles`.
278
+
279
+ ### 3.4 Accounts and login audit
280
+
281
+ `Host.accounts` records the identities used to access a host. Each account carries a purpose,
282
+ protocol, authentication type, credential reference, and the last observed login time/source.
283
+ For a web console, `loginUrl` stores the entry URL and `username` stores the login name; a
284
+ web account is still attached to the machine or business host that exposes the console.
285
+ `loginUrl` is optional but must be an absolute `http(s)` URL.
286
+ Accounts never carry password, key, token, or kubeconfig material; only `credentialRef` is stored.
287
+ When the node form or account form receives a password, the Client writes it through the DSH
288
+ credential service and stores the generated reference; the password is not written to YAML.
289
+
290
+ `Environment.loginRecords` records login observations, including failed attempts. Records are
291
+ bounded (200 per environment) and may reference a host and one of its accounts. Entries are
292
+ stored in the environment YAML rather than a separate record kind so a topology change and its
293
+ audit context remain one atomic write. The UI sorts them newest-first.
294
+
295
+ These fields are intentionally lightweight and optional. They are not a replacement for a
296
+ privileged-access-management (PAM) or SIEM system; they are context for DSH sessions.
297
+
298
+ ### 3.5 YAML example
299
+
300
+ ```yaml
301
+ id: test-a
302
+ schemaVersion: 1
303
+ name: Test Environment A
304
+ aliases: [ta, test-a]
305
+ category: test
306
+ description: Main test environment for aicoservice
307
+ roles:
308
+ - id: management
309
+ name: Management Plane
310
+ - id: business
311
+ name: Business Plane
312
+ hosts:
313
+ - id: host-001
314
+ name: test-a-node-01
315
+ kind: vm
316
+ os: ubuntu-22.04
317
+ platform: Linux / x86_64
318
+ ip: 10.0.1.10
319
+ sshPort: 22
320
+ roles: [management, business]
321
+ access: [ssh]
322
+ adminUser: root
323
+ isActive: true
324
+ credentialRef: cred-test-a-root
325
+ accounts:
326
+ - id: root
327
+ username: root
328
+ purpose: system administration
329
+ protocol: ssh
330
+ port: 22
331
+ authType: private-key
332
+ credentialRef: cred-test-a-root
333
+ lastLoginAt: 2026-10-31T08:12:00.000Z
334
+ lastLoginFrom: 10.0.8.21
335
+ - id: console
336
+ username: ops
337
+ purpose: business console
338
+ protocol: web
339
+ loginUrl: https://test-a.example.internal/login
340
+ authType: password
341
+ credentialRef: env-manager/test-a/console
342
+ clusters:
343
+ - id: k8s-01
344
+ name: test-a-k8s
345
+ kind: kubernetes
346
+ topology: separate
347
+ apiEndpoint: https://10.0.1.20:6443
348
+ namespaces: [dv-sop]
349
+ credentialRef: cred-test-a-kubeconfig
350
+ endpoints:
351
+ - id: ep-ssh-01
352
+ kind: ssh
353
+ hostRef: host-001
354
+ port: 22
355
+ credentialRef: cred-test-a-root
356
+ - id: ep-web-01
357
+ kind: web
358
+ url: https://test-a.example.internal
359
+ loginRecords:
360
+ - id: login-0001
361
+ at: 2026-10-31T08:12:00.000Z
362
+ username: root
363
+ hostRef: host-001
364
+ accountId: root
365
+ protocol: ssh
366
+ sourceIp: 10.0.8.21
367
+ result: success
368
+ note: jump-host session
369
+ createdAt: 2026-11-01T03:00:00.000Z
370
+ updatedAt: 2026-11-01T03:00:00.000Z
371
+ ```
372
+
373
+ ---
374
+
375
+ ## 4. Service
376
+
377
+ A service is environment-independent. Deployment placement lives in §5.
378
+
379
+ ```ts
380
+ interface GitInfo {
381
+ url: string;
382
+ defaultBranch: string; // default main
383
+ modulePath?: string; // monorepo subpath
384
+ }
385
+
386
+ interface BuildInfo {
387
+ tool: 'maven' | 'gradle' | 'npm' | 'custom';
388
+ command?: string;
389
+ }
390
+
391
+ interface ArtifactInfo {
392
+ name: string; // e.g. aicoservice.jar
393
+ path?: string; // template allowed
394
+ }
395
+
396
+ interface Service extends RecordMeta {
397
+ name: string;
398
+ aliases: string[];
399
+ category: string; // free-form service category, e.g. backend
400
+ description?: string;
401
+ git: GitInfo;
402
+ build?: BuildInfo;
403
+ artifact?: ArtifactInfo;
404
+ deployModes: RuntimeMode[]; // at least one
405
+ ruleProfileRefs: string[]; // ordered; later profiles win on field conflicts
406
+ }
407
+ ```
408
+
409
+ ### 4.1 YAML example
410
+
411
+ ```yaml
412
+ id: aicoservice
413
+ schemaVersion: 1
414
+ name: Aico Service
415
+ aliases: [aicoservice]
416
+ category: backend
417
+ description: Core AI coordination service
418
+ git:
419
+ url: https://git.example.internal/team/aicoservice.git
420
+ defaultBranch: main
421
+ build:
422
+ tool: maven
423
+ artifact:
424
+ name: aicoservice.jar
425
+ deployModes: [native, docker, kubernetes]
426
+ ruleProfileRefs: [java-common, company-default]
427
+ createdAt: 2026-11-01T03:00:00.000Z
428
+ updatedAt: 2026-11-01T03:00:00.000Z
429
+ ```
430
+
431
+ ---
432
+
433
+ ## 5. Deployment Binding
434
+
435
+ A deployment binding states which service runs where. A service may have several bindings in the same environment; `(environmentId, serviceId)` is therefore not unique.
436
+
437
+ ```ts
438
+ interface NativeLocation {
439
+ kind: 'native';
440
+ hostRef: string; // Host.id in the referenced environment
441
+ installDir?: string;
442
+ serviceName?: string; // systemd unit or Windows service name
443
+ processMatch?: string; // process command-line fragment
444
+ }
445
+
446
+ interface DockerLocation {
447
+ kind: 'docker';
448
+ hostRef: string;
449
+ containerName?: string;
450
+ image?: string;
451
+ volumes?: string[];
452
+ }
453
+
454
+ interface KubernetesLocation {
455
+ kind: 'kubernetes';
456
+ clusterRef: string; // Cluster.id in the referenced environment
457
+ namespace: string;
458
+ workload: {
459
+ kind: 'Deployment' | 'StatefulSet' | 'DaemonSet' | 'CronJob' | 'Pod';
460
+ name: string;
461
+ };
462
+ container?: string;
463
+ }
464
+
465
+ type DeploymentLocation =
466
+ | NativeLocation
467
+ | DockerLocation
468
+ | KubernetesLocation;
469
+
470
+ interface DeploymentBinding extends RecordMeta {
471
+ name?: string;
472
+ environmentId: string;
473
+ serviceId: string;
474
+ runtimeMode: RuntimeMode;
475
+ location: DeploymentLocation;
476
+ variables?: Record<string, string | number | boolean>;
477
+ ruleOverrides?: RuleOverride;
478
+ }
479
+ ```
480
+
481
+ Validation: `runtimeMode` must equal `location.kind`; the referenced host/cluster must exist in the referenced environment; the service must list `runtimeMode` in `deployModes`.
482
+
483
+ ### 5.1 YAML example
484
+
485
+ ```yaml
486
+ id: deploy-001
487
+ schemaVersion: 1
488
+ name: aicoservice-test-a
489
+ environmentId: test-a
490
+ serviceId: aicoservice
491
+ runtimeMode: kubernetes
492
+ location:
493
+ kind: kubernetes
494
+ clusterRef: k8s-01
495
+ namespace: dv-sop
496
+ workload:
497
+ kind: Deployment
498
+ name: aicoservice
499
+ container: aicoservice
500
+ variables:
501
+ replicaCount: 2
502
+ createdAt: 2026-11-01T03:00:00.000Z
503
+ updatedAt: 2026-11-01T03:00:00.000Z
504
+ ```
505
+
506
+ ---
507
+
508
+ ## 6. Rule Profile
509
+
510
+ A rule profile is a shared template bundle. Overrides live on the environment (§3) and the deployment (§5).
511
+
512
+ ```ts
513
+ interface LogSourceFile {
514
+ kind: 'file';
515
+ path: string; // template allowed
516
+ }
517
+
518
+ interface LogSourceContainer {
519
+ kind: 'container-stdout';
520
+ container: string; // template allowed
521
+ }
522
+
523
+ interface LogSourcePod {
524
+ kind: 'pod-logs';
525
+ namespace: string; // template allowed
526
+ workload: string; // template allowed
527
+ }
528
+
529
+ type LogSource = LogSourceFile | LogSourceContainer | LogSourcePod;
530
+
531
+ interface ClassLocation {
532
+ kind: 'build-output' | 'deployed-dir' | 'jar-entry';
533
+ path: string; // directory, or jar path for jar-entry
534
+ entryPrefix?: string; // jar-internal class prefix
535
+ }
536
+
537
+ interface RuleOverride {
538
+ variables?: Record<string, string | number | boolean>;
539
+ paths?: Record<string, string>;
540
+ logSources?: Partial<Record<RuntimeMode, LogSource>>;
541
+ classLocations?: Partial<Record<RuntimeMode, ClassLocation>>;
542
+ }
543
+
544
+ interface RuleProfile extends RecordMeta {
545
+ name?: string;
546
+ description?: string;
547
+ variables?: Record<string, string | number | boolean>;
548
+ paths?: Record<string, string>;
549
+ logSources?: Partial<Record<RuntimeMode, LogSource>>;
550
+ classLocations?: Partial<Record<RuntimeMode, ClassLocation>>;
551
+ }
552
+ ```
553
+
554
+ `classLocation.kind` distinguishes the three real sources: build output (`target/classes`), a deployed directory of classes, or an entry inside a JAR. V1 never collapses these into one path.
555
+
556
+ ### 6.1 YAML example
557
+
558
+ ```yaml
559
+ id: java-common
560
+ schemaVersion: 1
561
+ description: Common Java service paths and log sources
562
+ variables:
563
+ installRoot: /opt/company/services
564
+ paths:
565
+ serviceHome: ${installRoot}/${service.id}
566
+ jar: ${paths.serviceHome}/lib/${service.artifact.name}
567
+ logDir: ${paths.serviceHome}/logs
568
+ applicationLog: ${paths.logDir}/${service.id}.log
569
+ logSources:
570
+ native:
571
+ kind: file
572
+ path: ${paths.applicationLog}
573
+ docker:
574
+ kind: container-stdout
575
+ container: ${deployment.location.containerName}
576
+ kubernetes:
577
+ kind: pod-logs
578
+ namespace: ${deployment.location.namespace}
579
+ workload: ${deployment.location.workload.name}
580
+ classLocations:
581
+ native:
582
+ kind: deployed-dir
583
+ path: ${paths.serviceHome}/classes
584
+ kubernetes:
585
+ kind: jar-entry
586
+ path: ${paths.jar}
587
+ entryPrefix: com/company/aico
588
+ createdAt: 2026-11-01T03:00:00.000Z
589
+ updatedAt: 2026-11-01T03:00:00.000Z
590
+ ```
591
+
592
+ ---
593
+
594
+ ## 7. Operation Binding
595
+
596
+ ```ts
597
+ interface OperationMatch {
598
+ environmentId?: string;
599
+ serviceId?: string;
600
+ deploymentId?: string;
601
+ deploymentMode?: RuntimeMode;
602
+ serviceCategory?: string;
603
+ }
604
+
605
+ interface SkillReference {
606
+ name: string;
607
+ expectedSource?: string | null; // SkillSummary.source, e.g. project-dsh
608
+ expectedProvider?: string | null; // SkillSummary.provider
609
+ }
610
+
611
+ interface OperationPolicy {
612
+ requiresApproval: boolean;
613
+ allowProduction: boolean;
614
+ }
615
+
616
+ interface OperationBinding extends RecordMeta {
617
+ operation: string; // standard id or custom id
618
+ match: OperationMatch; // empty object means global
619
+ skill: SkillReference;
620
+ parameters?: Record<string, string>;
621
+ policy: OperationPolicy;
622
+ enabled: boolean;
623
+ }
624
+ ```
625
+
626
+ `match` must be one of five shapes; any other combination is `RECORD_INVALID`:
627
+
628
+ | Shape | Level |
629
+ |---|---|
630
+ | `{ deploymentId }` plus optional `environmentId`/`serviceId` | `deployment` |
631
+ | `{ environmentId, serviceId }` | `environment-service` |
632
+ | `{ serviceId }` | `service` |
633
+ | `{ serviceCategory, deploymentMode }` | `category-mode` |
634
+ | `{}` | `global` |
635
+
636
+ `environmentId` alone, `deploymentMode` alone, or `environmentId` + `deploymentMode` are
637
+ invalid: they would create a sixth level with no defined precedence.
638
+
639
+ ### 7.1 Standard operation ids
640
+
641
+ ```text
642
+ install, deploy, redeploy, replace, restart, upgrade, rollback, uninstall,
643
+ collect-logs, health-check, inspect, upload, download
644
+ ```
645
+
646
+ Any other id is treated as a custom operation and must appear explicitly in a binding.
647
+
648
+ ### 7.2 Write operations
649
+
650
+ `install`, `deploy`, `redeploy`, `replace`, `restart`, `upgrade`, `rollback`, `uninstall`, `upload` are write operations. For them, `policy.requiresApproval` defaults to `true` when the field is omitted, and a binding cannot implicitly grant production access.
651
+
652
+ ### 7.3 YAML example
653
+
654
+ ```yaml
655
+ id: binding-001
656
+ schemaVersion: 1
657
+ operation: redeploy
658
+ match:
659
+ environmentId: test-a
660
+ serviceId: aicoservice
661
+ skill:
662
+ name: redeploy-k8s-service
663
+ expectedSource: null
664
+ expectedProvider: null
665
+ parameters:
666
+ namespace: ${deployment.location.namespace}
667
+ workload: ${deployment.location.workload.name}
668
+ cluster: ${deployment.location.clusterRef}
669
+ policy:
670
+ requiresApproval: true
671
+ allowProduction: false
672
+ enabled: true
673
+ createdAt: 2026-11-01T03:00:00.000Z
674
+ updatedAt: 2026-11-01T03:00:00.000Z
675
+ ```
676
+
677
+ ---
678
+
679
+ ## 8. Resolved results
680
+
681
+ ### 8.1 Rule source and provenance
682
+
683
+ ```ts
684
+ type RuleLayer = 'global' | 'service' | 'environment' | 'deployment';
685
+
686
+ interface RuleSource {
687
+ layer: RuleLayer;
688
+ ruleProfileId?: string;
689
+ recordId?: string; // environmentId / deploymentId / serviceId
690
+ }
691
+
692
+ interface ProvenanceInput {
693
+ name: string; // e.g. paths.logDir
694
+ source: RuleSource;
695
+ }
696
+
697
+ interface ProvenanceEntry {
698
+ field: string; // e.g. paths.applicationLog, logSources.kubernetes.namespace
699
+ definedBy: RuleSource; // layer that supplied the winning template
700
+ inputs: ProvenanceInput[]; // referenced fields and their winning sources
701
+ shadowed: RuleSource[]; // lower-priority definitions that lost
702
+ value: string | number | boolean;
703
+ }
704
+
705
+ interface ResolvedField<T> {
706
+ value: T;
707
+ provenance: ProvenanceEntry;
708
+ }
709
+
710
+ interface ResolvedStructured<T> {
711
+ value: T;
712
+ provenance: ProvenanceEntry[]; // one entry per property
713
+ }
714
+ ```
715
+
716
+ `definedBy` answers "where did this template come from". `inputs` answers "what influenced the final value". Both are required: an override of `paths.logDir` changes `paths.applicationLog` even though the latter was defined at the service layer.
717
+
718
+ ### 8.2 Service Context
719
+
720
+ ```ts
721
+ interface CredentialStatus {
722
+ ref: string;
723
+ configured: boolean;
724
+ source?: string; // where the value lives, e.g. profile-vault
725
+ }
726
+
727
+ interface ServiceContext {
728
+ environmentId: string;
729
+ environmentName: string;
730
+ environmentCategory: EnvironmentCategory;
731
+ serviceId: string;
732
+ serviceName: string;
733
+ serviceCategory: string;
734
+ deploymentId: string;
735
+ deploymentName?: string;
736
+ runtimeMode: RuntimeMode;
737
+ source: {
738
+ gitUrl?: string;
739
+ defaultBranch?: string;
740
+ modulePath?: string;
741
+ };
742
+ artifact?: {
743
+ name: string;
744
+ path?: string;
745
+ };
746
+ location: DeploymentLocation;
747
+ variables: Record<string, ResolvedField<string | number | boolean>>;
748
+ paths: Record<string, ResolvedField<string>>;
749
+ logSource?: ResolvedStructured<LogSource>;
750
+ classLocation?: ResolvedStructured<ClassLocation>;
751
+ ruleProfiles: string[];
752
+ status: Status;
753
+ credentials: CredentialStatus[];
754
+ diagnostics: Diagnostic[];
755
+ }
756
+ ```
757
+
758
+ `status` starts as `configured-not-verified`. Without an online check, V1 must not claim `verified`.
759
+
760
+ ### 8.3 Operation Resolution
761
+
762
+ ```ts
763
+ interface Diagnostic {
764
+ code: string;
765
+ severity: Severity;
766
+ message: string;
767
+ file?: string; // registry-relative file name
768
+ path?: string; // field path, e.g. ruleProfileRefs[0]
769
+ details?: Record<string, unknown>;
770
+ }
771
+
772
+ interface ResolvedSkillReference {
773
+ name: string;
774
+ source?: string | null;
775
+ provider?: string | null;
776
+ available: boolean | 'unknown';
777
+ }
778
+
779
+ interface OperationResolution {
780
+ ok: boolean;
781
+ operation: string;
782
+ requested: {
783
+ environmentId: string;
784
+ serviceId?: string;
785
+ deploymentId?: string;
786
+ };
787
+ matchedBindingId?: string;
788
+ matchLevel?: MatchLevel;
789
+ skill?: ResolvedSkillReference;
790
+ parameters?: Record<string, string>;
791
+ policy?: OperationPolicy;
792
+ diagnostics: Diagnostic[];
793
+ }
794
+ ```
795
+
796
+ ---
797
+
798
+ ## 9. Session context
799
+
800
+ ```ts
801
+ interface SessionContext {
802
+ sessionId: string;
803
+ defaultEnvironmentId: string | null;
804
+ persisted: boolean;
805
+ updatedAt: string;
806
+ }
807
+ ```
808
+
809
+ Session context is per session and never mutates registry records. It is stored under `sessions/` in the plugin data directory, keyed by session id.
810
+
811
+ ---
812
+
813
+ ## 10. Operation record (P1 boundary)
814
+
815
+ ```ts
816
+ interface OperationRecord {
817
+ id: string;
818
+ sessionId: string;
819
+ environmentId: string;
820
+ serviceId?: string;
821
+ deploymentId?: string;
822
+ operation: string;
823
+ skillRef?: string;
824
+ startedAt: string;
825
+ finishedAt?: string;
826
+ outcome: 'success' | 'failure' | 'unknown';
827
+ evidenceRefs: string[]; // opaque, sanitized references
828
+ summary: string;
829
+ }
830
+ ```
831
+
832
+ V1 defines the type and a no-op bridge; it does not persist operation records itself.
833
+
834
+ ---
835
+
836
+ ## 11. Diagnostics catalogue
837
+
838
+ | Code | Severity | Meaning |
839
+ |---|---|---|
840
+ | `ENV_NOT_FOUND` | error | Environment id does not exist |
841
+ | `SERVICE_NOT_FOUND` | error | Service id does not exist |
842
+ | `DEPLOYMENT_NOT_FOUND` | error | Deployment id does not exist |
843
+ | `HOST_NOT_FOUND` | error | Deployment host reference, or a login record's host reference, is missing |
844
+ | `CLUSTER_NOT_FOUND` | error | Deployment cluster reference is missing in the environment |
845
+ | `DUPLICATE_HOST_ID` | warning | Two hosts in one environment share an id |
846
+ | `DUPLICATE_HOST_IP` | warning | Two hosts in one environment share an IP |
847
+ | `DUPLICATE_ACCOUNT_ID` | warning | Two accounts on one host share an id |
848
+ | `ACCOUNT_NOT_FOUND` | warning | A login record references an account that is not on its host |
849
+ | `RULE_PROFILE_NOT_FOUND` | error | `ruleProfileRefs` names a missing profile |
850
+ | `DEPLOYMENT_AMBIGUOUS` | error | No deployment id given and several bindings match |
851
+ | `DUPLICATE_ID` | warning | Two files declare the same id; the later file is skipped |
852
+ | `BINDING_NOT_FOUND` | error | No operation binding matches the context |
853
+ | `RECORD_INVALID` | error | Record failed schema validation |
854
+ | `RECORD_IN_USE` | error | Record is still referenced and cannot be deleted |
855
+ | `RECORD_LIMIT_EXCEEDED` | error | Record count would exceed `maxRecords` |
856
+ | `RECORD_TOO_LARGE` | error | Serialized record exceeds 1 MiB |
857
+ | `INVALID_ARGUMENT` | error | Tool or request input failed validation |
858
+ | `FORBIDDEN` | error | Request rejected by transport policy |
859
+ | `STORAGE_UNWRITABLE` | error | Data directory cannot be written |
860
+ | `SCHEMA_UNSUPPORTED` | warning | Unknown `schemaVersion`, record skipped |
861
+ | `UNKNOWN_FIELD` | warning | Read-only: a field outside the schema is present |
862
+ | `RULE_TEMPLATE_INVALID` | error | Malformed `${...}` or identifier |
863
+ | `RULE_UNDEFINED_VARIABLE` | error | Template references an unknown field |
864
+ | `RULE_CYCLE` | error | Template variable cycle |
865
+ | `RULE_PATH_TRAVERSAL` | error | Resolved path contains `..` |
866
+ | `RULE_TYPE_MISMATCH` | error | Template used where a non-string value is required |
867
+ | `OPERATION_UNKNOWN` | error | Operation is neither standard nor bound |
868
+ | `BINDING_CONFLICT` | error | Multiple bindings at the highest match level |
869
+ | `SKILL_UNAVAILABLE` | error | Skill not registered, or source/provider mismatch |
870
+ | `SKILL_REGISTRY_UNAVAILABLE` | warning | Skill registry cannot be queried; availability unknown |
871
+ | `POLICY_DENIED` | error | Production policy or approval policy rejects the operation |
872
+ | `PARAMETER_RESOLUTION_FAILED` | error | Parameter template failed to resolve |
873
+ | `CREDENTIAL_MISSING` | warning | `credentialRef` has no configured value |
874
+ | `MEMORY_BRIDGE_UNAVAILABLE` | warning | Memory plugin not installed |
875
+ | `SESSION_CONTEXT_UNAVAILABLE` | error | A session-scoped tool ran without an owning agent |
876
+ | `SESSION_CONTEXT_UNPERSISTED` | warning | Session state could not be written |
877
+ | `INTERNAL_ERROR` | error | Unexpected failure, message redacted |
878
+
879
+ ---
880
+
881
+ ## 12. Referential integrity
882
+
883
+ Validated on every write and by `env_validate`:
884
+
885
+ 1. `DeploymentBinding.environmentId` exists.
886
+ 2. `DeploymentBinding.serviceId` exists.
887
+ 3. `DeploymentBinding.location.hostRef` exists in that environment (native/docker).
888
+ 4. `DeploymentBinding.location.clusterRef` exists in that environment (kubernetes).
889
+ 5. `Service.ruleProfileRefs` all exist.
890
+ 6. `Environment.hosts[].roles` reference declared roles.
891
+ 7. `Environment.endpoints[].hostRef` / `clusterRef` exist in that environment.
892
+ 8. `Environment.hosts[].accounts[].id` are unique within their host.
893
+ 9. `Environment.loginRecords[].hostRef` exists in that environment.
894
+ 10. `Environment.loginRecords[].accountId` exists on the referenced host when both are set.
895
+ 11. `OperationBinding` match references (when present) exist.
896
+ 12. `OperationBinding.skill.name` is non-empty.
897
+
898
+ Duplicate host ids and duplicate host IPs are also reported as integrity errors.
899
+ Schema validation additionally rejects an environment whose `roles` is empty, and a host whose
900
+ `roles` is empty. A host role that is not declared in `Environment.roles` is reported as
901
+ `ROLE_NOT_FOUND` by integrity validation.
902
+
903
+ Broken references are reported, never silently repaired.
904
+
905
+ ---
906
+
907
+ ## 13. Identity and cardinality
908
+
909
+ ```text
910
+ Environment 1 ── N Host
911
+ Environment 1 ── N Cluster
912
+ Environment 1 ── N Role
913
+ Host N ── N Role (via Host.roles)
914
+ Host N ── N Environment (out of V1; hosts are embedded)
915
+ Service 1 ── N DeploymentBinding
916
+ Environment 1 ── N DeploymentBinding
917
+ Service 1 ── N RuleProfile (ordered ruleProfileRefs)
918
+ OperationBinding N ── predicate over Environment/Service/Deployment
919
+ ```
920
+
921
+ ---
922
+
923
+ ## 14. Out-of-scope model changes
924
+
925
+ The following are deliberately postponed:
926
+
927
+ - Extracting hosts into a top-level registry for cross-environment sharing.
928
+ - Storing discovered assets separately from declared assets.
929
+ - Vector or embedding-based retrieval for services.
930
+ - Per-user permissions inside one DSH profile.