@dsh-enhanced/lark-channel 0.1.48 → 0.1.49

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 (173) hide show
  1. package/README.md +19 -8
  2. package/docs/rsi-setup.md +123 -11
  3. package/docs/setup.md +27 -14
  4. package/docs/supervised-growth.md +2 -0
  5. package/lib/adapter.d.ts +6 -0
  6. package/lib/adapter.d.ts.map +1 -1
  7. package/lib/adapter.js +79 -8
  8. package/lib/adapter.js.map +1 -1
  9. package/lib/business-cli-install.d.ts +9 -0
  10. package/lib/business-cli-install.d.ts.map +1 -0
  11. package/lib/business-cli-install.js +367 -0
  12. package/lib/business-cli-install.js.map +1 -0
  13. package/lib/business-environment.d.ts +2 -0
  14. package/lib/business-environment.d.ts.map +1 -0
  15. package/lib/business-environment.js +16 -0
  16. package/lib/business-environment.js.map +1 -0
  17. package/lib/business-skill.d.ts +32 -0
  18. package/lib/business-skill.d.ts.map +1 -0
  19. package/lib/business-skill.js +202 -0
  20. package/lib/business-skill.js.map +1 -0
  21. package/lib/rsi-authority-config.d.ts +29 -0
  22. package/lib/rsi-authority-config.d.ts.map +1 -0
  23. package/lib/rsi-authority-config.js +233 -0
  24. package/lib/rsi-authority-config.js.map +1 -0
  25. package/lib/rsi-authority-resources.d.ts +36 -0
  26. package/lib/rsi-authority-resources.d.ts.map +1 -0
  27. package/lib/rsi-authority-resources.js +139 -0
  28. package/lib/rsi-authority-resources.js.map +1 -0
  29. package/lib/rsi-authority-runtime.d.ts +50 -0
  30. package/lib/rsi-authority-runtime.d.ts.map +1 -0
  31. package/lib/rsi-authority-runtime.js +393 -0
  32. package/lib/rsi-authority-runtime.js.map +1 -0
  33. package/lib/rsi-bootstrap-manifest.d.ts +39 -0
  34. package/lib/rsi-bootstrap-manifest.d.ts.map +1 -0
  35. package/lib/rsi-bootstrap-manifest.js +366 -0
  36. package/lib/rsi-bootstrap-manifest.js.map +1 -0
  37. package/lib/rsi-bootstrap.d.ts +30 -0
  38. package/lib/rsi-bootstrap.d.ts.map +1 -0
  39. package/lib/rsi-bootstrap.js +363 -0
  40. package/lib/rsi-bootstrap.js.map +1 -0
  41. package/lib/rsi-build.d.ts +45 -0
  42. package/lib/rsi-build.d.ts.map +1 -0
  43. package/lib/rsi-build.js +404 -0
  44. package/lib/rsi-build.js.map +1 -0
  45. package/lib/rsi-creation-build.d.ts +47 -0
  46. package/lib/rsi-creation-build.d.ts.map +1 -0
  47. package/lib/rsi-creation-build.js +333 -0
  48. package/lib/rsi-creation-build.js.map +1 -0
  49. package/lib/rsi-host-authorities.d.ts +18 -0
  50. package/lib/rsi-host-authorities.d.ts.map +1 -0
  51. package/lib/rsi-host-authorities.js +129 -0
  52. package/lib/rsi-host-authorities.js.map +1 -0
  53. package/lib/rsi-host-update.d.ts +113 -0
  54. package/lib/rsi-host-update.d.ts.map +1 -0
  55. package/lib/rsi-host-update.js +316 -0
  56. package/lib/rsi-host-update.js.map +1 -0
  57. package/lib/rsi-install-inputs.d.ts +43 -0
  58. package/lib/rsi-install-inputs.d.ts.map +1 -0
  59. package/lib/rsi-install-inputs.js +342 -0
  60. package/lib/rsi-install-inputs.js.map +1 -0
  61. package/lib/rsi-install.d.ts +54 -0
  62. package/lib/rsi-install.d.ts.map +1 -0
  63. package/lib/rsi-install.js +404 -0
  64. package/lib/rsi-install.js.map +1 -0
  65. package/lib/rsi-local-activation.d.ts +78 -0
  66. package/lib/rsi-local-activation.d.ts.map +1 -0
  67. package/lib/rsi-local-activation.js +485 -0
  68. package/lib/rsi-local-activation.js.map +1 -0
  69. package/lib/rsi-local-cohort.d.ts +70 -0
  70. package/lib/rsi-local-cohort.d.ts.map +1 -0
  71. package/lib/rsi-local-cohort.js +781 -0
  72. package/lib/rsi-local-cohort.js.map +1 -0
  73. package/lib/rsi-local-install.d.ts +40 -0
  74. package/lib/rsi-local-install.d.ts.map +1 -0
  75. package/lib/rsi-local-install.js +221 -0
  76. package/lib/rsi-local-install.js.map +1 -0
  77. package/lib/rsi-local-pair-install.d.ts +38 -0
  78. package/lib/rsi-local-pair-install.d.ts.map +1 -0
  79. package/lib/rsi-local-pair-install.js +200 -0
  80. package/lib/rsi-local-pair-install.js.map +1 -0
  81. package/lib/rsi-local-profile-update.d.ts +25 -0
  82. package/lib/rsi-local-profile-update.d.ts.map +1 -0
  83. package/lib/rsi-local-profile-update.js +226 -0
  84. package/lib/rsi-local-profile-update.js.map +1 -0
  85. package/lib/rsi-local-resources.d.ts +37 -0
  86. package/lib/rsi-local-resources.d.ts.map +1 -0
  87. package/lib/rsi-local-resources.js +184 -0
  88. package/lib/rsi-local-resources.js.map +1 -0
  89. package/lib/rsi-local-roots-extension.d.ts +11 -0
  90. package/lib/rsi-local-roots-extension.d.ts.map +1 -0
  91. package/lib/rsi-local-roots-extension.js +31 -0
  92. package/lib/rsi-local-roots-extension.js.map +1 -0
  93. package/lib/rsi-local-update-build.d.ts +18 -0
  94. package/lib/rsi-local-update-build.d.ts.map +1 -0
  95. package/lib/rsi-local-update-build.js +37 -0
  96. package/lib/rsi-local-update-build.js.map +1 -0
  97. package/lib/rsi-local-update.d.ts +39 -0
  98. package/lib/rsi-local-update.d.ts.map +1 -0
  99. package/lib/rsi-local-update.js +205 -0
  100. package/lib/rsi-local-update.js.map +1 -0
  101. package/lib/rsi-memory-learning.d.ts +22 -0
  102. package/lib/rsi-memory-learning.d.ts.map +1 -0
  103. package/lib/rsi-memory-learning.js +76 -0
  104. package/lib/rsi-memory-learning.js.map +1 -0
  105. package/lib/rsi-owner-profile.d.ts +37 -0
  106. package/lib/rsi-owner-profile.d.ts.map +1 -0
  107. package/lib/rsi-owner-profile.js +178 -0
  108. package/lib/rsi-owner-profile.js.map +1 -0
  109. package/lib/rsi-plugin-creation.d.ts +31 -0
  110. package/lib/rsi-plugin-creation.d.ts.map +1 -0
  111. package/lib/rsi-plugin-creation.js +179 -0
  112. package/lib/rsi-plugin-creation.js.map +1 -0
  113. package/lib/rsi-profile.d.ts +23 -0
  114. package/lib/rsi-profile.d.ts.map +1 -1
  115. package/lib/rsi-profile.js +154 -43
  116. package/lib/rsi-profile.js.map +1 -1
  117. package/lib/rsi-release-build.d.ts +41 -0
  118. package/lib/rsi-release-build.d.ts.map +1 -0
  119. package/lib/rsi-release-build.js +223 -0
  120. package/lib/rsi-release-build.js.map +1 -0
  121. package/lib/rsi-service-environment.d.ts +6 -0
  122. package/lib/rsi-service-environment.d.ts.map +1 -0
  123. package/lib/rsi-service-environment.js +113 -0
  124. package/lib/rsi-service-environment.js.map +1 -0
  125. package/lib/rsi-service-setup.d.ts +10 -0
  126. package/lib/rsi-service-setup.d.ts.map +1 -0
  127. package/lib/rsi-service-setup.js +34 -0
  128. package/lib/rsi-service-setup.js.map +1 -0
  129. package/lib/rsi-setup.d.ts +35 -2
  130. package/lib/rsi-setup.d.ts.map +1 -1
  131. package/lib/rsi-setup.js +459 -32
  132. package/lib/rsi-setup.js.map +1 -1
  133. package/lib/rsi-source-maintenance.d.ts +25 -0
  134. package/lib/rsi-source-maintenance.d.ts.map +1 -0
  135. package/lib/rsi-source-maintenance.js +348 -0
  136. package/lib/rsi-source-maintenance.js.map +1 -0
  137. package/lib/rsi-source-update.d.ts +26 -0
  138. package/lib/rsi-source-update.d.ts.map +1 -0
  139. package/lib/rsi-source-update.js +276 -0
  140. package/lib/rsi-source-update.js.map +1 -0
  141. package/lib/rsi-source.d.ts +34 -0
  142. package/lib/rsi-source.d.ts.map +1 -0
  143. package/lib/rsi-source.js +375 -0
  144. package/lib/rsi-source.js.map +1 -0
  145. package/lib/rsi-systemd-bootstrap.d.ts +46 -0
  146. package/lib/rsi-systemd-bootstrap.d.ts.map +1 -0
  147. package/lib/rsi-systemd-bootstrap.js +271 -0
  148. package/lib/rsi-systemd-bootstrap.js.map +1 -0
  149. package/lib/sdk.d.ts.map +1 -1
  150. package/lib/sdk.js +26 -1
  151. package/lib/sdk.js.map +1 -1
  152. package/lib/setup-business.d.ts +49 -0
  153. package/lib/setup-business.d.ts.map +1 -0
  154. package/lib/setup-business.js +335 -0
  155. package/lib/setup-business.js.map +1 -0
  156. package/lib/setup-profile.d.ts +1 -1
  157. package/lib/setup-profile.d.ts.map +1 -1
  158. package/lib/setup-profile.js +25 -2
  159. package/lib/setup-profile.js.map +1 -1
  160. package/lib/setup.d.ts +12 -1
  161. package/lib/setup.d.ts.map +1 -1
  162. package/lib/setup.js +138 -34
  163. package/lib/setup.js.map +1 -1
  164. package/lib/systemd.d.ts +10 -0
  165. package/lib/systemd.d.ts.map +1 -1
  166. package/lib/systemd.js +23 -6
  167. package/lib/systemd.js.map +1 -1
  168. package/lib/types.d.ts +7 -0
  169. package/lib/types.d.ts.map +1 -1
  170. package/lib/types.js.map +1 -1
  171. package/lib/version.d.ts +1 -1
  172. package/lib/version.js +1 -1
  173. package/package.json +32 -20
