@dsh-enhanced/lark-channel 0.1.47 → 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.
- package/README.md +19 -8
- package/docs/rsi-setup.md +123 -11
- package/docs/setup.md +27 -14
- package/docs/supervised-growth.md +2 -0
- package/lib/adapter.d.ts +6 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +79 -8
- package/lib/adapter.js.map +1 -1
- package/lib/business-cli-install.d.ts +9 -0
- package/lib/business-cli-install.d.ts.map +1 -0
- package/lib/business-cli-install.js +367 -0
- package/lib/business-cli-install.js.map +1 -0
- package/lib/business-environment.d.ts +2 -0
- package/lib/business-environment.d.ts.map +1 -0
- package/lib/business-environment.js +16 -0
- package/lib/business-environment.js.map +1 -0
- package/lib/business-skill.d.ts +32 -0
- package/lib/business-skill.d.ts.map +1 -0
- package/lib/business-skill.js +202 -0
- package/lib/business-skill.js.map +1 -0
- package/lib/rsi-authority-config.d.ts +29 -0
- package/lib/rsi-authority-config.d.ts.map +1 -0
- package/lib/rsi-authority-config.js +233 -0
- package/lib/rsi-authority-config.js.map +1 -0
- package/lib/rsi-authority-resources.d.ts +36 -0
- package/lib/rsi-authority-resources.d.ts.map +1 -0
- package/lib/rsi-authority-resources.js +139 -0
- package/lib/rsi-authority-resources.js.map +1 -0
- package/lib/rsi-authority-runtime.d.ts +50 -0
- package/lib/rsi-authority-runtime.d.ts.map +1 -0
- package/lib/rsi-authority-runtime.js +393 -0
- package/lib/rsi-authority-runtime.js.map +1 -0
- package/lib/rsi-bootstrap-manifest.d.ts +39 -0
- package/lib/rsi-bootstrap-manifest.d.ts.map +1 -0
- package/lib/rsi-bootstrap-manifest.js +366 -0
- package/lib/rsi-bootstrap-manifest.js.map +1 -0
- package/lib/rsi-bootstrap.d.ts +30 -0
- package/lib/rsi-bootstrap.d.ts.map +1 -0
- package/lib/rsi-bootstrap.js +363 -0
- package/lib/rsi-bootstrap.js.map +1 -0
- package/lib/rsi-build.d.ts +45 -0
- package/lib/rsi-build.d.ts.map +1 -0
- package/lib/rsi-build.js +404 -0
- package/lib/rsi-build.js.map +1 -0
- package/lib/rsi-creation-build.d.ts +47 -0
- package/lib/rsi-creation-build.d.ts.map +1 -0
- package/lib/rsi-creation-build.js +333 -0
- package/lib/rsi-creation-build.js.map +1 -0
- package/lib/rsi-host-authorities.d.ts +18 -0
- package/lib/rsi-host-authorities.d.ts.map +1 -0
- package/lib/rsi-host-authorities.js +129 -0
- package/lib/rsi-host-authorities.js.map +1 -0
- package/lib/rsi-host-update.d.ts +113 -0
- package/lib/rsi-host-update.d.ts.map +1 -0
- package/lib/rsi-host-update.js +316 -0
- package/lib/rsi-host-update.js.map +1 -0
- package/lib/rsi-install-inputs.d.ts +43 -0
- package/lib/rsi-install-inputs.d.ts.map +1 -0
- package/lib/rsi-install-inputs.js +342 -0
- package/lib/rsi-install-inputs.js.map +1 -0
- package/lib/rsi-install.d.ts +54 -0
- package/lib/rsi-install.d.ts.map +1 -0
- package/lib/rsi-install.js +404 -0
- package/lib/rsi-install.js.map +1 -0
- package/lib/rsi-local-activation.d.ts +78 -0
- package/lib/rsi-local-activation.d.ts.map +1 -0
- package/lib/rsi-local-activation.js +485 -0
- package/lib/rsi-local-activation.js.map +1 -0
- package/lib/rsi-local-cohort.d.ts +70 -0
- package/lib/rsi-local-cohort.d.ts.map +1 -0
- package/lib/rsi-local-cohort.js +781 -0
- package/lib/rsi-local-cohort.js.map +1 -0
- package/lib/rsi-local-install.d.ts +40 -0
- package/lib/rsi-local-install.d.ts.map +1 -0
- package/lib/rsi-local-install.js +221 -0
- package/lib/rsi-local-install.js.map +1 -0
- package/lib/rsi-local-pair-install.d.ts +38 -0
- package/lib/rsi-local-pair-install.d.ts.map +1 -0
- package/lib/rsi-local-pair-install.js +200 -0
- package/lib/rsi-local-pair-install.js.map +1 -0
- package/lib/rsi-local-profile-update.d.ts +25 -0
- package/lib/rsi-local-profile-update.d.ts.map +1 -0
- package/lib/rsi-local-profile-update.js +226 -0
- package/lib/rsi-local-profile-update.js.map +1 -0
- package/lib/rsi-local-resources.d.ts +37 -0
- package/lib/rsi-local-resources.d.ts.map +1 -0
- package/lib/rsi-local-resources.js +184 -0
- package/lib/rsi-local-resources.js.map +1 -0
- package/lib/rsi-local-roots-extension.d.ts +11 -0
- package/lib/rsi-local-roots-extension.d.ts.map +1 -0
- package/lib/rsi-local-roots-extension.js +31 -0
- package/lib/rsi-local-roots-extension.js.map +1 -0
- package/lib/rsi-local-update-build.d.ts +18 -0
- package/lib/rsi-local-update-build.d.ts.map +1 -0
- package/lib/rsi-local-update-build.js +37 -0
- package/lib/rsi-local-update-build.js.map +1 -0
- package/lib/rsi-local-update.d.ts +39 -0
- package/lib/rsi-local-update.d.ts.map +1 -0
- package/lib/rsi-local-update.js +205 -0
- package/lib/rsi-local-update.js.map +1 -0
- package/lib/rsi-memory-learning.d.ts +22 -0
- package/lib/rsi-memory-learning.d.ts.map +1 -0
- package/lib/rsi-memory-learning.js +76 -0
- package/lib/rsi-memory-learning.js.map +1 -0
- package/lib/rsi-owner-profile.d.ts +37 -0
- package/lib/rsi-owner-profile.d.ts.map +1 -0
- package/lib/rsi-owner-profile.js +178 -0
- package/lib/rsi-owner-profile.js.map +1 -0
- package/lib/rsi-plugin-creation.d.ts +31 -0
- package/lib/rsi-plugin-creation.d.ts.map +1 -0
- package/lib/rsi-plugin-creation.js +179 -0
- package/lib/rsi-plugin-creation.js.map +1 -0
- package/lib/rsi-profile.d.ts +23 -0
- package/lib/rsi-profile.d.ts.map +1 -1
- package/lib/rsi-profile.js +154 -43
- package/lib/rsi-profile.js.map +1 -1
- package/lib/rsi-release-build.d.ts +41 -0
- package/lib/rsi-release-build.d.ts.map +1 -0
- package/lib/rsi-release-build.js +223 -0
- package/lib/rsi-release-build.js.map +1 -0
- package/lib/rsi-service-environment.d.ts +6 -0
- package/lib/rsi-service-environment.d.ts.map +1 -0
- package/lib/rsi-service-environment.js +113 -0
- package/lib/rsi-service-environment.js.map +1 -0
- package/lib/rsi-service-setup.d.ts +10 -0
- package/lib/rsi-service-setup.d.ts.map +1 -0
- package/lib/rsi-service-setup.js +34 -0
- package/lib/rsi-service-setup.js.map +1 -0
- package/lib/rsi-setup.d.ts +35 -2
- package/lib/rsi-setup.d.ts.map +1 -1
- package/lib/rsi-setup.js +459 -32
- package/lib/rsi-setup.js.map +1 -1
- package/lib/rsi-source-maintenance.d.ts +25 -0
- package/lib/rsi-source-maintenance.d.ts.map +1 -0
- package/lib/rsi-source-maintenance.js +348 -0
- package/lib/rsi-source-maintenance.js.map +1 -0
- package/lib/rsi-source-update.d.ts +26 -0
- package/lib/rsi-source-update.d.ts.map +1 -0
- package/lib/rsi-source-update.js +276 -0
- package/lib/rsi-source-update.js.map +1 -0
- package/lib/rsi-source.d.ts +34 -0
- package/lib/rsi-source.d.ts.map +1 -0
- package/lib/rsi-source.js +375 -0
- package/lib/rsi-source.js.map +1 -0
- package/lib/rsi-systemd-bootstrap.d.ts +46 -0
- package/lib/rsi-systemd-bootstrap.d.ts.map +1 -0
- package/lib/rsi-systemd-bootstrap.js +271 -0
- package/lib/rsi-systemd-bootstrap.js.map +1 -0
- package/lib/sdk.d.ts.map +1 -1
- package/lib/sdk.js +26 -1
- package/lib/sdk.js.map +1 -1
- package/lib/setup-business.d.ts +49 -0
- package/lib/setup-business.d.ts.map +1 -0
- package/lib/setup-business.js +335 -0
- package/lib/setup-business.js.map +1 -0
- package/lib/setup-profile.d.ts +1 -1
- package/lib/setup-profile.d.ts.map +1 -1
- package/lib/setup-profile.js +25 -2
- package/lib/setup-profile.js.map +1 -1
- package/lib/setup.d.ts +12 -1
- package/lib/setup.d.ts.map +1 -1
- package/lib/setup.js +138 -34
- package/lib/setup.js.map +1 -1
- package/lib/systemd.d.ts +10 -0
- package/lib/systemd.d.ts.map +1 -1
- package/lib/systemd.js +23 -6
- package/lib/systemd.js.map +1 -1
- package/lib/types.d.ts +7 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- 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 {
|
|
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 === '--
|
|
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
|
|
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
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
188
|
-
...
|
|
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
|
-
|
|
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
|
|
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.
|
|
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/服务安装,
|