package/lib/rsi-setup.js CHANGED
@@ -1,31 +1,56 @@
1
- import { constants } from 'node:fs';
2
- import { chmod, lstat, open, realpath, rename, unlink } from 'node:fs/promises';
1
+ import { constants, realpathSync } from 'node:fs';
2
+ import { chmod, lstat, mkdir, open, realpath, rename, unlink } from 'node:fs/promises';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import { createHash, createPublicKey, randomUUID } from 'node:crypto';
5
5
  import { homedir } from 'node:os';
6
- import { dirname, isAbsolute, join, resolve } from 'node:path';
6
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import { createRequire } from 'node:module';
7
8
  import { isDeepStrictEqual } from 'node:util';
8
- import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
9
+ import { isAlias, isMap, isScalar, isSeq, parseDocument } from 'yaml';
9
10
  import { compileRsiProfiles } from './rsi-profile.js';
10
11
  import { withDshHomeLifecycleLock } from './setup.js';
11
- import { supervisedGrowthBindingQuery, supervisedGrowthDatabasePaths } from './supervised-growth-profile.js';
12
+ import { assertRsiSchedulerActivation, captureRsiAutomationInventories, rsiAutomationInventory, rsiOwnerBindingQuery, rsiDatabasePaths } from './rsi-owner-profile.js';
12
13
  import { installDshResidentService } from './resident.js';
14
+ import { prepareRsiSourceWorkspace } from './rsi-source.js';
15
+ import { prepareRsiBuildEnvironment, RsiBuildUnavailableError } from './rsi-build.js';
16
+ import { prepareRsiReleaseBuildEnvironment, RsiReleaseBuildUnavailableError } from './rsi-release-build.js';
17
+ import { prepareRsiAuthorityRuntime } from './rsi-authority-runtime.js';
18
+ import { prepareRsiAuthorityResources } from './rsi-authority-resources.js';
19
+ import { version } from './version.js';
20
+ import { renderDshSystemdService, systemdServicePaths } from './systemd.js';
21
+ import { rsiServiceEnvironmentPath } from './rsi-service-environment.js';
22
+ import { resolveRsiServiceEnvironments } from './rsi-service-setup.js';
23
+ import { validateRsiHostAuthorities } from './rsi-host-authorities.js';
24
+ import { validateRsiMemoryLearningSetup } from './rsi-memory-learning.js';
25
+ import { resolveInstalledRsiDsh } from './rsi-install-inputs.js';
13
26
  const MAX_BYTES = 2_097_152;
14
27
  const profilePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
15
28
  const digest = (value) => createHash('sha256').update(value).digest('hex');
16
29
  function fail(message) { throw new Error(`rsi setup: ${message}`); }
17
30
  // Compare semantic YAML nodes, including executable !!js tags, without depending
18
31
  // on comments or formatting retained by the profile patch serializer.
19
- function yamlValue(node) {
32
+ function yamlValue(node, document, ancestors = new Set()) {
33
+ if (isAlias(node)) {
34
+ const resolved = node.resolve(document);
35
+ if (!resolved || ancestors.has(resolved))
36
+ fail('cyclic or unresolved YAML alias');
37
+ return yamlValue(resolved, document, new Set(ancestors).add(resolved));
38
+ }
20
39
  if (isScalar(node))
21
40
  return { tag: node.tag, value: node.value };
22
41
  if (isSeq(node))
23
- return { tag: node.tag, items: node.items.map(yamlValue) };
42
+ return { tag: node.tag, items: node.items.map(item => yamlValue(item, document, ancestors)) };
24
43
  if (isMap(node))
25
- return { tag: node.tag, entries: node.items.map(item => [yamlValue(item.key), yamlValue(item.value)])
44
+ return { tag: node.tag, entries: node.items.map(item => [yamlValue(item.key, document, ancestors), yamlValue(item.value, document, ancestors)])
26
45
  .sort((left, right) => JSON.stringify(left[0]).localeCompare(JSON.stringify(right[0]))) };
27
46
  return node;
28
47
  }
48
+ function patchValue(source, label) {
49
+ const document = parseDocument(source, { uniqueKeys: true });
50
+ if (document.errors.length || !isSeq(document.contents))
51
+ fail(`invalid ${label} patch`);
52
+ return yamlValue(document.contents, document);
53
+ }
29
54
  export function assertRsiEffectivePatch(patch, effective) {
30
55
  const expected = parseDocument(patch), actual = parseDocument(effective);
31
56
  if (expected.errors.length || actual.errors.length || !isSeq(expected.contents) || !isSeq(actual.contents))
@@ -36,7 +61,7 @@ export function assertRsiEffectivePatch(patch, effective) {
36
61
  const matches = actual.contents.items.filter(item => isMap(item) && item.get('id') === row.get('id'));
37
62
  const resolved = matches[0];
38
63
  if (matches.length !== 1 || !isMap(resolved) || resolved.get('disabled') === true && row.get('disabled') !== true
39
- || !isDeepStrictEqual(yamlValue(row.get('config', true)), yamlValue(resolved.get('config', true))))
64
+ || !isDeepStrictEqual(yamlValue(row.get('config', true), expected), yamlValue(resolved.get('config', true), actual)))
40
65
  fail(`effective profile overrides compiled config for ${String(row.get('id'))}`);
41
66
  }
42
67
  }
@@ -46,7 +71,7 @@ export function parseRsiSetupArgs(argv) {
46
71
  const seen = new Set();
47
72
  for (let i = 0; i < argv.length; i++) {
48
73
  const key = argv[i];
49
- if (seen.has(key))
74
+ if (key !== '--bundle' && seen.has(key))
50
75
  fail(`duplicate option ${key}`);
51
76
  seen.add(key);
52
77
  if (key === '--help' || key === '-h')
@@ -59,12 +84,42 @@ export function parseRsiSetupArgs(argv) {
59
84
  result.start = true;
60
85
  else if (key === '--confirm-hosts-stopped')
61
86
  result.confirmStopped = true;
62
- else if (key === '--manifest' || key === '--dsh-home') {
87
+ else if (key === '--prepare-source')
88
+ result.prepareSource = true;
89
+ else if (key === '--prepare-build')
90
+ result.prepareBuild = true;
91
+ else if (key === '--prepare-authorities')
92
+ result.prepareAuthorities = true;
93
+ else if (key === '--install-owner')
94
+ result.installOwner = true;
95
+ else if (key === '--ack-existing-automations')
96
+ result.ackExistingAutomations = true;
97
+ else if (key === '--install-local-cohort')
98
+ result.installLocalCohort = true;
99
+ else if (key === '--prepare-local-update')
100
+ result.prepareLocalUpdate = true;
101
+ else if (key === '--add-memory-learning')
102
+ result.addMemoryLearning = true;
103
+ else if (key === '--bundle') {
104
+ const value = argv[++i];
105
+ if (!value || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(value))
106
+ fail('--bundle requires a plugin slug');
107
+ (result.bundles ??= []).push(value);
108
+ }
109
+ else if (key === '--optional-build')
110
+ result.optionalBuild = true;
111
+ else if (key === '--manifest' || key === '--dsh-home' || key === '--profile' || key === '--source-repository' || key === '--docker-path') {
63
112
  const value = argv[++i];
64
113
  if (!value || value.startsWith('--'))
65
114
  fail(`${key} needs a value`);
66
115
  if (key === '--manifest')
67
116
  result.manifestPath = resolve(value);
117
+ else if (key === '--profile')
118
+ result.profile = value;
119
+ else if (key === '--source-repository')
120
+ result.sourceRepository = value;
121
+ else if (key === '--docker-path')
122
+ result.dockerPath = value;
68
123
  else
69
124
  result.dshHome = value;
70
125
  }
@@ -73,6 +128,63 @@ export function parseRsiSetupArgs(argv) {
73
128
  }
74
129
  if (result.help)
75
130
  return result;
131
+ if (result.addMemoryLearning && !result.prepareLocalUpdate)
132
+ fail('--add-memory-learning requires --prepare-local-update');
133
+ if (result.ackExistingAutomations && (!result.installOwner && !result.apply || result.rollback
134
+ || result.prepareLocalUpdate || result.installLocalCohort || result.prepareSource || result.prepareBuild || result.prepareAuthorities))
135
+ fail('--ack-existing-automations requires --install-owner or --apply');
136
+ if (result.prepareLocalUpdate) {
137
+ if (result.installLocalCohort || result.installOwner || result.prepareSource || result.prepareBuild
138
+ || result.prepareAuthorities || result.manifestPath || result.apply || result.rollback || result.start
139
+ || result.confirmStopped || result.optionalBuild || result.dockerPath || result.bundles)
140
+ fail('local update preparation cannot be combined with other operations');
141
+ if (!isAbsolute(result.dshHome) || !result.profile || !profilePattern.test(result.profile))
142
+ fail('local update preparation requires --profile and absolute DSH_HOME');
143
+ if (result.sourceRepository && !isAbsolute(result.sourceRepository))
144
+ fail('--source-repository must be absolute');
145
+ return result;
146
+ }
147
+ if (result.bundles && !result.installLocalCohort)
148
+ fail('--bundle requires --install-local-cohort');
149
+ if (result.installLocalCohort) {
150
+ if (result.installOwner || result.prepareSource || result.prepareBuild || result.prepareAuthorities || result.manifestPath
151
+ || result.apply || result.rollback || result.start || result.confirmStopped || result.optionalBuild || result.dockerPath)
152
+ fail('local cohort installation cannot be combined with other operations');
153
+ if (!isAbsolute(result.dshHome) || !result.profile || !profilePattern.test(result.profile)
154
+ || !result.sourceRepository || !isAbsolute(result.sourceRepository) || !result.bundles?.length
155
+ || new Set(result.bundles).size !== result.bundles.length)
156
+ fail('local cohort installation requires a profile, absolute source repository and unique bundles');
157
+ return result;
158
+ }
159
+ if ((result.optionalBuild || result.dockerPath) && !result.prepareBuild)
160
+ fail('--optional-build and --docker-path require --prepare-build');
161
+ if (result.installOwner) {
162
+ if (result.prepareSource || result.prepareBuild || result.prepareAuthorities || result.manifestPath || result.apply
163
+ || result.rollback || result.start || result.confirmStopped || result.optionalBuild || result.dockerPath)
164
+ fail('owner installation cannot be combined with other operations');
165
+ if (!isAbsolute(result.dshHome) || !result.profile || !profilePattern.test(result.profile))
166
+ fail('owner installation requires --profile and absolute DSH_HOME');
167
+ if (result.sourceRepository && !isAbsolute(result.sourceRepository))
168
+ fail('--source-repository must be absolute');
169
+ return result;
170
+ }
171
+ if (result.prepareSource || result.prepareBuild || result.prepareAuthorities) {
172
+ if ([result.prepareSource, result.prepareBuild, result.prepareAuthorities].filter(Boolean).length > 1)
173
+ fail('resource preparation modes cannot be combined');
174
+ if (result.manifestPath || result.apply || result.rollback || result.start || result.confirmStopped)
175
+ fail('resource preparation cannot be combined with profile configuration operations');
176
+ if (!isAbsolute(result.dshHome) || !result.profile || !profilePattern.test(result.profile))
177
+ fail('resource preparation requires --profile and absolute DSH_HOME');
178
+ if (result.prepareAuthorities && result.sourceRepository)
179
+ fail('--source-repository requires --prepare-source or --prepare-build');
180
+ if (result.sourceRepository && !isAbsolute(result.sourceRepository))
181
+ fail('--source-repository must be absolute');
182
+ if (result.dockerPath && !isAbsolute(result.dockerPath))
183
+ fail('--docker-path must be absolute');
184
+ return result;
185
+ }
186
+ if (result.profile || result.sourceRepository)
187
+ fail('--profile and --source-repository require --prepare-source, --prepare-build or --prepare-authorities');
76
188
  if (!result.manifestPath || !isAbsolute(result.dshHome))
77
189
  fail('--manifest and absolute DSH_HOME are required');
78
190
  if (result.rollback && (result.apply || result.start))
@@ -151,29 +263,132 @@ async function readJournal(path, expected) {
151
263
  throw error;
152
264
  }
153
265
  const value = JSON.parse(content);
154
- if (value.schemaVersion !== 1 || !['prepared', 'applied'].includes(value.stage) || !/^[a-f0-9]{64}$/u.test(value.manifestDigest)
266
+ if (![1, 2].includes(value.schemaVersion) || !['prepared', 'applied'].includes(value.stage) || !/^[a-f0-9]{64}$/u.test(value.manifestDigest)
155
267
  || !Array.isArray(value.entries) || value.entries.length !== 2
156
268
  || value.entries.some((entry, index) => entry.profile !== expected[index] || typeof entry.before !== 'string' || typeof entry.after !== 'string'))
157
269
  fail('journal does not match this profile pair');
270
+ if (value.schemaVersion === 2) {
271
+ for (const entries of [value.environments, value.units]) {
272
+ if (!Array.isArray(entries) || entries.length !== 2 || entries.some((entry, index) => entry.profile !== expected[index]
273
+ || (entry.before !== null && typeof entry.before !== 'string') || (entry.after !== null && typeof entry.after !== 'string')))
274
+ fail('invalid service environment journal');
275
+ }
276
+ }
277
+ else if (value.environments || value.units)
278
+ fail('legacy journal cannot contain service resources');
158
279
  return value;
159
280
  }
160
- async function rollback(home, journalPath, journal) {
161
- // Check both files before restoring either. Retry also accepts a half-restored pair.
281
+ async function readOptionalFile(path, privateFile = true) {
282
+ try {
283
+ return await readOwnedFile(path, privateFile);
284
+ }
285
+ catch (error) {
286
+ if (error.code !== 'ENOENT')
287
+ throw error;
288
+ // realpath also reports ENOENT for dangling links; those are not absent files.
289
+ try {
290
+ await lstat(path);
291
+ }
292
+ catch (linkedError) {
293
+ if (linkedError.code === 'ENOENT')
294
+ return null;
295
+ throw linkedError;
296
+ }
297
+ fail(`unsafe file: ${path}`);
298
+ }
299
+ }
300
+ async function writeOptionalFile(path, value) {
301
+ if (value !== null) {
302
+ await atomicWrite(path, value);
303
+ return;
304
+ }
305
+ try {
306
+ await unlink(path);
307
+ await syncDirectory(dirname(path));
308
+ }
309
+ catch (error) {
310
+ if (error.code !== 'ENOENT')
311
+ throw error;
312
+ }
313
+ }
314
+ function auxiliary(home, journal, ports) {
315
+ return [
316
+ ...(journal.environments ?? []).map(entry => ({ ...entry, path: rsiServiceEnvironmentPath(home, entry.profile), privateFile: true })),
317
+ ...(journal.units ?? []).map(entry => ({ ...entry, path: ports.serviceUnitPath(entry.profile, home), privateFile: false })),
318
+ ];
319
+ }
320
+ async function rollback(home, journalPath, journal, ports) {
321
+ // Check every resource before restoring any. Retry accepts a half-restored transaction.
162
322
  for (const entry of journal.entries) {
163
323
  const current = await readOwnedFile(patchPath(home, entry.profile), false);
164
324
  if (current !== entry.before && current !== entry.after)
165
325
  fail('profile changed outside setup; retain journal for reconciliation');
166
326
  }
327
+ const resources = auxiliary(home, journal, ports);
328
+ for (const entry of resources) {
329
+ const current = await readOptionalFile(entry.path, entry.privateFile);
330
+ if (current !== entry.before && current !== entry.after)
331
+ fail('service resource changed outside setup; retain journal for reconciliation');
332
+ }
167
333
  for (const entry of journal.entries)
168
334
  await atomicWrite(patchPath(home, entry.profile), entry.before);
335
+ for (const entry of resources)
336
+ await writeOptionalFile(entry.path, entry.before);
337
+ if (journal.units)
338
+ ports.reloadServices();
169
339
  await unlink(journalPath);
170
340
  await syncDirectory(home);
171
341
  }
172
- const defaultPorts = {
342
+ export async function readRsiInstalledBase(profile, home, environment = process.env) {
343
+ if (!profilePattern.test(profile))
344
+ fail('invalid profile for installed base');
345
+ await safeDirectory(join(home, 'profiles', profile));
346
+ const dsh = await resolveInstalledRsiDsh(environment);
347
+ const packageName = '@deepseek-ai/dsh-base';
348
+ const installAnchor = join(dsh.root, 'package.json');
349
+ let installRoot = dsh.root;
350
+ for (let directory = dirname(dsh.root); directory !== dirname(directory); directory = dirname(directory)) {
351
+ if (basename(directory) === 'node_modules')
352
+ installRoot = directory;
353
+ }
354
+ for (const searchPath of createRequire(installAnchor).resolve.paths(packageName) ?? []) {
355
+ const packageRoot = join(searchPath, packageName);
356
+ const manifestPath = join(packageRoot, 'package.json');
357
+ let manifestBytes;
358
+ try {
359
+ const physical = await realpath(manifestPath);
360
+ const child = relative(installRoot, physical);
361
+ if (!child || child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child))
362
+ fail('installed DSH base escapes Host installation');
363
+ manifestBytes = await readOwnedFile(physical, false, true);
364
+ }
365
+ catch (error) {
366
+ if (error.code === 'ENOENT')
367
+ continue;
368
+ throw error;
369
+ }
370
+ let manifest;
371
+ try {
372
+ manifest = JSON.parse(manifestBytes);
373
+ }
374
+ catch {
375
+ fail('installed DSH base manifest is invalid');
376
+ }
377
+ const entry = manifest;
378
+ if (entry?.name !== packageName || entry.version !== dsh.version || entry.dsh?.bundle?.patch !== './cordis.patch.yml') {
379
+ fail('installed DSH base identity or patch declaration differs from Host');
380
+ }
381
+ const patchPath = await realpath(join(packageRoot, 'cordis.patch.yml'));
382
+ const child = relative(installRoot, patchPath);
383
+ if (!child || child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child))
384
+ fail('installed DSH base patch escapes Host installation');
385
+ return readOwnedFile(patchPath, false, true);
386
+ }
387
+ fail('installed DSH base is absent from Host installation');
388
+ }
389
+ export const rsiSetupPorts = {
173
390
  async base(profile, home) {
174
- // Resolve the installed package symlink, then pin a bounded non-writable file.
175
- const path = await realpath(join(home, 'profiles', profile, 'node_modules', '@deepseek-ai', 'dsh-base', 'cordis.patch.yml'));
176
- return readOwnedFile(path, false, true);
391
+ return readRsiInstalledBase(profile, home);
177
392
  },
178
393
  dump(profile, home) {
179
394
  const result = spawnSync('dsh', ['--profile', profile, '--dump-config'], { encoding: 'utf8', timeout: 60_000,
@@ -184,11 +399,21 @@ const defaultPorts = {
184
399
  },
185
400
  async snapshot(effective, home) {
186
401
  const { inspectActiveLarkOwnerBindingsLocally } = await import('@dsh-enhanced/assistant-delivery');
187
- return inspectActiveLarkOwnerBindingsLocally({ databasePath: supervisedGrowthDatabasePaths(effective, home).deliveryDatabasePath,
188
- ...supervisedGrowthBindingQuery(effective, home) });
402
+ return inspectActiveLarkOwnerBindingsLocally({ databasePath: rsiDatabasePaths(effective, home).deliveryDatabasePath,
403
+ ...rsiOwnerBindingQuery(effective, home) });
189
404
  },
405
+ async automationInventory(effective, home) { return rsiAutomationInventory(effective, home); },
190
406
  compile: compileRsiProfiles,
191
407
  validateAuthorities: validateRsiAuthorities,
408
+ resolveEnvironments: resolveRsiServiceEnvironments,
409
+ serviceUnitPath: (profile, home) => systemdServicePaths({ dshHome: home, profile, home: realpathSync(homedir()) }).unitPath,
410
+ async renderServiceUnit(profile, home, environment) { return (await renderDshSystemdService({ dshHome: home, profile }, { environment })).source; },
411
+ reloadServices() {
412
+ const result = spawnSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf8', timeout: 10_000, maxBuffer: 65_536 });
413
+ if (result.status !== 0 || result.error)
414
+ fail('cannot reload service definitions');
415
+ },
416
+ validateServiceUnits: validateRsiServiceUnitEnvironment,
192
417
  assertStopped(profile) {
193
418
  const result = spawnSync('systemctl', ['--user', 'show', `dsh-profile-${profile}.service`, '--property=ActiveState', '--value'], { encoding: 'utf8', timeout: 10_000, maxBuffer: 65_536 });
194
419
  if (result.status !== 0 || result.error || !['inactive', 'failed'].includes(result.stdout.trim()))
@@ -196,8 +421,20 @@ const defaultPorts = {
196
421
  },
197
422
  async start(profile, home) { await installDshResidentService({ dshHome: home, profile }); },
198
423
  };
424
+ /** Read back the loaded unit after daemon-reload, before starting either Host. */
425
+ export async function validateRsiServiceUnitEnvironment(manifest, environment) {
426
+ const path = environment.DSH_SYSTEMD_HOST_ATTESTOR_CONFIG;
427
+ if (!path)
428
+ return;
429
+ const wrapper = JSON.parse(await readOwnedFile(path));
430
+ if (wrapper.schemaVersion !== 4)
431
+ return;
432
+ const result = spawnSync('systemctl', ['--user', 'show', `dsh-profile-${manifest.targetProfile}.service`, '--property=Environment', '--value'], { encoding: 'utf8', timeout: 10_000, maxBuffer: 65_536 });
433
+ if (result.status !== 0 || result.error || result.stdout.trimEnd() !== wrapper.template?.unitProperties?.Environment)
434
+ fail('effective service environment differs from the Host installation grant');
435
+ }
199
436
  /** Validate imported finite grants, without calling any signing/authorization method. */
200
- export async function validateRsiAuthorities(manifest, binding) {
437
+ export async function validateRsiAuthorities(manifest, binding, environment) {
201
438
  const cp = await import('@dsh-enhanced/plugin-control-plane');
202
439
  const config = manifest.controlPlane;
203
440
  if (!config.sourceJobs || !config.sourceApprovals || !config.sourceReleases || !config.sourceAdoptions || !config.taskObservations)
@@ -213,6 +450,11 @@ export async function validateRsiAuthorities(manifest, binding) {
213
450
  const expectedOwner = manifest.sourceReviews.owner;
214
451
  if (expectedOwner.principalRecordId !== binding.owner.id || expectedOwner.principalVersion !== binding.owner.version)
215
452
  fail('grant owner has changed');
453
+ // compileRsiProfiles first derives and checks all seven owner fields against
454
+ // effective Delivery. This secondary check binds grants to that owner; it
455
+ // cannot independently resolve a route from the principal receipt alone.
456
+ if (manifest.memoryLearning !== undefined)
457
+ validateRsiMemoryLearningSetup(manifest.memoryLearning, expectedOwner);
216
458
  const trust = await cp.loadTrustConfig(config.trustPath);
217
459
  if (trust.schemaVersion !== 4 || !trust.hostAttestor || !trust.releaseAdapters
218
460
  || ['pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'].some(phase => !Object.hasOwn(trust.releaseAdapters, phase)))
@@ -229,6 +471,8 @@ export async function validateRsiAuthorities(manifest, binding) {
229
471
  || adoptions.grant.target.profile !== manifest.targetProfile || adoptions.grant.target.profilePath !== config.runtimeObserver?.profilePath
230
472
  || adoptions.grant.target.profilePath !== join(trust.dshHome, 'profiles', manifest.targetProfile)
231
473
  || !isDeepStrictEqual(adoptions.grant.executor, { id, version, path, sha256 })
474
+ || !isDeepStrictEqual(adoptions.grant.liveQualification, config.sourceAdoptions.liveQualification)
475
+ || !isDeepStrictEqual(adoptions.grant.hostDeploymentInputs, config.sourceAdoptions.hostDeploymentInputs)
232
476
  || adoptions.grant.catalogPath !== config.catalogPath || !isDeepStrictEqual(adoptions.grant.handoff, config.sourceAdoptions.handoff)
233
477
  || approvals.grant.repository !== config.sourceJobs.repository || releases.grant.repository !== config.sourceJobs.repository
234
478
  || approvals.grant.worktreeRoot !== join(config.statePath, 'source-worktrees') || releases.grant.worktreeRoot !== approvals.grant.worktreeRoot
@@ -257,11 +501,36 @@ export async function validateRsiAuthorities(manifest, binding) {
257
501
  if (!configured.equals(registered))
258
502
  fail('authority key does not match registered trust');
259
503
  }
504
+ if (config.liveQualification) {
505
+ const qualification = JSON.parse(await readOwnedFile(config.liveQualification.authority.configPath));
506
+ cp.validateLiveQualificationAuthorityConfig(qualification);
507
+ if (qualification.controlDatabasePath !== ledgerPath || !isDeepStrictEqual(qualification.grant.owner, expectedOwner)
508
+ || !isDeepStrictEqual(qualification.grant.terms, config.sourceAdoptions.liveQualification)
509
+ || qualification.grant.installationId !== trust.installationId || !isDeepStrictEqual(qualification.grant.ledger, trust.ledger)
510
+ || qualification.grant.profilePath !== config.liveQualification.profilePath
511
+ || !isDeepStrictEqual([...qualification.grant.packages].sort(), selected.map(plugin => `@dsh-enhanced/${plugin}`))
512
+ || qualification.grant.expiresAt <= Date.now())
513
+ fail('live qualification authority terms mismatch');
514
+ const trusted = cp.resolveTrustKey(trust, 'host-attestation', qualification.authority, qualification.keyId);
515
+ const configured = createPublicKey(await readOwnedFile(qualification.keyPath)).export({ type: 'spki', format: 'der' });
516
+ if (!configured.equals(createPublicKey(trusted.publicKeyPem).export({ type: 'spki', format: 'der' })))
517
+ fail('live qualification authority key mismatch');
518
+ }
260
519
  if ([approvals.grant.expiresAt, releases.grant.expiresAt, adoptions.grant.expiresAt, observations.grant.policy.expiresAt,
261
520
  manifest.sourceReviews.expiresAt].some(value => value <= Date.now()))
262
521
  fail('finite authority has expired');
522
+ await validateRsiHostAuthorities({ trust, adoption: adoptions, owner: expectedOwner, ledgerPath, trustPath: config.trustPath,
523
+ targetProfile: manifest.targetProfile,
524
+ liveQualification: config.sourceAdoptions.liveQualification, hostDeploymentInputs: config.sourceAdoptions.hostDeploymentInputs,
525
+ handoff: config.sourceAdoptions.handoff, ...(environment === undefined ? {} : { environment }), readPrivate: path => readOwnedFile(path) });
526
+ }
527
+ export async function configureRsiSetup(args, ports = rsiSetupPorts) {
528
+ return withDshHomeLifecycleLock(args.dshHome, () => configureRsiSetupLocked(args, ports));
263
529
  }
264
- export async function configureRsiSetup(args, ports = defaultPorts) {
530
+ /** Internal installer entry: caller already owns the DSH_HOME lifecycle lock. */
531
+ export async function configureRsiSetupLocked(args, ports = rsiSetupPorts) {
532
+ if (args.prepareLocalUpdate || args.installLocalCohort || args.bundles || args.installOwner || args.prepareSource || args.prepareBuild || args.prepareAuthorities || args.optionalBuild || args.dockerPath || args.profile || args.sourceRepository)
533
+ fail('resource preparation is a separate setup operation');
265
534
  if (args.rollback && (args.apply || args.start) || args.start && !args.apply)
266
535
  fail('incompatible setup operations');
267
536
  const home = args.dshHome;
@@ -269,7 +538,7 @@ export async function configureRsiSetup(args, ports = defaultPorts) {
269
538
  const manifestBytes = await readOwnedFile(args.manifestPath);
270
539
  const manifest = JSON.parse(manifestBytes);
271
540
  const pair = profiles(manifest);
272
- return withDshHomeLifecycleLock(home, async () => {
541
+ return (async () => {
273
542
  for (const profile of pair)
274
543
  await safeDirectory(join(home, 'profiles', profile));
275
544
  const journalPath = join(home, '.rsi-setup-journal.json');
@@ -283,7 +552,7 @@ export async function configureRsiSetup(args, ports = defaultPorts) {
283
552
  if (args.rollback) {
284
553
  if (!previous)
285
554
  fail('no setup journal to roll back');
286
- await rollback(home, journalPath, previous);
555
+ await rollback(home, journalPath, previous, ports);
287
556
  return { mode: 'rolled-back', profiles: pair };
288
557
  }
289
558
  if (previous?.stage === 'prepared')
@@ -298,50 +567,90 @@ export async function configureRsiSetup(args, ports = defaultPorts) {
298
567
  const input = { manifest, dshHome: home, targetPatch: original[0], coordinatorPatch: original[1],
299
568
  targetEffective: effective[0], coordinatorEffective: effective[1], coordinatorBase, owner };
300
569
  const compiled = await ports.compile(input);
301
- await ports.validateAuthorities(manifest, owner);
570
+ const inventories = await captureRsiAutomationInventories(ports.automationInventory, effective[0], effective[1], home, pair[1]);
571
+ if (args.expectedAutomationInventories && !isDeepStrictEqual(inventories, args.expectedAutomationInventories))
572
+ fail('Automation inventory changed since installer preflight');
573
+ assertRsiSchedulerActivation(inventories, args.ackExistingAutomations === true);
574
+ const originalEnvironments = await Promise.all(pair.map(profile => readOptionalFile(rsiServiceEnvironmentPath(home, profile))));
575
+ const environments = await ports.resolveEnvironments(manifest, home);
576
+ const originalUnits = environments ? await Promise.all(pair.map(profile => readOptionalFile(ports.serviceUnitPath(profile, home), false))) : undefined;
577
+ const nextUnits = environments ? await Promise.all(pair.map((profile, index) => ports.renderServiceUnit(profile, home, index === 0 ? environments.target : environments.coordinator))) : undefined;
578
+ await ports.validateAuthorities(manifest, owner, environments?.target);
302
579
  if (!args.apply)
303
580
  return { mode: 'checked', profiles: pair };
304
581
  if (await readOwnedFile(args.manifestPath) !== manifestBytes || await ports.base(pair[1], home) !== coordinatorBase
305
582
  || !isDeepStrictEqual(snapshot, await ports.snapshot(effective[0], home)))
306
583
  fail('manifest or owner snapshot changed before write');
584
+ if (!isDeepStrictEqual(inventories, await captureRsiAutomationInventories(ports.automationInventory, effective[0], effective[1], home, pair[1])))
585
+ fail('Automation inventory changed before write');
307
586
  for (let i = 0; i < pair.length; i++) {
308
587
  ports.assertStopped(pair[i]);
309
588
  if (await readOwnedFile(patchPath(home, pair[i]), false) !== original[i])
310
589
  fail('profile changed before write');
590
+ if (environments && (await readOptionalFile(rsiServiceEnvironmentPath(home, pair[i])) !== originalEnvironments[i]
591
+ || await readOptionalFile(ports.serviceUnitPath(pair[i], home), false) !== originalUnits[i]))
592
+ fail('service resource changed before write');
311
593
  }
312
594
  const next = [compiled.targetPatch, compiled.coordinatorPatch];
313
595
  if (previous && previous.entries.some((entry, i) => entry.after !== original[i]))
314
596
  fail('profile changed since previous setup; reconcile its journal first');
315
- const journal = { schemaVersion: 1, manifestDigest: digest(manifestBytes), stage: 'prepared',
316
- entries: pair.map((profile, i) => ({ profile, before: original[i], after: next[i] })) };
597
+ const journal = { schemaVersion: environments ? 2 : 1, manifestDigest: digest(manifestBytes), stage: 'prepared',
598
+ entries: pair.map((profile, i) => ({ profile, before: original[i], after: next[i] })),
599
+ ...(environments ? {
600
+ environments: pair.map((profile, i) => ({ profile, before: originalEnvironments[i], after: JSON.stringify({ schemaVersion: 1,
601
+ dshHome: home, profile, environment: i === 0 ? environments.target : environments.coordinator }) + '\n' })),
602
+ units: pair.map((profile, i) => ({ profile, before: originalUnits[i], after: nextUnits[i] })),
603
+ } : {}) };
604
+ if (previous)
605
+ for (const entry of auxiliary(home, previous, ports)) {
606
+ if (await readOptionalFile(entry.path, entry.privateFile) !== entry.after)
607
+ fail('service resource changed since previous setup; reconcile its journal first');
608
+ }
317
609
  if (Buffer.byteLength(JSON.stringify(journal)) > MAX_BYTES)
318
610
  fail('combined profile journal exceeds setup size limit');
319
- const unchanged = next.every((value, i) => value === original[i]);
611
+ const unchanged = next.every((value, i) => value === original[i]) && auxiliary(home, journal, ports).every(entry => entry.before === entry.after);
320
612
  if (unchanged)
321
613
  for (let i = 0; i < pair.length; i++)
322
614
  assertRsiEffectivePatch(next[i], effective[i]);
323
615
  if (!unchanged) {
616
+ for (const entry of auxiliary(home, journal, ports)) {
617
+ await mkdir(dirname(entry.path), { recursive: true, mode: 0o700 });
618
+ await safeDirectory(dirname(entry.path));
619
+ if (entry.privateFile && ((await lstat(dirname(entry.path))).mode & 0o077) !== 0)
620
+ fail('service environment directory must be private');
621
+ }
324
622
  await atomicWrite(journalPath, JSON.stringify(journal));
325
623
  try {
326
624
  for (const entry of journal.entries)
327
625
  await atomicWrite(patchPath(home, entry.profile), entry.after);
626
+ for (const entry of auxiliary(home, journal, ports))
627
+ await writeOptionalFile(entry.path, entry.after);
328
628
  const final = pair.map(profile => ports.dump(profile, home));
329
629
  for (let i = 0; i < pair.length; i++)
330
630
  assertRsiEffectivePatch(next[i], final[i]);
331
631
  const recompiled = await ports.compile({ ...input, targetPatch: next[0], coordinatorPatch: next[1],
332
632
  targetEffective: final[0], coordinatorEffective: final[1] });
333
- if (!isDeepStrictEqual(recompiled, compiled))
633
+ if (!isDeepStrictEqual(patchValue(recompiled.targetPatch, 'recompiled target'), patchValue(compiled.targetPatch, 'compiled target'))
634
+ || !isDeepStrictEqual(patchValue(recompiled.coordinatorPatch, 'recompiled coordinator'), patchValue(compiled.coordinatorPatch, 'compiled coordinator')))
334
635
  fail('effective profile differs from the compiled deployment');
636
+ if (environments) {
637
+ ports.reloadServices();
638
+ await ports.validateServiceUnits(manifest, environments.target);
639
+ }
335
640
  journal.stage = 'applied';
336
641
  await atomicWrite(journalPath, JSON.stringify(journal));
337
642
  }
338
643
  catch (error) {
339
- await rollback(home, journalPath, journal);
644
+ await rollback(home, journalPath, journal, ports);
340
645
  if (previous)
341
646
  await atomicWrite(journalPath, JSON.stringify(previous));
342
647
  throw error;
343
648
  }
344
649
  }
650
+ if (unchanged && environments) {
651
+ ports.reloadServices();
652
+ await ports.validateServiceUnits(manifest, environments.target);
653
+ }
345
654
  // Starting is explicit and follows successful composition of both profiles.
346
655
  // A failed start retains the applied journal; it never rewrites a live Host.
347
656
  if (args.start) {
@@ -349,12 +658,130 @@ export async function configureRsiSetup(args, ports = defaultPorts) {
349
658
  await ports.start(pair[0], home);
350
659
  }
351
660
  return { mode: args.start ? 'services-started' : 'configured', profiles: pair };
352
- });
661
+ })();
353
662
  }
354
663
  export async function runRsiSetup(argv = process.argv.slice(2)) {
355
664
  const args = parseRsiSetupArgs(argv);
356
665
  if (args.help) {
357
- process.stdout.write('Usage: dsh-rsi-setup --manifest <private.json> [--dsh-home <absolute>] [--apply --confirm-hosts-stopped [--start] | --rollback --confirm-hosts-stopped]\nDefault: validate installed profiles and finite authority configuration without changing profiles.\n');
666
+ process.stdout.write('Usage: dsh-rsi-setup --manifest <private.json> [--dsh-home <absolute>] [--apply --confirm-hosts-stopped [--ack-existing-automations] [--start] | --rollback --confirm-hosts-stopped]\n dsh-rsi-setup --prepare-source --profile <name> [--dsh-home <absolute>] [--source-repository <local-absolute>]\n dsh-rsi-setup --prepare-build --profile <name> [--dsh-home <absolute>] [--source-repository <local-absolute>] [--docker-path <absolute>] [--optional-build]\n dsh-rsi-setup --prepare-authorities --profile <name> [--dsh-home <absolute>]\n dsh-rsi-setup --install-owner --profile <name> [--dsh-home <absolute>] [--source-repository <local-absolute>] [--ack-existing-automations]\n dsh-rsi-setup --install-local-cohort --profile <name> --source-repository <local-absolute> --bundle <slug> [--bundle <slug> ...] [--dsh-home <absolute>]\n--ack-existing-automations acknowledges active or paused durable rows when enabling or switching an Automation scheduler; accepted only with --install-owner or --apply, and does not bypass inventory drift checks.\nDefault: validate installed profiles and finite authority configuration without changing profiles.\nSource preparation creates a private checkout and release repository for the installed version. Build preparation also prepares private authority tools, signing identities and local release storage on Linux, creates an offline image and exports a pinned native release toolchain/store/cache; --optional-build reports which build prerequisites are unavailable. Authority preparation alone needs neither Docker nor a source checkout and does not issue grants or start Hosts.\n');
667
+ process.stdout.write(' dsh-rsi-setup --prepare-local-update --profile <name> [--dsh-home <absolute>] [--source-repository <local-absolute>] [--add-memory-learning]\nPrepare local update source and tarballs outside the active Home; does not install or activate them. --add-memory-learning explicitly prepares the disabled learner bundle for a verified pre-owner installation.\n');
668
+ return;
669
+ }
670
+ if (args.prepareLocalUpdate) {
671
+ const controller = new AbortController();
672
+ const cancel = () => controller.abort(new Error('local update preparation interrupted'));
673
+ process.on('SIGINT', cancel);
674
+ process.on('SIGTERM', cancel);
675
+ try {
676
+ const { prepareRsiLocalUpdate } = await import('./rsi-local-update.js');
677
+ const { readRsiLocalCohort } = await import('./rsi-local-cohort.js');
678
+ const sourceRepository = args.sourceRepository ?? (await readRsiLocalCohort({ dshHome: args.dshHome, profile: args.profile })).sourceRepository;
679
+ const prepared = await prepareRsiLocalUpdate({ dshHome: args.dshHome, profile: args.profile, sourceRepository,
680
+ ...(args.addMemoryLearning ? { rootExtension: 'memory-learning' } : {}), signal: controller.signal });
681
+ process.stdout.write(`${JSON.stringify({ mode: prepared.mode, root: prepared.root, profile: prepared.profile,
682
+ sourceCommit: prepared.source.sourceCommit, version: prepared.cohort.version,
683
+ packages: prepared.cohort.packages.length, receiptDigest: prepared.receiptDigest,
684
+ ...(prepared.extension ? { extension: prepared.extension } : {}) })}\n`);
685
+ }
686
+ finally {
687
+ process.off('SIGINT', cancel);
688
+ process.off('SIGTERM', cancel);
689
+ }
690
+ return;
691
+ }
692
+ if (args.installLocalCohort) {
693
+ const controller = new AbortController();
694
+ const cancel = () => controller.abort(new Error('local cohort installation interrupted'));
695
+ process.once('SIGINT', cancel);
696
+ process.once('SIGTERM', cancel);
697
+ try {
698
+ const { installRsiLocalTarget } = await import('./rsi-local-install.js');
699
+ process.stdout.write(`${JSON.stringify(await installRsiLocalTarget({ dshHome: args.dshHome, profile: args.profile,
700
+ sourceRepository: args.sourceRepository, bundles: args.bundles, signal: controller.signal }))}\n`);
701
+ }
702
+ finally {
703
+ process.off('SIGINT', cancel);
704
+ process.off('SIGTERM', cancel);
705
+ }
706
+ return;
707
+ }
708
+ if (args.installOwner) {
709
+ if (process.platform !== 'linux') {
710
+ process.stdout.write(`${JSON.stringify({ mode: 'not-ready', reason: 'automatic dual Host installation requires Linux systemd user services' })}\n`);
711
+ process.exitCode = 3;
712
+ return;
713
+ }
714
+ const controller = new AbortController();
715
+ const cancel = () => controller.abort(new Error('owner installation interrupted'));
716
+ process.once('SIGINT', cancel);
717
+ process.once('SIGTERM', cancel);
718
+ try {
719
+ const { installRsiOwnerDeployment } = await import('./rsi-install.js');
720
+ const result = await installRsiOwnerDeployment({ dshHome: args.dshHome, profile: args.profile,
721
+ sourceRepository: args.sourceRepository, ackExistingAutomations: args.ackExistingAutomations, signal: controller.signal });
722
+ process.stdout.write(`${JSON.stringify(result)}\n`);
723
+ process.stderr.write(result.mode === 'ready' ? '自迭代双 Host 已启动并通过就绪检查。\n'
724
+ : `普通 supervised 安装已保留;自迭代双 Host 未就绪:${result.reason}。\n`);
725
+ if (result.mode === 'not-ready')
726
+ process.exitCode = 3;
727
+ }
728
+ finally {
729
+ process.off('SIGINT', cancel);
730
+ process.off('SIGTERM', cancel);
731
+ }
732
+ return;
733
+ }
734
+ if (args.prepareSource || args.prepareBuild || args.prepareAuthorities) {
735
+ await safeDirectory(args.dshHome);
736
+ const controller = new AbortController();
737
+ const cancel = () => controller.abort(new Error('resource preparation interrupted'));
738
+ process.once('SIGINT', cancel);
739
+ process.once('SIGTERM', cancel);
740
+ try {
741
+ const prepared = await withDshHomeLifecycleLock(args.dshHome, async () => {
742
+ const authorities = async () => ({
743
+ authorityRuntime: await prepareRsiAuthorityRuntime({ dshHome: args.dshHome, profile: args.profile, signal: controller.signal }),
744
+ authorityResources: await prepareRsiAuthorityResources({ dshHome: args.dshHome, profile: args.profile, signal: controller.signal }),
745
+ });
746
+ if (args.prepareAuthorities)
747
+ return authorities();
748
+ const source = await prepareRsiSourceWorkspace({
749
+ dshHome: args.dshHome, profile: args.profile, version, sourceRepository: args.sourceRepository, signal: controller.signal,
750
+ });
751
+ if (!args.prepareBuild)
752
+ return source;
753
+ const provisioned = { ...source, ...(process.platform === 'linux' ? await authorities()
754
+ : { authorityResourcesUnavailable: 'private authority runtime currently requires Linux' }) };
755
+ try {
756
+ const build = await prepareRsiBuildEnvironment({ dshHome: args.dshHome, profile: args.profile, source,
757
+ dockerPath: args.dockerPath, signal: controller.signal });
758
+ try {
759
+ const release = await prepareRsiReleaseBuildEnvironment({ dshHome: args.dshHome, profile: args.profile,
760
+ build, signal: controller.signal });
761
+ return { ...provisioned, sourceBuild: build.sourceBuild, releaseBuild: release.releaseBuild };
762
+ }
763
+ catch (error) {
764
+ controller.signal.throwIfAborted();
765
+ if (!args.optionalBuild || !(error instanceof RsiReleaseBuildUnavailableError))
766
+ throw error;
767
+ process.stderr.write(`自迭代发布构建环境未就绪:${error.message}。已保留源码与验证镜像。\n`);
768
+ return { ...provisioned, sourceBuild: build.sourceBuild, releaseBuildUnavailable: error.message };
769
+ }
770
+ }
771
+ catch (error) {
772
+ controller.signal.throwIfAborted();
773
+ if (!args.optionalBuild || !(error instanceof RsiBuildUnavailableError))
774
+ throw error;
775
+ process.stderr.write(`自迭代构建环境未就绪:${error.message}。已保留源码准备结果。\n`);
776
+ return { ...provisioned, buildUnavailable: error.message };
777
+ }
778
+ });
779
+ process.stdout.write(`${JSON.stringify(prepared)}\n`);
780
+ }
781
+ finally {
782
+ process.off('SIGINT', cancel);
783
+ process.off('SIGTERM', cancel);
784
+ }
358
785
  return;
359
786
  }
360
787
  // 平台门控只属于 CLI 外壳:事务逻辑经注入的 ports 隔离 systemctl/服务安装,