@dsh-enhanced/lark-channel 0.1.64 → 0.1.66

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 (155) hide show
  1. package/README.md +20 -6
  2. package/cordis.patch.yml +1 -0
  3. package/docs/operations.md +1 -1
  4. package/docs/progress-security.md +3 -0
  5. package/docs/rsi-setup.md +11 -7
  6. package/docs/setup.md +2 -1
  7. package/lib/adapter.d.ts +8 -0
  8. package/lib/adapter.d.ts.map +1 -1
  9. package/lib/adapter.js +34 -2
  10. package/lib/adapter.js.map +1 -1
  11. package/lib/config.d.ts +4 -0
  12. package/lib/config.d.ts.map +1 -1
  13. package/lib/config.js +52 -8
  14. package/lib/config.js.map +1 -1
  15. package/lib/index.d.ts +5 -0
  16. package/lib/index.d.ts.map +1 -1
  17. package/lib/index.js +24 -1
  18. package/lib/index.js.map +1 -1
  19. package/lib/progress.d.ts +11 -1
  20. package/lib/progress.d.ts.map +1 -1
  21. package/lib/progress.js +13 -1
  22. package/lib/progress.js.map +1 -1
  23. package/lib/rsi-authority-runtime.d.ts +1 -0
  24. package/lib/rsi-authority-runtime.d.ts.map +1 -1
  25. package/lib/rsi-authority-runtime.js +121 -18
  26. package/lib/rsi-authority-runtime.js.map +1 -1
  27. package/lib/rsi-bootstrap-manifest.d.ts +12 -5
  28. package/lib/rsi-bootstrap-manifest.d.ts.map +1 -1
  29. package/lib/rsi-bootstrap-manifest.js +63 -9
  30. package/lib/rsi-bootstrap-manifest.js.map +1 -1
  31. package/lib/rsi-bootstrap.js +3 -3
  32. package/lib/rsi-bootstrap.js.map +1 -1
  33. package/lib/rsi-build.d.ts +12 -0
  34. package/lib/rsi-build.d.ts.map +1 -1
  35. package/lib/rsi-build.js +55 -5
  36. package/lib/rsi-build.js.map +1 -1
  37. package/lib/rsi-creation-build.d.ts +3 -0
  38. package/lib/rsi-creation-build.d.ts.map +1 -1
  39. package/lib/rsi-creation-build.js +45 -0
  40. package/lib/rsi-creation-build.js.map +1 -1
  41. package/lib/rsi-host-update.d.ts +51 -0
  42. package/lib/rsi-host-update.d.ts.map +1 -1
  43. package/lib/rsi-host-update.js +61 -1
  44. package/lib/rsi-host-update.js.map +1 -1
  45. package/lib/rsi-install.d.ts +10 -3
  46. package/lib/rsi-install.d.ts.map +1 -1
  47. package/lib/rsi-install.js +65 -15
  48. package/lib/rsi-install.js.map +1 -1
  49. package/lib/rsi-linux-cgroup-roots.d.ts +3 -0
  50. package/lib/rsi-linux-cgroup-roots.d.ts.map +1 -0
  51. package/lib/rsi-linux-cgroup-roots.js +3 -0
  52. package/lib/rsi-linux-cgroup-roots.js.map +1 -0
  53. package/lib/rsi-linux-creation-build.d.ts +36 -0
  54. package/lib/rsi-linux-creation-build.d.ts.map +1 -0
  55. package/lib/rsi-linux-creation-build.js +213 -0
  56. package/lib/rsi-linux-creation-build.js.map +1 -0
  57. package/lib/rsi-linux-native-materialize.d.ts +31 -0
  58. package/lib/rsi-linux-native-materialize.d.ts.map +1 -0
  59. package/lib/rsi-linux-native-materialize.js +162 -0
  60. package/lib/rsi-linux-native-materialize.js.map +1 -0
  61. package/lib/rsi-linux-native-relocation.d.ts +120 -0
  62. package/lib/rsi-linux-native-relocation.d.ts.map +1 -0
  63. package/lib/rsi-linux-native-relocation.js +1555 -0
  64. package/lib/rsi-linux-native-relocation.js.map +1 -0
  65. package/lib/rsi-linux-native-resources.d.ts +97 -0
  66. package/lib/rsi-linux-native-resources.d.ts.map +1 -0
  67. package/lib/rsi-linux-native-resources.js +521 -0
  68. package/lib/rsi-linux-native-resources.js.map +1 -0
  69. package/lib/rsi-linux-repository-bootstrap.d.ts +17 -0
  70. package/lib/rsi-linux-repository-bootstrap.d.ts.map +1 -0
  71. package/lib/rsi-linux-repository-bootstrap.js +317 -0
  72. package/lib/rsi-linux-repository-bootstrap.js.map +1 -0
  73. package/lib/rsi-linux-runner-states.d.ts +20 -0
  74. package/lib/rsi-linux-runner-states.d.ts.map +1 -0
  75. package/lib/rsi-linux-runner-states.js +70 -0
  76. package/lib/rsi-linux-runner-states.js.map +1 -0
  77. package/lib/rsi-linux-runtime-bootstrap.d.ts +10 -0
  78. package/lib/rsi-linux-runtime-bootstrap.d.ts.map +1 -0
  79. package/lib/rsi-linux-runtime-bootstrap.js +38 -0
  80. package/lib/rsi-linux-runtime-bootstrap.js.map +1 -0
  81. package/lib/rsi-linux-source-build.d.ts +28 -0
  82. package/lib/rsi-linux-source-build.d.ts.map +1 -0
  83. package/lib/rsi-linux-source-build.js +43 -0
  84. package/lib/rsi-linux-source-build.js.map +1 -0
  85. package/lib/rsi-local-install.d.ts +2 -0
  86. package/lib/rsi-local-install.d.ts.map +1 -1
  87. package/lib/rsi-local-install.js +42 -1
  88. package/lib/rsi-local-install.js.map +1 -1
  89. package/lib/rsi-local-pair-install.d.ts +15 -0
  90. package/lib/rsi-local-pair-install.d.ts.map +1 -1
  91. package/lib/rsi-local-pair-install.js +21 -6
  92. package/lib/rsi-local-pair-install.js.map +1 -1
  93. package/lib/rsi-local-resources.d.ts +2 -2
  94. package/lib/rsi-local-resources.d.ts.map +1 -1
  95. package/lib/rsi-local-resources.js +5 -0
  96. package/lib/rsi-local-resources.js.map +1 -1
  97. package/lib/rsi-local-update-build.d.ts +25 -6
  98. package/lib/rsi-local-update-build.d.ts.map +1 -1
  99. package/lib/rsi-local-update-build.js +46 -7
  100. package/lib/rsi-local-update-build.js.map +1 -1
  101. package/lib/rsi-local-update.d.ts +5 -2
  102. package/lib/rsi-local-update.d.ts.map +1 -1
  103. package/lib/rsi-local-update.js +6 -5
  104. package/lib/rsi-local-update.js.map +1 -1
  105. package/lib/rsi-plugin-creation.d.ts +16 -1
  106. package/lib/rsi-plugin-creation.d.ts.map +1 -1
  107. package/lib/rsi-plugin-creation.js +60 -14
  108. package/lib/rsi-plugin-creation.js.map +1 -1
  109. package/lib/rsi-plugin-revision.d.ts +35 -0
  110. package/lib/rsi-plugin-revision.d.ts.map +1 -0
  111. package/lib/rsi-plugin-revision.js +200 -0
  112. package/lib/rsi-plugin-revision.js.map +1 -0
  113. package/lib/rsi-profile.d.ts +8 -0
  114. package/lib/rsi-profile.d.ts.map +1 -1
  115. package/lib/rsi-profile.js +55 -2
  116. package/lib/rsi-profile.js.map +1 -1
  117. package/lib/rsi-release-build.d.ts +24 -8
  118. package/lib/rsi-release-build.d.ts.map +1 -1
  119. package/lib/rsi-release-build.js +89 -37
  120. package/lib/rsi-release-build.js.map +1 -1
  121. package/lib/rsi-scanner-retirement-config.d.ts +34 -0
  122. package/lib/rsi-scanner-retirement-config.d.ts.map +1 -0
  123. package/lib/rsi-scanner-retirement-config.js +272 -0
  124. package/lib/rsi-scanner-retirement-config.js.map +1 -0
  125. package/lib/rsi-scanner-retirement-runtime.d.ts +23 -0
  126. package/lib/rsi-scanner-retirement-runtime.d.ts.map +1 -0
  127. package/lib/rsi-scanner-retirement-runtime.js +98 -0
  128. package/lib/rsi-scanner-retirement-runtime.js.map +1 -0
  129. package/lib/rsi-scanner-retirement-stage.d.ts +55 -0
  130. package/lib/rsi-scanner-retirement-stage.d.ts.map +1 -0
  131. package/lib/rsi-scanner-retirement-stage.js +183 -0
  132. package/lib/rsi-scanner-retirement-stage.js.map +1 -0
  133. package/lib/rsi-scanner-retirement-transition.d.ts +57 -0
  134. package/lib/rsi-scanner-retirement-transition.d.ts.map +1 -0
  135. package/lib/rsi-scanner-retirement-transition.js +320 -0
  136. package/lib/rsi-scanner-retirement-transition.js.map +1 -0
  137. package/lib/rsi-setup.d.ts +20 -0
  138. package/lib/rsi-setup.d.ts.map +1 -1
  139. package/lib/rsi-setup.js +102 -5
  140. package/lib/rsi-setup.js.map +1 -1
  141. package/lib/rsi-source-maintenance.d.ts +9 -1
  142. package/lib/rsi-source-maintenance.d.ts.map +1 -1
  143. package/lib/rsi-source-maintenance.js +64 -5
  144. package/lib/rsi-source-maintenance.js.map +1 -1
  145. package/lib/rsi-source-update.js +2 -2
  146. package/lib/rsi-source-update.js.map +1 -1
  147. package/lib/service.d.ts.map +1 -1
  148. package/lib/service.js +1 -0
  149. package/lib/service.js.map +1 -1
  150. package/lib/setup-profile.d.ts.map +1 -1
  151. package/lib/setup-profile.js +1 -0
  152. package/lib/setup-profile.js.map +1 -1
  153. package/lib/version.d.ts +1 -1
  154. package/lib/version.js +1 -1
  155. package/package.json +21 -21
@@ -0,0 +1,1555 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants, createReadStream } from 'node:fs';
3
+ import { chmod, lstat, mkdir, open, readdir, readFile, readlink, realpath, symlink } from 'node:fs/promises';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
6
+ import { isDeepStrictEqual } from 'node:util';
7
+ import { hashLinuxRuntimeTree, linuxRuntimeDigest, validateLinuxRuntime, validateLinuxUserNamespaceMapping, } from '@dsh-enhanced/assistant-isolation';
8
+ const PEER_SPECIFIER = '@dsh-enhanced/assistant-verifier/linux-behavior-runtime';
9
+ const PROFILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
10
+ const HEX = /^[a-f0-9]{64}$/u;
11
+ const COMMIT = /^[a-f0-9]{40}$/u;
12
+ const SEMVER = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
13
+ const MAX_ENTRIES = 500_000;
14
+ const MAX_BYTES = 8 * 1024 ** 3;
15
+ const MAX_RECEIPT_BYTES = 4 * 1024 * 1024;
16
+ const CHUNK = 64 * 1024;
17
+ const ELEMENT_NAMES = ['runtime', 'repository-runtime', 'system', 'behavior-runtime',
18
+ 'release-toolchain', 'release-store', 'release-cache'];
19
+ function fail(message) { throw new Error(`rsi Linux native relocation: ${message}`); }
20
+ const sha = (value) => createHash('sha256').update(value).digest('hex');
21
+ const digest = (value) => sha(JSON.stringify(value));
22
+ const stamp = (info) => JSON.stringify([info.dev, info.ino, info.uid, info.mode, info.size,
23
+ info.mtimeMs, info.ctimeMs]);
24
+ const canonical = (value) => typeof value === 'string' && isAbsolute(value)
25
+ && value === resolve(value) && value !== '/' && ![...value].some(character => character.charCodeAt(0) < 32);
26
+ const within = (path, root) => path === root || path.startsWith(`${root}/`);
27
+ const natural = (value) => Number.isSafeInteger(value) && value >= 0;
28
+ function object(value, message) {
29
+ if (value === null || typeof value !== 'object' || Array.isArray(value)
30
+ || Object.getPrototypeOf(value) !== Object.prototype)
31
+ fail(message);
32
+ return value;
33
+ }
34
+ function exactKeys(value, keys, message) {
35
+ if (Object.keys(value).sort().join() !== [...keys].sort().join())
36
+ fail(message);
37
+ }
38
+ function stringField(value, key, message) {
39
+ const field = value[key];
40
+ if (typeof field !== 'string')
41
+ fail(message);
42
+ return field;
43
+ }
44
+ function hexField(value, key, message) {
45
+ const field = stringField(value, key, message);
46
+ if (!HEX.test(field))
47
+ fail(message);
48
+ return field;
49
+ }
50
+ function naturalField(value, key, message) {
51
+ const field = value[key];
52
+ if (!natural(field))
53
+ fail(message);
54
+ return field;
55
+ }
56
+ function readInt(value, key) {
57
+ const field = value[key];
58
+ if (!natural(field))
59
+ fail('invalid recorded identity');
60
+ return field;
61
+ }
62
+ /** Keep AbortError identity while prefixing every other rejection. */
63
+ function reject(error, signal, message) {
64
+ if (signal?.aborted || error.name === 'AbortError')
65
+ throw error;
66
+ fail(`${message}: ${error.message}`);
67
+ }
68
+ async function exists(path) {
69
+ try {
70
+ await lstat(path);
71
+ return true;
72
+ }
73
+ catch (error) {
74
+ if (error.code === 'ENOENT')
75
+ return false;
76
+ throw error;
77
+ }
78
+ }
79
+ /** Canonical owner-private directory, as the stock readers require it. */
80
+ async function privateDirectory(path, message) {
81
+ const info = await lstat(path);
82
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.()
83
+ || info.mode & 0o077 || await realpath(path) !== path)
84
+ fail(message);
85
+ }
86
+ async function syncDirectory(path) {
87
+ const fd = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
88
+ try {
89
+ await fd.sync();
90
+ }
91
+ finally {
92
+ await fd.close();
93
+ }
94
+ }
95
+ async function makePrivateDirectory(path) {
96
+ await mkdir(path, { recursive: true, mode: 0o700 });
97
+ await chmod(path, 0o700);
98
+ await privateDirectory(path, 'relocation destination is not a private directory');
99
+ }
100
+ /** Every descriptor family records its immutable tree as a nested `root`. */
101
+ function readRoot(descriptor) {
102
+ const root = object(descriptor.root, 'invalid recorded root');
103
+ exactKeys(root, ['dev', 'ino', 'path', 'sha256'], 'invalid recorded root');
104
+ const path = stringField(root, 'path', 'invalid recorded root');
105
+ if (!canonical(path))
106
+ fail('invalid recorded root');
107
+ return root;
108
+ }
109
+ function validateSnapshot(value) {
110
+ const snapshot = object(value, 'invalid system snapshot');
111
+ exactKeys(snapshot, ['bytes', 'entries', 'manifestSha256', 'root', 'schemaVersion'], 'invalid system snapshot');
112
+ if (snapshot.schemaVersion !== 1)
113
+ fail('invalid system snapshot');
114
+ hexField(snapshot, 'manifestSha256', 'invalid system snapshot');
115
+ naturalField(snapshot, 'bytes', 'invalid system snapshot');
116
+ naturalField(snapshot, 'entries', 'invalid system snapshot');
117
+ readRoot(snapshot);
118
+ return snapshot;
119
+ }
120
+ function validateCgroupRoots(value, home) {
121
+ const roots = object(value, 'invalid cgroup roots');
122
+ exactKeys(roots, ['cpu', 'freezer', 'memory', 'mode', 'pids'], 'invalid cgroup roots');
123
+ if (roots.mode !== 'v1')
124
+ fail('invalid cgroup roots');
125
+ const owner = process.getuid?.();
126
+ const seen = new Set();
127
+ for (const name of ['cpu', 'freezer', 'memory', 'pids']) {
128
+ const path = stringField(roots, name, 'invalid cgroup roots');
129
+ if (!canonical(path) || !path.startsWith('/sys/fs/cgroup/') || within(path, home) || seen.has(path)
130
+ || owner !== undefined && !path.endsWith(`/dsh-native/uid-${owner}`))
131
+ fail('invalid cgroup roots');
132
+ seen.add(path);
133
+ }
134
+ return roots;
135
+ }
136
+ function validateUserNamespace(value) {
137
+ try {
138
+ return object(validateLinuxUserNamespaceMapping(value), 'invalid user namespace');
139
+ }
140
+ catch (error) {
141
+ reject(error, undefined, 'invalid user namespace');
142
+ }
143
+ }
144
+ function validateBehavior(peer, value) {
145
+ try {
146
+ return peer.validateLinuxPluginBehaviorRuntime(value);
147
+ }
148
+ catch (error) {
149
+ reject(error, undefined, 'behavior descriptor differs');
150
+ }
151
+ }
152
+ function validateRuntime(value) {
153
+ try {
154
+ return validateLinuxRuntime(value);
155
+ }
156
+ catch (error) {
157
+ reject(error, undefined, 'runtime descriptor differs');
158
+ }
159
+ }
160
+ async function hashTree(path, signal, message) {
161
+ try {
162
+ return await hashLinuxRuntimeTree(path, signal);
163
+ }
164
+ catch (error) {
165
+ reject(error, signal, message);
166
+ }
167
+ }
168
+ function behaviorPeer() {
169
+ const requirePeer = createRequire(import.meta.url);
170
+ try {
171
+ requirePeer.resolve(PEER_SPECIFIER);
172
+ }
173
+ catch (error) {
174
+ if (error.code === 'MODULE_NOT_FOUND'
175
+ || error.code === 'ERR_MODULE_NOT_FOUND') {
176
+ fail(`Linux native relocation requires installed peer ${PEER_SPECIFIER}`);
177
+ }
178
+ throw error;
179
+ }
180
+ const peer = requirePeer(PEER_SPECIFIER);
181
+ if (typeof peer.validateLinuxPluginBehaviorRuntime !== 'function'
182
+ || typeof peer.linuxPluginBehaviorRuntimeDigest !== 'function') {
183
+ fail('installed Linux behavior runtime peer is unavailable; update the assistant-verifier peer');
184
+ }
185
+ return peer;
186
+ }
187
+ const runtimeRelative = (profile) => `rsi-linux-runtimes/${profile}/runtime`;
188
+ const repositoryRuntimeRelative = (profile) => `rsi-linux-repository-runtimes/${profile}/runtime`;
189
+ const systemRelative = (profile) => `rsi-linux-repository-runtimes/${profile}/system`;
190
+ const behaviorRelative = (profile) => `rsi-authorities/${profile}/state/linux-creation-behavior/runtime`;
191
+ const creationRelative = (profile) => `rsi-linux-creation-builds/${profile}`;
192
+ const releaseRelative = (profile) => `rsi-release-builds/${profile}`;
193
+ const releaseTreeRelative = (profile, name) => `${releaseRelative(profile)}/${name}`;
194
+ const runtimeFamily = (profile) => `rsi-linux-runtimes/${profile}`;
195
+ const repositoryFamily = (profile) => `rsi-linux-repository-runtimes/${profile}`;
196
+ const authorityFamily = (profile) => `rsi-authorities/${profile}`;
197
+ const behaviorFamily = (profile) => `rsi-authorities/${profile}/state/linux-creation-behavior`;
198
+ function under(home, path) { return join(home, ...path.split('/')); }
199
+ function relativeOf(name, profile) {
200
+ switch (name) {
201
+ case 'runtime': return runtimeRelative(profile);
202
+ case 'repository-runtime': return repositoryRuntimeRelative(profile);
203
+ case 'system': return systemRelative(profile);
204
+ case 'behavior-runtime': return behaviorRelative(profile);
205
+ case 'release-toolchain': return releaseTreeRelative(profile, 'toolchain');
206
+ case 'release-store': return releaseTreeRelative(profile, 'store');
207
+ case 'release-cache': return releaseTreeRelative(profile, 'cache');
208
+ }
209
+ }
210
+ function rewrite(value, from, to) {
211
+ if (value === from)
212
+ return to;
213
+ return value.startsWith(`${from}/`) ? `${to}${value.slice(from.length)}` : value;
214
+ }
215
+ /** Rewrite only recorded absolute paths, preserving every key's position. */
216
+ function relocate(value, from, to) {
217
+ if (typeof value === 'string')
218
+ return rewrite(value, from, to);
219
+ if (Array.isArray(value))
220
+ return value.map(item => relocate(item, from, to));
221
+ if (value !== null && typeof value === 'object') {
222
+ const result = {};
223
+ for (const [key, item] of Object.entries(value))
224
+ result[key] = relocate(item, from, to);
225
+ return result;
226
+ }
227
+ return value;
228
+ }
229
+ /** Recompute the trailing self-digest without reordering retained fields. */
230
+ function seal(receipt) {
231
+ const content = {};
232
+ for (const [key, value] of Object.entries(receipt))
233
+ if (key !== 'receiptDigest')
234
+ content[key] = value;
235
+ return { ...content, receiptDigest: digest(content) };
236
+ }
237
+ function verifyReceiptDigest(receipt, message) {
238
+ const content = {};
239
+ for (const [key, value] of Object.entries(receipt))
240
+ if (key !== 'receiptDigest')
241
+ content[key] = value;
242
+ if (!HEX.test(stringField(receipt, 'receiptDigest', message)) || digest(content) !== receipt.receiptDigest) {
243
+ fail(message);
244
+ }
245
+ }
246
+ async function readReceipt(path, privateMode) {
247
+ const info = await lstat(path);
248
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > MAX_RECEIPT_BYTES
249
+ || info.uid !== process.getuid?.() || info.mode & 0o022 || privateMode && info.mode & 0o077) {
250
+ fail('unsafe source receipt');
251
+ }
252
+ const fd = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
253
+ try {
254
+ if (stamp(await fd.stat()) !== stamp(info))
255
+ fail('source receipt changed');
256
+ const text = await fd.readFile({ encoding: 'utf8' });
257
+ if (stamp(await fd.stat()) !== stamp(info) || stamp(await lstat(path)) !== stamp(info)) {
258
+ fail('source receipt changed');
259
+ }
260
+ return parseObject(text, 'invalid source receipt');
261
+ }
262
+ finally {
263
+ await fd.close();
264
+ }
265
+ }
266
+ function parseObject(text, message) {
267
+ let parsed;
268
+ try {
269
+ parsed = JSON.parse(text);
270
+ }
271
+ catch {
272
+ fail(message);
273
+ }
274
+ return object(parsed, message);
275
+ }
276
+ async function inventory(path, names, message) {
277
+ let entries;
278
+ try {
279
+ entries = await readdir(path);
280
+ }
281
+ catch (error) {
282
+ reject(error, undefined, message);
283
+ }
284
+ if (entries.sort().join() !== [...names].sort().join())
285
+ fail(message);
286
+ }
287
+ async function treeMeta(path, root, signal) {
288
+ const info = await lstat(path);
289
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.()
290
+ || await realpath(path) !== path)
291
+ fail('unsafe source tree');
292
+ if (info.dev !== readInt(root, 'dev') || info.ino !== readInt(root, 'ino'))
293
+ fail('source tree identity differs');
294
+ const recorded = stringField(root, 'sha256', 'invalid recorded root');
295
+ if (!HEX.test(recorded) || await hashTree(path, signal, 'source tree differs') !== recorded) {
296
+ fail('source tree digest differs');
297
+ }
298
+ }
299
+ /** The release build pins are sealed content digests produced by the Control
300
+ * Plane release adapter, which canonicalises keys and sorts directory entries.
301
+ * Re-deriving them here authenticates the source trees before they are copied
302
+ * and proves the copy carries exactly the pinned bytes after it. */
303
+ function canonicalReleaseValue(value) {
304
+ if (Array.isArray(value))
305
+ return `[${value.map(canonicalReleaseValue).join(',')}]`;
306
+ if (value !== null && typeof value === 'object') {
307
+ return `{${Object.entries(value).filter(([, item]) => item !== undefined)
308
+ .sort(([left], [right]) => left.localeCompare(right))
309
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalReleaseValue(item)}`).join(',')}}`;
310
+ }
311
+ return JSON.stringify(value);
312
+ }
313
+ const RELEASE_PIN_MAX_BYTES = 268_435_456;
314
+ async function releaseInventory(path, signal) {
315
+ checkAbort(signal);
316
+ const owner = process.getuid?.();
317
+ const root = await realpath(path);
318
+ if (root !== path)
319
+ fail('source release tree is not canonical');
320
+ const info = await lstat(path);
321
+ if (!info.isDirectory() || info.isSymbolicLink() || info.mode & 0o022
322
+ || owner !== undefined && info.uid !== owner)
323
+ fail('unsafe source release tree');
324
+ const entries = [];
325
+ const visit = async (directory, prefix) => {
326
+ checkAbort(signal);
327
+ for (const name of (await readdir(directory)).sort()) {
328
+ checkAbort(signal);
329
+ const entryPath = join(directory, name);
330
+ const entryName = prefix === '' ? name : `${prefix}/${name}`;
331
+ const entry = await lstat(entryPath);
332
+ if ((!entry.isSymbolicLink() && entry.mode & 0o022)
333
+ || owner !== undefined && entry.uid !== owner)
334
+ fail('unsafe source release tree entry');
335
+ if (entry.isDirectory()) {
336
+ entries.push({ path: entryName, type: 'directory', mode: entry.mode & 0o777 });
337
+ await visit(entryPath, entryName);
338
+ }
339
+ else if (entry.isFile()) {
340
+ if (entry.size > RELEASE_PIN_MAX_BYTES)
341
+ fail('source release tree file is oversized');
342
+ const bytes = await readFile(entryPath);
343
+ entries.push({ path: entryName, type: 'file', mode: entry.mode & 0o777, bytes: bytes.length,
344
+ sha256: sha(bytes) });
345
+ }
346
+ else if (entry.isSymbolicLink()) {
347
+ const target = await readlink(entryPath);
348
+ if (isAbsolute(target))
349
+ fail('source release tree has an absolute symlink');
350
+ const resolved = resolve(dirname(entryPath), target);
351
+ if (resolved !== path && !resolved.startsWith(`${path}/`)) {
352
+ fail('source release tree has an escaping symlink');
353
+ }
354
+ entries.push({ path: entryName, type: 'symlink', target });
355
+ }
356
+ else
357
+ fail('source release tree has an unsupported entry');
358
+ }
359
+ };
360
+ await visit(path, '');
361
+ return sha(canonicalReleaseValue(entries));
362
+ }
363
+ async function pinnedExecutableDigest(path, signal) {
364
+ checkAbort(signal);
365
+ const owner = process.getuid?.();
366
+ const info = await lstat(path);
367
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size < 1
368
+ || info.mode & 0o022 || (info.mode & 0o111) === 0 || owner !== undefined && info.uid !== owner && info.uid !== 0
369
+ || await realpath(path) !== path)
370
+ fail('unsafe source release executable');
371
+ if (info.size > RELEASE_PIN_MAX_BYTES)
372
+ fail('source release executable is oversized');
373
+ return sha(await readFile(path));
374
+ }
375
+ function releasePin(value, message) {
376
+ const item = object(value, message);
377
+ exactKeys(item, ['path', 'sha256'], message);
378
+ const path = stringField(item, 'path', message);
379
+ if (!canonical(path))
380
+ fail(message);
381
+ return { path, sha256: hexField(item, 'sha256', message) };
382
+ }
383
+ const RELEASE_PIN_KEYS = ['cacheRoot', 'nodeExecutable', 'pnpmExecutable', 'pnpmRoot', 'sandboxExecutable',
384
+ 'storeRoot', 'tarExecutable'];
385
+ /** Validate the recorded pin layout against one Home without reading its trees. */
386
+ function releaseLayout(receipt, home, profile) {
387
+ const build = object(receipt.releaseBuild, 'source release receipt differs');
388
+ exactKeys(build, RELEASE_PIN_KEYS, 'source release receipt differs');
389
+ const pins = {
390
+ sandboxExecutable: releasePin(build.sandboxExecutable, 'source release receipt differs'),
391
+ tarExecutable: releasePin(build.tarExecutable, 'source release receipt differs'),
392
+ nodeExecutable: releasePin(build.nodeExecutable, 'source release receipt differs'),
393
+ pnpmExecutable: releasePin(build.pnpmExecutable, 'source release receipt differs'),
394
+ pnpmRoot: releasePin(build.pnpmRoot, 'source release receipt differs'),
395
+ storeRoot: releasePin(build.storeRoot, 'source release receipt differs'),
396
+ cacheRoot: releasePin(build.cacheRoot, 'source release receipt differs'),
397
+ };
398
+ if (pins.sandboxExecutable.path !== '/usr/bin/bwrap' || pins.tarExecutable.path !== '/usr/bin/tar'
399
+ || pins.pnpmRoot.path !== under(home, releaseTreeRelative(profile, 'toolchain'))
400
+ || pins.storeRoot.path !== under(home, releaseTreeRelative(profile, 'store'))
401
+ || pins.cacheRoot.path !== under(home, releaseTreeRelative(profile, 'cache'))
402
+ || pins.nodeExecutable.path !== join(pins.pnpmRoot.path, 'node')
403
+ || pins.pnpmExecutable.path !== join(pins.pnpmRoot.path, 'pnpm')) {
404
+ fail('source release receipt differs');
405
+ }
406
+ return pins;
407
+ }
408
+ /** Re-derive every pin from the trees that are about to be copied. */
409
+ async function releasePins(pins, signal) {
410
+ for (const [label, item, kind] of [
411
+ ['sandbox', pins.sandboxExecutable, 'executable'],
412
+ ['tar', pins.tarExecutable, 'executable'],
413
+ ['node', pins.nodeExecutable, 'executable'],
414
+ ['pnpm', pins.pnpmExecutable, 'executable'],
415
+ ['pnpm root', pins.pnpmRoot, 'tree'],
416
+ ['pnpm store', pins.storeRoot, 'tree'],
417
+ ['pnpm cache', pins.cacheRoot, 'tree'],
418
+ ]) {
419
+ const computed = kind === 'executable'
420
+ ? await pinnedExecutableDigest(item.path, signal)
421
+ : await releaseInventory(item.path, signal);
422
+ if (computed !== item.sha256)
423
+ fail(`source release ${label} differs`);
424
+ }
425
+ // The stock release readers re-check these two semantics after the cutover.
426
+ const manifest = JSON.parse(await readFile(join(pins.pnpmRoot.path, 'package.json'), 'utf8'));
427
+ const version = stringField(manifest, 'version', 'source release pnpm manifest differs');
428
+ if (!SEMVER.test(version))
429
+ fail('source release pnpm manifest differs');
430
+ const storeVersion = Number.parseInt(version, 10);
431
+ const projects = join(pins.storeRoot.path, `v${storeVersion}`, 'projects');
432
+ const projectsInfo = await lstat(projects).catch(() => undefined);
433
+ if (projectsInfo === undefined || !projectsInfo.isDirectory() || projectsInfo.isSymbolicLink()) {
434
+ fail('source release pnpm store registry differs');
435
+ }
436
+ }
437
+ async function authenticateBaseRuntime(home, profile, signal, physicalHome = home) {
438
+ const runtimePath = under(physicalHome, runtimeRelative(profile));
439
+ // The schema-3 bootstrap receipt binds the immutable Node/pnpm toolchain.
440
+ const runtimeReceipt = await readReceipt(join(under(physicalHome, runtimeFamily(profile)), 'bootstrap.json'), true);
441
+ exactKeys(runtimeReceipt, ['archives', 'backend', 'dshHome', 'lockSha256', 'manifestHashes', 'profile',
442
+ 'receiptDigest', 'runtime', 'schemaVersion', 'sourceCommit'], 'source runtime receipt differs');
443
+ verifyReceiptDigest(runtimeReceipt, 'source runtime receipt digest differs');
444
+ if (runtimeReceipt.schemaVersion !== 3 || runtimeReceipt.backend !== 'linux-native'
445
+ || runtimeReceipt.dshHome !== home || runtimeReceipt.profile !== profile)
446
+ fail('source runtime receipt differs');
447
+ const sourceCommit = stringField(runtimeReceipt, 'sourceCommit', 'source runtime receipt differs');
448
+ if (!COMMIT.test(sourceCommit))
449
+ fail('source runtime receipt differs');
450
+ const lockSha256 = hexField(runtimeReceipt, 'lockSha256', 'source runtime receipt differs');
451
+ const runtime = validateRuntime(runtimeReceipt.runtime);
452
+ if (runtime.root.path !== under(home, runtimeRelative(profile)) || runtime.sourceCommit !== sourceCommit || runtime.lockSha256 !== lockSha256) {
453
+ fail('source runtime receipt differs');
454
+ }
455
+ const manifests = object(runtimeReceipt.manifestHashes, 'source runtime receipt differs');
456
+ if (Object.keys(manifests).length === 0
457
+ || Object.values(manifests).some(value => typeof value !== 'string' || !HEX.test(value))) {
458
+ fail('source runtime receipt differs');
459
+ }
460
+ const archives = object(runtimeReceipt.archives, 'source runtime receipt differs');
461
+ exactKeys(archives, ['node', 'pnpm', 'pnpmExe', 'pnpmNative'], 'source runtime receipt differs');
462
+ for (const key of ['node', 'pnpm', 'pnpmExe', 'pnpmNative'])
463
+ hexField(archives, key, 'source runtime receipt differs');
464
+ await treeMeta(runtimePath, readRoot(runtime), signal);
465
+ return { runtime, runtimeReceipt, sourceCommit, lockSha256 };
466
+ }
467
+ async function authenticateReleaseSource(home, profile, runtime, sourceCommit, signal, physicalHome = home) {
468
+ // The release build pins the fixed toolchain/store/cache trees that the
469
+ // native runner executes with; its receipt must describe this exact Home and
470
+ // the base runtime this Home was prepared from.
471
+ await inventory(under(physicalHome, releaseRelative(profile)), ['bootstrap.json', 'cache', 'store', 'toolchain'], 'source release family differs');
472
+ const releaseReceipt = await readReceipt(join(under(physicalHome, releaseRelative(profile)), 'bootstrap.json'), true);
473
+ exactKeys(releaseReceipt, ['backend', 'dshHome', 'profile', 'receiptDigest', 'releaseBuild', 'runtimeDigest',
474
+ 'schemaVersion', 'sourceCommit'], 'source release receipt differs');
475
+ verifyReceiptDigest(releaseReceipt, 'source release receipt digest differs');
476
+ const baseRuntimeDigest = linuxRuntimeDigest(runtime);
477
+ if (releaseReceipt.schemaVersion !== 2 || releaseReceipt.backend !== 'linux-native'
478
+ || releaseReceipt.dshHome !== home || releaseReceipt.profile !== profile
479
+ || releaseReceipt.sourceCommit !== sourceCommit || releaseReceipt.runtimeDigest !== baseRuntimeDigest) {
480
+ fail('source release receipt differs');
481
+ }
482
+ const release = releaseLayout(releaseReceipt, home, profile);
483
+ await releasePins(relocate(release, home, physicalHome), signal);
484
+ return releaseReceipt;
485
+ }
486
+ async function authenticateSource(home, profile, peer, signal) {
487
+ const repositoryPath = under(home, repositoryRuntimeRelative(profile));
488
+ const systemPath = under(home, systemRelative(profile));
489
+ const behaviorPath = under(home, behaviorRelative(profile));
490
+ const creationPath = under(home, creationRelative(profile));
491
+ await inventory(under(home, runtimeFamily(profile)), ['bootstrap.json', 'runtime'], 'source runtime family differs');
492
+ await inventory(under(home, repositoryFamily(profile)), ['bootstrap.json', 'runtime', 'system'], 'source repository family differs');
493
+ await inventory(under(home, behaviorFamily(profile)), ['behavior.json', 'runtime'], 'source behavior family differs');
494
+ await inventory(creationPath, ['bootstrap.json'], 'source creation family differs');
495
+ const { runtime, runtimeReceipt, sourceCommit, lockSha256 } = await authenticateBaseRuntime(home, profile, signal);
496
+ // The schema-4 repository receipt binds the base runtime, system snapshot and
497
+ // user namespace; only its path-bearing parts are relocated.
498
+ const repositoryReceipt = await readReceipt(join(under(home, repositoryFamily(profile)), 'bootstrap.json'), true);
499
+ exactKeys(repositoryReceipt, ['backend', 'baseRuntimeDigest', 'dshHome', 'lockSha256', 'nodeArchiveDigest',
500
+ 'npmVersion', 'ownerGid', 'ownerUid', 'profile', 'receiptDigest', 'runtime', 'runtimeDigest', 'schemaVersion',
501
+ 'sourceCommit', 'sourceDigest', 'systemSnapshot', 'systemSnapshotDigest', 'userNamespace',
502
+ 'userNamespaceDigest'], 'source repository receipt differs');
503
+ verifyReceiptDigest(repositoryReceipt, 'source repository receipt digest differs');
504
+ const repository = validateRuntime(repositoryReceipt.runtime);
505
+ const snapshot = validateSnapshot(repositoryReceipt.systemSnapshot);
506
+ const userNamespace = validateUserNamespace(repositoryReceipt.userNamespace);
507
+ const uid = process.getuid?.(), gid = process.getgid?.();
508
+ const sourceDigest = hexField(repositoryReceipt, 'sourceDigest', 'source repository receipt differs');
509
+ const nodeArchiveDigest = hexField(repositoryReceipt, 'nodeArchiveDigest', 'source repository receipt differs');
510
+ const npmVersion = stringField(repositoryReceipt, 'npmVersion', 'source repository receipt differs');
511
+ if (repositoryReceipt.schemaVersion !== 4 || repositoryReceipt.backend !== 'linux-native'
512
+ || repositoryReceipt.dshHome !== home || repositoryReceipt.profile !== profile
513
+ || repositoryReceipt.ownerUid !== uid || repositoryReceipt.ownerGid !== gid
514
+ || repositoryReceipt.sourceCommit !== sourceCommit || repositoryReceipt.lockSha256 !== lockSha256
515
+ || !SEMVER.test(npmVersion) || !HEX.test(sourceDigest) || !HEX.test(nodeArchiveDigest)
516
+ || repositoryReceipt.baseRuntimeDigest !== digest(runtimeReceipt.runtime)
517
+ || repositoryReceipt.runtimeDigest !== digest(repositoryReceipt.runtime)
518
+ || repositoryReceipt.systemSnapshotDigest !== digest(repositoryReceipt.systemSnapshot)
519
+ || repositoryReceipt.userNamespaceDigest !== digest(repositoryReceipt.userNamespace)
520
+ || repository.root.path !== repositoryPath || repository.sourceCommit !== sourceCommit
521
+ || repository.lockSha256 !== lockSha256
522
+ || readRoot(snapshot).path !== systemPath)
523
+ fail('source repository receipt differs');
524
+ await treeMeta(repositoryPath, readRoot(repository), signal);
525
+ await treeMeta(systemPath, readRoot(snapshot), signal);
526
+ // The behavior deployment receipt is the immutable authority for the plugin
527
+ // observer; its digest is also the creation receipt's preparation digest.
528
+ const behaviorReceipt = await readReceipt(join(under(home, behaviorFamily(profile)), 'behavior.json'), false);
529
+ exactKeys(behaviorReceipt, ['baseRuntimeDigest', 'compilerSha256', 'descriptor', 'receiptDigest', 'schemaVersion'], 'source behavior receipt differs');
530
+ verifyReceiptDigest(behaviorReceipt, 'source behavior receipt digest differs');
531
+ if (behaviorReceipt.schemaVersion !== 1
532
+ || behaviorReceipt.baseRuntimeDigest !== linuxRuntimeDigest(repository)
533
+ || !HEX.test(stringField(behaviorReceipt, 'compilerSha256', 'source behavior receipt differs'))) {
534
+ fail('source behavior receipt differs');
535
+ }
536
+ const behavior = validateBehavior(peer, behaviorReceipt.descriptor);
537
+ if (behavior.runtime.root.path !== behaviorPath)
538
+ fail('source behavior receipt differs');
539
+ await treeMeta(behaviorPath, readRoot(behavior.runtime), signal);
540
+ // The creation receipt aggregates the other three and carries cgroup roots
541
+ // that must already live outside the source Home.
542
+ const creationReceipt = await readReceipt(join(creationPath, 'bootstrap.json'), true);
543
+ exactKeys(creationReceipt, ['backend', 'behavior', 'behaviorDigest', 'buildRuntimeDigest', 'cgroupRoots',
544
+ 'dshHome', 'lockSha256', 'preparationDigest', 'profile', 'repositoryRuntimeDigest', 'schemaVersion',
545
+ 'sourceCommit', 'systemSnapshotDigest', 'userNamespaceDigest', 'receiptDigest'], 'source creation receipt differs');
546
+ verifyReceiptDigest(creationReceipt, 'source creation receipt digest differs');
547
+ validateCgroupRoots(creationReceipt.cgroupRoots, home);
548
+ const repositoryRuntimeDigest = linuxRuntimeDigest(repository);
549
+ if (creationReceipt.schemaVersion !== 3 || creationReceipt.backend !== 'linux-native'
550
+ || creationReceipt.dshHome !== home || creationReceipt.profile !== profile
551
+ || creationReceipt.sourceCommit !== sourceCommit || creationReceipt.lockSha256 !== lockSha256
552
+ || creationReceipt.buildRuntimeDigest !== repositoryRuntimeDigest
553
+ || creationReceipt.repositoryRuntimeDigest !== repositoryRuntimeDigest
554
+ || creationReceipt.preparationDigest !== behaviorReceipt.receiptDigest
555
+ || creationReceipt.behaviorDigest !== peer.linuxPluginBehaviorRuntimeDigest(behavior)
556
+ || creationReceipt.systemSnapshotDigest !== digest(repositoryReceipt.systemSnapshot)
557
+ || creationReceipt.userNamespaceDigest !== digest(repositoryReceipt.userNamespace)
558
+ || !isDeepStrictEqual(creationReceipt.behavior, behaviorReceipt.descriptor)) {
559
+ fail('source creation receipt differs');
560
+ }
561
+ const releaseReceipt = await authenticateReleaseSource(home, profile, runtime, sourceCommit, signal);
562
+ return {
563
+ runtime, runtimeReceipt, repository, repositoryReceipt, snapshot, userNamespace,
564
+ behavior, behaviorReceipt, creationReceipt, releaseReceipt,
565
+ };
566
+ }
567
+ function checkAbort(signal) { signal?.throwIfAborted(); }
568
+ async function writeAll(fd, buffer, signal) {
569
+ let offset = 0;
570
+ while (offset < buffer.length) {
571
+ checkAbort(signal);
572
+ const written = await fd.write(buffer, offset, buffer.length - offset);
573
+ if (written.bytesWritten <= 0)
574
+ fail('copy write stalled');
575
+ offset += written.bytesWritten;
576
+ }
577
+ }
578
+ async function copyFile(source, destination, before, budget, signal) {
579
+ budget.entries += 1;
580
+ budget.bytes += before.size;
581
+ if (budget.entries > MAX_ENTRIES)
582
+ fail('source tree entry bound exceeded');
583
+ if (budget.bytes > MAX_BYTES)
584
+ fail('source tree byte bound exceeded');
585
+ if (before.nlink !== 1 || before.mode & 0o022)
586
+ fail('unsafe source tree file');
587
+ const input = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
588
+ let output;
589
+ try {
590
+ if (stamp(await input.stat()) !== stamp(before))
591
+ fail('source tree changed during copy');
592
+ output = await open(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
593
+ let copied = 0;
594
+ const stream = createReadStream(source, signal
595
+ ? { fd: input.fd, autoClose: false, highWaterMark: CHUNK, signal }
596
+ : { fd: input.fd, autoClose: false, highWaterMark: CHUNK });
597
+ for await (const chunk of stream) {
598
+ checkAbort(signal);
599
+ const buffer = chunk;
600
+ copied += buffer.length;
601
+ if (copied > before.size)
602
+ fail('source tree file grew during copy');
603
+ await writeAll(output, buffer, signal);
604
+ }
605
+ if (copied !== before.size)
606
+ fail('source tree file changed during copy');
607
+ await output.sync();
608
+ await output.chmod(before.mode & 0o777);
609
+ if (stamp(await input.stat()) !== stamp(before) || stamp(await lstat(source)) !== stamp(before)) {
610
+ fail('source tree changed during copy');
611
+ }
612
+ }
613
+ finally {
614
+ await output?.close();
615
+ await input.close();
616
+ }
617
+ }
618
+ async function copyDirectory(source, destination, before, root, budget, signal) {
619
+ budget.entries += 1;
620
+ if (budget.entries > MAX_ENTRIES)
621
+ fail('source tree entry bound exceeded');
622
+ if (before.mode & 0o022)
623
+ fail('unsafe source tree directory');
624
+ await mkdir(destination, { mode: 0o700 });
625
+ const names = (await readdir(source)).sort();
626
+ for (const name of names) {
627
+ checkAbort(signal);
628
+ await copyEntry(join(source, name), join(destination, name), root, budget, signal);
629
+ }
630
+ await chmod(destination, before.mode & 0o777);
631
+ await syncDirectory(destination);
632
+ if (stamp(await lstat(source)) !== stamp(before))
633
+ fail('source tree changed during copy');
634
+ }
635
+ async function copyLink(source, destination, root, budget) {
636
+ budget.entries += 1;
637
+ if (budget.entries > MAX_ENTRIES)
638
+ fail('source tree entry bound exceeded');
639
+ const target = await readlink(source);
640
+ if (isAbsolute(target) || !within(resolve(dirname(source), target), root)
641
+ || !within(await realpath(source), root))
642
+ fail('source tree link escaped root');
643
+ await symlink(target, destination);
644
+ }
645
+ async function copyEntry(source, destination, root, budget, signal) {
646
+ checkAbort(signal);
647
+ const before = await lstat(source);
648
+ if (before.uid !== process.getuid?.())
649
+ fail('foreign-owned source tree entry');
650
+ if (before.isSymbolicLink()) {
651
+ await copyLink(source, destination, root, budget);
652
+ }
653
+ else if (before.isDirectory()) {
654
+ await copyDirectory(source, destination, before, root, budget, signal);
655
+ }
656
+ else if (before.isFile()) {
657
+ await copyFile(source, destination, before, budget, signal);
658
+ }
659
+ else
660
+ fail('unsupported source tree entry');
661
+ }
662
+ async function copyTree(source, destination, signal, expectedSha256) {
663
+ const root = await lstat(source);
664
+ if (!root.isDirectory() || root.isSymbolicLink() || root.uid !== process.getuid?.()
665
+ || await realpath(source) !== source)
666
+ fail('unsafe source tree');
667
+ const budget = { entries: 0, bytes: 0 };
668
+ await copyDirectory(source, destination, root, source, budget, signal);
669
+ const copied = await lstat(destination);
670
+ const copiedSha256 = await hashTree(destination, signal, 'copied tree differs');
671
+ if (expectedSha256 !== undefined && copiedSha256 !== expectedSha256)
672
+ fail('copied tree digest differs');
673
+ return { path: destination, dev: copied.dev, ino: copied.ino, sha256: copiedSha256, ...budget };
674
+ }
675
+ /** Re-seal the release receipt against the copied trees: pins only move, the
676
+ * runtime digest now binds the relocated base runtime. */
677
+ function relocatedReleaseReceipt(source, sourceHome, logicalHome, baseDigest) {
678
+ const receipt = object(relocate(source, sourceHome, logicalHome), 'invalid relocated release receipt');
679
+ const build = object(receipt.releaseBuild, 'invalid relocated release receipt');
680
+ exactKeys(build, RELEASE_PIN_KEYS, 'invalid relocated release receipt');
681
+ for (const key of RELEASE_PIN_KEYS) {
682
+ const pin = releasePin(build[key], 'invalid relocated release receipt');
683
+ build[key] = pin;
684
+ }
685
+ receipt.runtimeDigest = baseDigest;
686
+ receipt.dshHome = logicalHome;
687
+ return seal(receipt);
688
+ }
689
+ function setRoot(root, meta) {
690
+ root.dev = meta.dev;
691
+ root.ino = meta.ino;
692
+ root.sha256 = meta.sha256;
693
+ }
694
+ function rewriteRuntime(receipt, meta, path) {
695
+ const runtime = object(receipt.runtime, 'relocated runtime differs');
696
+ const root = readRoot(runtime);
697
+ root.path = path;
698
+ setRoot(root, meta);
699
+ validateRuntime(runtime);
700
+ }
701
+ async function writeReceipt(path, receipt) {
702
+ const fds = Buffer.from(JSON.stringify(receipt));
703
+ const fd = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
704
+ try {
705
+ await fd.writeFile(fds);
706
+ await fd.chmod(0o600);
707
+ await fd.sync();
708
+ }
709
+ finally {
710
+ await fd.close();
711
+ }
712
+ await syncDirectory(dirname(path));
713
+ }
714
+ function relocatedRuntimeReceipt(source, sourceHome, logicalHome, meta, runtimePath) {
715
+ const receipt = object(relocate(source, sourceHome, logicalHome), 'invalid relocated runtime receipt');
716
+ rewriteRuntime(receipt, meta, runtimePath);
717
+ return seal(receipt);
718
+ }
719
+ function relocatedRepositoryReceipt(source, sourceHome, logicalHome, runtimeMeta, systemMeta, runtimePath, systemPath, base) {
720
+ const receipt = object(relocate(source, sourceHome, logicalHome), 'invalid relocated repository receipt');
721
+ rewriteRuntime(receipt, runtimeMeta, runtimePath);
722
+ const snapshot = validateSnapshot(receipt.systemSnapshot);
723
+ const root = readRoot(snapshot);
724
+ root.path = systemPath;
725
+ setRoot(root, systemMeta);
726
+ const repositoryRuntime = object(receipt.runtime, 'invalid relocated repository runtime');
727
+ validateRuntime(repositoryRuntime);
728
+ receipt.baseRuntimeDigest = digest(base);
729
+ receipt.runtimeDigest = digest(repositoryRuntime);
730
+ receipt.systemSnapshotDigest = digest(receipt.systemSnapshot);
731
+ receipt.userNamespaceDigest = digest(receipt.userNamespace);
732
+ return seal(receipt);
733
+ }
734
+ function relocatedBehaviorReceipt(source, sourceHome, logicalHome, meta, runtimePath, baseDigest, peer) {
735
+ const receipt = object(relocate(source, sourceHome, logicalHome), 'invalid relocated behavior receipt');
736
+ const descriptor = object(receipt.descriptor, 'invalid relocated behavior descriptor');
737
+ rewriteRuntime(descriptor, meta, runtimePath);
738
+ validateBehavior(peer, descriptor);
739
+ receipt.baseRuntimeDigest = baseDigest;
740
+ return seal(receipt);
741
+ }
742
+ function relocatedCreationReceipt(source, sourceHome, logicalHome, behavior, values) {
743
+ const receipt = object(relocate(source, sourceHome, logicalHome), 'invalid relocated creation receipt');
744
+ // Embed the relocated behavior descriptor so the recorded identity and the digest agree.
745
+ receipt.behavior = behavior;
746
+ receipt.buildRuntimeDigest = values.repositoryRuntimeDigest;
747
+ receipt.repositoryRuntimeDigest = values.repositoryRuntimeDigest;
748
+ receipt.preparationDigest = values.preparationDigest;
749
+ receipt.behaviorDigest = values.behaviorDigest;
750
+ receipt.systemSnapshotDigest = values.systemSnapshotDigest;
751
+ receipt.userNamespaceDigest = values.userNamespaceDigest;
752
+ return seal(receipt);
753
+ }
754
+ function pureIdentity(input) {
755
+ const { sourceHome, physicalHome, logicalHome } = input;
756
+ for (const [name, value] of [['sourceHome', sourceHome], ['physicalHome', physicalHome],
757
+ ['logicalHome', logicalHome]]) {
758
+ if (!canonical(value))
759
+ fail(`invalid ${name}`);
760
+ }
761
+ if (input.existingLogicalHome !== undefined) {
762
+ if (!canonical(input.existingLogicalHome))
763
+ fail('invalid existingLogicalHome');
764
+ if (input.existingLogicalHome !== logicalHome)
765
+ fail('existing logical Home is not this logical Home');
766
+ }
767
+ if (!PROFILE.test(input.profile))
768
+ fail('invalid profile');
769
+ if (physicalHome === sourceHome || physicalHome === logicalHome || logicalHome === sourceHome) {
770
+ fail('relocation homes overlap');
771
+ }
772
+ if (within(physicalHome, sourceHome) || within(sourceHome, physicalHome)
773
+ || within(logicalHome, sourceHome) || within(sourceHome, logicalHome)
774
+ || within(physicalHome, logicalHome) || within(logicalHome, physicalHome))
775
+ fail('relocation homes overlap');
776
+ }
777
+ /** Substitute for the `logical Home already exists` refusal when the caller
778
+ * attests this is the live Home being cut over: the path must exist and be its
779
+ * own realpath, so a dangling path or a symlink to another Home still fails. */
780
+ async function attestedLogicalHome(logicalHome) {
781
+ if (!(await exists(logicalHome)))
782
+ fail('existing logical Home is missing');
783
+ if (await realpath(logicalHome) !== logicalHome)
784
+ fail('existing logical Home is not canonical');
785
+ }
786
+ /** A fresh physical Home must hold none of the families. An in-place stage of
787
+ * the live Home (`inPlace`) already carries the owner's authority state, which
788
+ * the relocation must keep: there only the behavior family it writes inside the
789
+ * authority must be absent, and the authority directories it descends through
790
+ * must already be private. */
791
+ async function assertTargetsAbsent(physicalHome, profile, inPlace) {
792
+ const authority = inPlace ? behaviorFamily(profile) : authorityFamily(profile);
793
+ for (const path of [runtimeFamily(profile), repositoryFamily(profile), authority,
794
+ creationRelative(profile), releaseRelative(profile)]) {
795
+ if (await exists(under(physicalHome, path)))
796
+ fail('relocation target already exists');
797
+ }
798
+ if (!inPlace)
799
+ return;
800
+ for (const path of [authorityFamily(profile), `${authorityFamily(profile)}/state`]) {
801
+ const location = under(physicalHome, path);
802
+ if (await exists(location))
803
+ await privateDirectory(location, 'unsafe existing authority directory');
804
+ }
805
+ }
806
+ function relocationContent(proof) {
807
+ return {
808
+ schemaVersion: proof.schemaVersion,
809
+ backend: proof.backend,
810
+ profile: proof.profile,
811
+ sourceHome: proof.sourceHome,
812
+ physicalHome: proof.physicalHome,
813
+ logicalHome: proof.logicalHome,
814
+ behaviorRootRelative: proof.behaviorRootRelative,
815
+ elements: proof.elements.map(item => ({
816
+ name: item.name, logicalPath: item.logicalPath, physicalPath: item.physicalPath, dev: item.dev,
817
+ ino: item.ino, sha256: item.sha256, entries: item.entries, bytes: item.bytes,
818
+ })),
819
+ digests: {
820
+ runtime: proof.digests.runtime,
821
+ repository: proof.digests.repository,
822
+ behavior: proof.digests.behavior,
823
+ creation: proof.digests.creation,
824
+ release: proof.digests.release,
825
+ },
826
+ };
827
+ }
828
+ function relocationDigest(proof) {
829
+ return digest(relocationContent(proof));
830
+ }
831
+ /** Relocate one prepared Linux-native DSH Home onto a fresh physical Home.
832
+ * The source is authenticated before copying and never modified. An
833
+ * `existingLogicalHome` attestation only replaces the "logical Home already
834
+ * exists" refusal; every other check and the proof shape stay the same. */
835
+ export async function relocateLinuxRsiNativeFamily(input) {
836
+ pureIdentity(input);
837
+ const { sourceHome, physicalHome, logicalHome, profile, signal } = input;
838
+ if (process.platform !== 'linux')
839
+ fail('Linux native relocation requires Linux');
840
+ signal?.throwIfAborted();
841
+ await privateDirectory(sourceHome, 'unsafe source Home');
842
+ if (input.existingLogicalHome === undefined) {
843
+ if (await exists(logicalHome))
844
+ fail('logical Home already exists');
845
+ }
846
+ else
847
+ await attestedLogicalHome(logicalHome);
848
+ if (await exists(physicalHome))
849
+ await privateDirectory(physicalHome, 'unsafe physical Home');
850
+ else
851
+ await makePrivateDirectory(physicalHome);
852
+ await assertTargetsAbsent(physicalHome, profile, input.existingLogicalHome !== undefined);
853
+ const peer = behaviorPeer();
854
+ const source = await authenticateSource(sourceHome, profile, peer, signal);
855
+ checkAbort(signal);
856
+ const runtimePath = under(physicalHome, runtimeRelative(profile));
857
+ const repositoryPath = under(physicalHome, repositoryRuntimeRelative(profile));
858
+ const systemPath = under(physicalHome, systemRelative(profile));
859
+ const behaviorPath = under(physicalHome, behaviorRelative(profile));
860
+ const logicalRuntimePath = under(logicalHome, runtimeRelative(profile));
861
+ const logicalRepositoryPath = under(logicalHome, repositoryRuntimeRelative(profile));
862
+ const logicalSystemPath = under(logicalHome, systemRelative(profile));
863
+ const logicalBehaviorPath = under(logicalHome, behaviorRelative(profile));
864
+ const releaseFamilyPath = under(physicalHome, releaseRelative(profile));
865
+ const releasePaths = {
866
+ toolchain: under(physicalHome, releaseTreeRelative(profile, 'toolchain')),
867
+ store: under(physicalHome, releaseTreeRelative(profile, 'store')),
868
+ cache: under(physicalHome, releaseTreeRelative(profile, 'cache')),
869
+ };
870
+ const logicalReleasePaths = {
871
+ toolchain: under(logicalHome, releaseTreeRelative(profile, 'toolchain')),
872
+ store: under(logicalHome, releaseTreeRelative(profile, 'store')),
873
+ cache: under(logicalHome, releaseTreeRelative(profile, 'cache')),
874
+ };
875
+ await makePrivateDirectory(under(physicalHome, runtimeFamily(profile)));
876
+ await makePrivateDirectory(under(physicalHome, repositoryFamily(profile)));
877
+ await makePrivateDirectory(under(physicalHome, behaviorFamily(profile)));
878
+ await makePrivateDirectory(under(physicalHome, creationRelative(profile)));
879
+ await makePrivateDirectory(releaseFamilyPath);
880
+ const runtimeCopy = await copyTree(source.runtime.root.path, runtimePath, signal, source.runtime.root.sha256);
881
+ checkAbort(signal);
882
+ const repositoryCopy = await copyTree(source.repository.root.path, repositoryPath, signal, source.repository.root.sha256);
883
+ checkAbort(signal);
884
+ const systemRoot = readRoot(source.snapshot);
885
+ const systemCopy = await copyTree(systemRoot.path, systemPath, signal, stringField(systemRoot, 'sha256', 'invalid recorded root'));
886
+ checkAbort(signal);
887
+ const behaviorCopy = await copyTree(source.behavior.runtime.root.path, behaviorPath, signal, source.behavior.runtime.root.sha256);
888
+ checkAbort(signal);
889
+ // The release trees are sealed by the Control Plane's canonical inventory
890
+ // digest, not by the runtime tree digest: copy them and immediately prove the
891
+ // copy against the authenticated pins.
892
+ const sourcePin = releaseLayout(source.releaseReceipt, sourceHome, profile);
893
+ const releaseCopies = {
894
+ toolchain: await copyTree(sourcePin.pnpmRoot.path, releasePaths.toolchain, signal),
895
+ store: await copyTree(sourcePin.storeRoot.path, releasePaths.store, signal),
896
+ cache: await copyTree(sourcePin.cacheRoot.path, releasePaths.cache, signal),
897
+ };
898
+ checkAbort(signal);
899
+ for (const [label, copy, pin] of [
900
+ ['toolchain', releaseCopies.toolchain, sourcePin.pnpmRoot],
901
+ ['store', releaseCopies.store, sourcePin.storeRoot],
902
+ ['cache', releaseCopies.cache, sourcePin.cacheRoot],
903
+ ]) {
904
+ if (await releaseInventory(copy.path, signal) !== pin.sha256) {
905
+ fail(`copied release ${label} differs`);
906
+ }
907
+ }
908
+ for (const [label, copy, pin] of [
909
+ ['node', join(releaseCopies.toolchain.path, 'node'), sourcePin.nodeExecutable],
910
+ ['pnpm', join(releaseCopies.toolchain.path, 'pnpm'), sourcePin.pnpmExecutable],
911
+ ]) {
912
+ if (await pinnedExecutableDigest(copy, signal) !== pin.sha256)
913
+ fail(`copied release ${label} differs`);
914
+ }
915
+ checkAbort(signal);
916
+ const runtimeReceipt = relocatedRuntimeReceipt(source.runtimeReceipt, sourceHome, logicalHome, runtimeCopy, logicalRuntimePath);
917
+ const repositoryReceipt = relocatedRepositoryReceipt(source.repositoryReceipt, sourceHome, logicalHome, repositoryCopy, systemCopy, logicalRepositoryPath, logicalSystemPath, object(runtimeReceipt.runtime, 'invalid relocated runtime receipt'));
918
+ // The creation build and the behavior receipt must bind the relocated
919
+ // repository runtime, never the authenticated source one: the copy has a new
920
+ // root path/dev/ino, so only this digest can be re-derived by the readers
921
+ // after the same-filesystem rename cutover.
922
+ const repositoryRuntimeDigest = linuxRuntimeDigest(validateRuntime(object(repositoryReceipt.runtime, 'invalid relocated repository runtime')));
923
+ const behaviorReceipt = relocatedBehaviorReceipt(source.behaviorReceipt, sourceHome, logicalHome, behaviorCopy, logicalBehaviorPath, repositoryRuntimeDigest, peer);
924
+ const behaviorDigest = peer.linuxPluginBehaviorRuntimeDigest(validateBehavior(peer, object(behaviorReceipt.descriptor, 'invalid relocated behavior descriptor')));
925
+ const creationReceipt = relocatedCreationReceipt(source.creationReceipt, sourceHome, logicalHome, object(behaviorReceipt.descriptor, 'invalid relocated behavior descriptor'), {
926
+ repositoryRuntimeDigest,
927
+ preparationDigest: stringField(behaviorReceipt, 'receiptDigest', 'invalid relocated behavior receipt'),
928
+ behaviorDigest,
929
+ systemSnapshotDigest: digest(repositoryReceipt.systemSnapshot),
930
+ userNamespaceDigest: digest(repositoryReceipt.userNamespace),
931
+ });
932
+ const elements = [
933
+ element('runtime', logicalRuntimePath, runtimeCopy),
934
+ element('repository-runtime', logicalRepositoryPath, repositoryCopy),
935
+ element('system', logicalSystemPath, systemCopy),
936
+ element('behavior-runtime', logicalBehaviorPath, behaviorCopy),
937
+ element('release-toolchain', logicalReleasePaths.toolchain, releaseCopies.toolchain),
938
+ element('release-store', logicalReleasePaths.store, releaseCopies.store),
939
+ element('release-cache', logicalReleasePaths.cache, releaseCopies.cache),
940
+ ];
941
+ // The release receipt must bind the relocated base runtime while keeping the
942
+ // logical Home paths the post-cutover stock readers re-derive.
943
+ const releaseReceipt = relocatedReleaseReceipt(source.releaseReceipt, sourceHome, logicalHome, linuxRuntimeDigest(validateRuntime(object(runtimeReceipt.runtime, 'invalid relocated runtime receipt'))));
944
+ if (releaseLayout(releaseReceipt, logicalHome, profile).pnpmRoot.path !== logicalReleasePaths.toolchain) {
945
+ fail('relocated release receipt differs');
946
+ }
947
+ const digests = {
948
+ runtime: stringField(runtimeReceipt, 'receiptDigest', 'invalid relocated runtime receipt'),
949
+ repository: stringField(repositoryReceipt, 'receiptDigest', 'invalid relocated repository receipt'),
950
+ behavior: stringField(behaviorReceipt, 'receiptDigest', 'invalid relocated behavior receipt'),
951
+ creation: stringField(creationReceipt, 'receiptDigest', 'invalid relocated creation receipt'),
952
+ release: stringField(releaseReceipt, 'receiptDigest', 'invalid relocated release receipt'),
953
+ };
954
+ await writeReceipt(join(under(physicalHome, runtimeFamily(profile)), 'bootstrap.json'), runtimeReceipt);
955
+ await writeReceipt(join(under(physicalHome, repositoryFamily(profile)), 'bootstrap.json'), repositoryReceipt);
956
+ await writeReceipt(join(under(physicalHome, behaviorFamily(profile)), 'behavior.json'), behaviorReceipt);
957
+ await writeReceipt(join(physicalHome, creationRelative(profile), 'bootstrap.json'), creationReceipt);
958
+ await writeReceipt(join(releaseFamilyPath, 'bootstrap.json'), releaseReceipt);
959
+ const proof = {
960
+ schemaVersion: 1,
961
+ backend: 'linux-native',
962
+ profile,
963
+ sourceHome,
964
+ physicalHome,
965
+ logicalHome,
966
+ behaviorRootRelative: behaviorRelative(profile),
967
+ elements,
968
+ digests: { ...digests, relocation: '' },
969
+ };
970
+ proof.digests.relocation = relocationDigest(proof);
971
+ return proof;
972
+ }
973
+ function element(name, logicalPath, copy) {
974
+ return {
975
+ name, logicalPath, physicalPath: copy.path, dev: copy.dev, ino: copy.ino, sha256: copy.sha256,
976
+ entries: copy.entries, bytes: copy.bytes,
977
+ };
978
+ }
979
+ function proofShape(proof) {
980
+ const record = object(proof, 'invalid relocation proof');
981
+ exactKeys(record, ['backend', 'behaviorRootRelative', 'digests', 'elements', 'logicalHome', 'physicalHome',
982
+ 'profile', 'schemaVersion', 'sourceHome'], 'invalid relocation proof');
983
+ if (record.schemaVersion !== 1 || record.backend !== 'linux-native')
984
+ fail('invalid relocation proof');
985
+ const profile = stringField(record, 'profile', 'invalid relocation proof');
986
+ if (!PROFILE.test(profile))
987
+ fail('invalid relocation proof');
988
+ const sourceHome = stringField(record, 'sourceHome', 'invalid relocation proof');
989
+ const physicalHome = stringField(record, 'physicalHome', 'invalid relocation proof');
990
+ const logicalHome = stringField(record, 'logicalHome', 'invalid relocation proof');
991
+ for (const [name, value] of [['sourceHome', sourceHome], ['physicalHome', physicalHome],
992
+ ['logicalHome', logicalHome]])
993
+ if (!canonical(value))
994
+ fail(`invalid relocation proof ${name}`);
995
+ if (sourceHome === physicalHome || sourceHome === logicalHome || physicalHome === logicalHome
996
+ || within(sourceHome, physicalHome) || within(physicalHome, sourceHome)
997
+ || within(sourceHome, logicalHome) || within(logicalHome, sourceHome)
998
+ || within(physicalHome, logicalHome) || within(logicalHome, physicalHome)) {
999
+ fail('invalid relocation proof homes');
1000
+ }
1001
+ if (stringField(record, 'behaviorRootRelative', 'invalid relocation proof') !== behaviorRelative(profile)) {
1002
+ fail('invalid relocation proof behavior root');
1003
+ }
1004
+ const listed = record.elements;
1005
+ if (!Array.isArray(listed) || listed.length !== ELEMENT_NAMES.length)
1006
+ fail('invalid relocation proof elements');
1007
+ const elements = new Map();
1008
+ for (const item of listed) {
1009
+ const value = object(item, 'invalid relocation proof element');
1010
+ exactKeys(value, ['bytes', 'dev', 'entries', 'ino', 'logicalPath', 'name', 'physicalPath', 'sha256'], 'invalid relocation proof element');
1011
+ const name = stringField(value, 'name', 'invalid relocation proof element');
1012
+ if (!ELEMENT_NAMES.includes(name) || elements.has(name))
1013
+ fail('invalid relocation proof element');
1014
+ const logicalPath = stringField(value, 'logicalPath', 'invalid relocation proof element');
1015
+ const physicalPath = stringField(value, 'physicalPath', 'invalid relocation proof element');
1016
+ const expected = relativeOf(name, profile);
1017
+ if (!canonical(logicalPath) || !canonical(physicalPath)
1018
+ || relative(logicalHome, logicalPath) !== expected || relative(physicalHome, physicalPath) !== expected
1019
+ || logicalPath !== under(logicalHome, expected) || physicalPath !== under(physicalHome, expected)) {
1020
+ fail('relocation element paths differ');
1021
+ }
1022
+ const entries = naturalField(value, 'entries', 'invalid relocation proof element');
1023
+ if (entries < 1)
1024
+ fail('invalid relocation proof element');
1025
+ elements.set(name, {
1026
+ logicalPath, physicalPath,
1027
+ dev: naturalField(value, 'dev', 'invalid relocation proof element'),
1028
+ ino: naturalField(value, 'ino', 'invalid relocation proof element'),
1029
+ sha256: hexField(value, 'sha256', 'invalid relocation proof element'),
1030
+ entries,
1031
+ bytes: naturalField(value, 'bytes', 'invalid relocation proof element'),
1032
+ });
1033
+ }
1034
+ if (elements.size !== ELEMENT_NAMES.length)
1035
+ fail('invalid relocation proof elements');
1036
+ const digests = object(record.digests, 'invalid relocation proof digests');
1037
+ exactKeys(digests, ['behavior', 'creation', 'release', 'relocation', 'repository', 'runtime'], 'invalid relocation proof digests');
1038
+ for (const key of ['behavior', 'creation', 'release', 'relocation', 'repository', 'runtime']) {
1039
+ hexField(digests, key, 'invalid relocation proof digests');
1040
+ }
1041
+ return { record, elements, digests };
1042
+ }
1043
+ async function verifyCopies(elements) {
1044
+ for (const name of ELEMENT_NAMES) {
1045
+ const meta = elements.get(name) ?? fail('invalid relocation proof elements');
1046
+ await privateDirectory(dirname(meta.physicalPath), 'unsafe relocation destination');
1047
+ const info = await lstat(meta.physicalPath);
1048
+ if (!info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.()
1049
+ || info.dev !== meta.dev || info.ino !== meta.ino || await realpath(meta.physicalPath) !== meta.physicalPath
1050
+ || await hashTree(meta.physicalPath, undefined, 'relocated tree differs') !== meta.sha256) {
1051
+ fail('relocated tree differs');
1052
+ }
1053
+ }
1054
+ }
1055
+ async function readRelocated(physicalHome, profile, peer, elements) {
1056
+ const runtimeReceipt = await readReceipt(join(under(physicalHome, runtimeFamily(profile)), 'bootstrap.json'), false);
1057
+ const repositoryReceipt = await readReceipt(join(under(physicalHome, repositoryFamily(profile)), 'bootstrap.json'), false);
1058
+ const behaviorReceipt = await readReceipt(join(under(physicalHome, behaviorFamily(profile)), 'behavior.json'), false);
1059
+ const creationReceipt = await readReceipt(join(join(physicalHome, creationRelative(profile)), 'bootstrap.json'), false);
1060
+ const releaseReceipt = await readReceipt(join(under(physicalHome, releaseRelative(profile)), 'bootstrap.json'), false);
1061
+ const runtimeRuntime = validateRuntime(runtimeReceipt.runtime);
1062
+ const repositoryRuntime = validateRuntime(repositoryReceipt.runtime);
1063
+ const snapshot = validateSnapshot(repositoryReceipt.systemSnapshot);
1064
+ const userNamespace = validateUserNamespace(repositoryReceipt.userNamespace);
1065
+ const behavior = validateBehavior(peer, behaviorReceipt.descriptor);
1066
+ if (runtimeReceipt.schemaVersion !== 3 || runtimeReceipt.backend !== 'linux-native'
1067
+ || runtimeRuntime.root.path !== elements.get('runtime')?.logicalPath)
1068
+ fail('relocated runtime receipt differs');
1069
+ if (repositoryReceipt.schemaVersion !== 4 || repositoryReceipt.backend !== 'linux-native'
1070
+ || repositoryRuntime.root.path !== elements.get('repository-runtime')?.logicalPath
1071
+ || readRoot(snapshot).path !== elements.get('system')?.logicalPath) {
1072
+ fail('relocated repository receipt differs');
1073
+ }
1074
+ if (behaviorReceipt.schemaVersion !== 1
1075
+ || behavior.runtime.root.path !== elements.get('behavior-runtime')?.logicalPath) {
1076
+ fail('relocated behavior receipt differs');
1077
+ }
1078
+ if (creationReceipt.schemaVersion !== 3 || creationReceipt.backend !== 'linux-native') {
1079
+ fail('relocated creation receipt differs');
1080
+ }
1081
+ if (releaseReceipt.schemaVersion !== 2 || releaseReceipt.backend !== 'linux-native') {
1082
+ fail('relocated release receipt differs');
1083
+ }
1084
+ return {
1085
+ runtime: runtimeReceipt, repository: repositoryReceipt, snapshot, userNamespace,
1086
+ runtimeRuntime, repositoryRuntime, behavior, behaviorReceipt, creationReceipt, releaseReceipt,
1087
+ };
1088
+ }
1089
+ function sameTree(root, meta) {
1090
+ return root.dev === meta.dev && root.ino === meta.ino && root.sha256 === meta.sha256;
1091
+ }
1092
+ async function validateRelocatedReceipts(family, logicalHome, physicalHome, profile, elements, peer, digests) {
1093
+ const { runtime, repository, snapshot, userNamespace, runtimeRuntime, repositoryRuntime, behavior, behaviorReceipt, creationReceipt, releaseReceipt } = family;
1094
+ const runtimeMeta = elements.get('runtime') ?? fail('invalid relocation proof elements');
1095
+ const repositoryMeta = elements.get('repository-runtime') ?? fail('invalid relocation proof elements');
1096
+ const systemMeta = elements.get('system') ?? fail('invalid relocation proof elements');
1097
+ const behaviorMeta = elements.get('behavior-runtime') ?? fail('invalid relocation proof elements');
1098
+ verifyReceiptDigest(runtime, 'relocated runtime receipt digest differs');
1099
+ if (runtime.dshHome !== logicalHome || runtime.profile !== profile
1100
+ || !sameTree(readRoot(runtimeRuntime), runtimeMeta)) {
1101
+ fail('relocated runtime receipt differs');
1102
+ }
1103
+ if (digests.runtime !== runtime.receiptDigest)
1104
+ fail('relocated runtime receipt digest differs');
1105
+ verifyReceiptDigest(repository, 'relocated repository receipt digest differs');
1106
+ const repositoryRuntimeDigest = linuxRuntimeDigest(repositoryRuntime);
1107
+ if (repository.dshHome !== logicalHome || repository.profile !== profile
1108
+ || !sameTree(readRoot(repositoryRuntime), repositoryMeta)
1109
+ || !sameTree(readRoot(snapshot), systemMeta)
1110
+ || repository.baseRuntimeDigest !== digest(runtime.runtime)
1111
+ || repository.runtimeDigest !== digest(repository.runtime)
1112
+ || repository.systemSnapshotDigest !== digest(repository.systemSnapshot)
1113
+ || repository.userNamespaceDigest !== digest(repository.userNamespace)
1114
+ || repository.ownerUid !== process.getuid?.() || repository.ownerGid !== process.getgid?.()) {
1115
+ fail('relocated repository receipt differs');
1116
+ }
1117
+ if (digests.repository !== repository.receiptDigest)
1118
+ fail('relocated repository receipt digest differs');
1119
+ verifyReceiptDigest(behaviorReceipt, 'relocated behavior receipt digest differs');
1120
+ if (behaviorReceipt.baseRuntimeDigest !== repositoryRuntimeDigest
1121
+ || !sameTree(readRoot(behavior.runtime), behaviorMeta)) {
1122
+ fail('relocated behavior receipt differs');
1123
+ }
1124
+ if (digests.behavior !== behaviorReceipt.receiptDigest)
1125
+ fail('relocated behavior receipt digest differs');
1126
+ verifyReceiptDigest(creationReceipt, 'relocated creation receipt digest differs');
1127
+ validateCgroupRoots(creationReceipt.cgroupRoots, logicalHome);
1128
+ if (creationReceipt.dshHome !== logicalHome || creationReceipt.profile !== profile
1129
+ || creationReceipt.buildRuntimeDigest !== repositoryRuntimeDigest
1130
+ || creationReceipt.repositoryRuntimeDigest !== repositoryRuntimeDigest
1131
+ || creationReceipt.preparationDigest !== behaviorReceipt.receiptDigest
1132
+ || creationReceipt.behaviorDigest !== peer.linuxPluginBehaviorRuntimeDigest(behavior)
1133
+ || creationReceipt.systemSnapshotDigest !== digest(snapshot)
1134
+ || creationReceipt.userNamespaceDigest !== digest(userNamespace)
1135
+ || !isDeepStrictEqual(creationReceipt.behavior, behaviorReceipt.descriptor)
1136
+ || creationReceipt.sourceCommit !== repository.sourceCommit
1137
+ || creationReceipt.lockSha256 !== repository.lockSha256)
1138
+ fail('relocated creation receipt differs');
1139
+ if (digests.creation !== creationReceipt.receiptDigest)
1140
+ fail('relocated creation receipt digest differs');
1141
+ // Release build: the pins must describe the logical Home, and re-deriving
1142
+ // them from the physical trees must reproduce the sealed digests.
1143
+ verifyReceiptDigest(releaseReceipt, 'relocated release receipt digest differs');
1144
+ exactKeys(releaseReceipt, ['backend', 'dshHome', 'profile', 'receiptDigest', 'releaseBuild', 'runtimeDigest',
1145
+ 'schemaVersion', 'sourceCommit'], 'relocated release receipt differs');
1146
+ const pins = releaseLayout(releaseReceipt, logicalHome, profile);
1147
+ const toolchainMeta = elements.get('release-toolchain') ?? fail('invalid relocation proof elements');
1148
+ const storeMeta = elements.get('release-store') ?? fail('invalid relocation proof elements');
1149
+ const cacheMeta = elements.get('release-cache') ?? fail('invalid relocation proof elements');
1150
+ if (releaseReceipt.dshHome !== logicalHome || releaseReceipt.profile !== profile
1151
+ || releaseReceipt.sourceCommit !== repository.sourceCommit
1152
+ || releaseReceipt.runtimeDigest !== linuxRuntimeDigest(runtimeRuntime)
1153
+ || pins.pnpmRoot.path !== toolchainMeta.logicalPath || pins.storeRoot.path !== storeMeta.logicalPath
1154
+ || pins.cacheRoot.path !== cacheMeta.logicalPath) {
1155
+ fail('relocated release receipt differs');
1156
+ }
1157
+ if (digests.release !== releaseReceipt.receiptDigest)
1158
+ fail('relocated release receipt digest differs');
1159
+ const physicalOf = (path) => {
1160
+ const suffix = relative(logicalHome, path);
1161
+ if (suffix === '' || suffix.startsWith('..') || isAbsolute(suffix)) {
1162
+ fail('relocated release receipt differs');
1163
+ }
1164
+ return under(physicalHome, suffix);
1165
+ };
1166
+ for (const [label, item, kind] of [
1167
+ ['toolchain', pins.pnpmRoot, 'tree'],
1168
+ ['store', pins.storeRoot, 'tree'],
1169
+ ['cache', pins.cacheRoot, 'tree'],
1170
+ ['node', pins.nodeExecutable, 'executable'],
1171
+ ['pnpm', pins.pnpmExecutable, 'executable'],
1172
+ ]) {
1173
+ const physical = physicalOf(item.path);
1174
+ const computed = kind === 'tree' ? await releaseInventory(physical, undefined)
1175
+ : await pinnedExecutableDigest(physical, undefined);
1176
+ if (computed !== item.sha256)
1177
+ fail(`relocated release ${label} differs`);
1178
+ }
1179
+ }
1180
+ /** Re-derive a relocation proof from its physical Home without ever reading the
1181
+ * logical Home; the proof must still describe a byte-identical, un-opened copy. */
1182
+ export async function verifyLinuxRsiNativeRelocation(proof) {
1183
+ await openLinuxRsiNativeRelocatedFamily(proof);
1184
+ }
1185
+ function relocatedFamily(family, logicalHome) {
1186
+ const message = 'relocated receipt differs';
1187
+ const { runtime, behavior, creationReceipt, releaseReceipt } = family;
1188
+ const sourceCommit = stringField(runtime, 'sourceCommit', message);
1189
+ const lockSha256 = hexField(runtime, 'lockSha256', message);
1190
+ const cgroupRoots = validateCgroupRoots(creationReceipt.cgroupRoots, logicalHome);
1191
+ const releaseBuild = object(releaseReceipt.releaseBuild, message);
1192
+ return {
1193
+ runtime: { schemaVersion: 3, backend: 'linux-native', sourceCommit, lockSha256, runtime: family.runtimeRuntime },
1194
+ repository: {
1195
+ schemaVersion: 4,
1196
+ backend: 'linux-native',
1197
+ sourceCommit,
1198
+ lockSha256,
1199
+ runtime: family.repositoryRuntime,
1200
+ systemSnapshot: family.snapshot,
1201
+ userNamespace: family.userNamespace,
1202
+ },
1203
+ behavior,
1204
+ creationBuild: {
1205
+ schemaVersion: 3,
1206
+ backend: 'linux-native',
1207
+ sourceCommit,
1208
+ behavior,
1209
+ cgroupRoots: cgroupRoots,
1210
+ buildRuntimeDigest: hexField(creationReceipt, 'buildRuntimeDigest', message),
1211
+ },
1212
+ release: {
1213
+ schemaVersion: 2,
1214
+ backend: 'linux-native',
1215
+ sourceCommit: stringField(releaseReceipt, 'sourceCommit', message),
1216
+ runtimeDigest: hexField(releaseReceipt, 'runtimeDigest', message),
1217
+ releaseBuild: releaseBuild,
1218
+ },
1219
+ };
1220
+ }
1221
+ /** Verify a relocation proof and return its typed families in one pass, so the
1222
+ * ~585 MB release trees are hashed once instead of twice. */
1223
+ export async function openLinuxRsiNativeRelocatedFamily(proof) {
1224
+ const { record, elements, digests } = proofShape(proof);
1225
+ if (relocationDigest(proof) !== digests.relocation)
1226
+ fail('relocation proof digest differs');
1227
+ if (process.platform !== 'linux')
1228
+ fail('Linux native relocation verification requires Linux');
1229
+ const peer = behaviorPeer();
1230
+ const logicalHome = stringField(record, 'logicalHome', 'invalid relocation proof');
1231
+ const physicalHome = stringField(record, 'physicalHome', 'invalid relocation proof');
1232
+ const profile = stringField(record, 'profile', 'invalid relocation proof');
1233
+ await verifyCopies(elements);
1234
+ const family = await readRelocated(physicalHome, profile, peer, elements);
1235
+ await validateRelocatedReceipts(family, logicalHome, physicalHome, profile, elements, peer, digests);
1236
+ return relocatedFamily(family, logicalHome);
1237
+ }
1238
+ /** Re-open a verified relocation at another physical Home; callers may only
1239
+ * supply the proof's own physical Home or its logical Home.
1240
+ *
1241
+ * The one supported case is the rename(2) cutover: `rename` preserves dev/ino,
1242
+ * so the staged trees keep their sealed content identity while their paths move
1243
+ * under the live Home. Receipts record logical paths, so only the element and
1244
+ * receipt roots change; the proof itself stays the sealed staged record and is
1245
+ * never rewritten. */
1246
+ export async function openLinuxRsiNativeRelocatedFamilyAt(proof, physicalHome) {
1247
+ const { record, elements, digests } = proofShape(proof);
1248
+ const stagedHome = stringField(record, 'physicalHome', 'invalid relocation proof');
1249
+ if (!canonical(physicalHome))
1250
+ fail('invalid relocation physical Home');
1251
+ if (physicalHome === stagedHome)
1252
+ return openLinuxRsiNativeRelocatedFamily(proof);
1253
+ const logicalHome = stringField(record, 'logicalHome', 'invalid relocation proof');
1254
+ if (physicalHome !== logicalHome)
1255
+ fail('relocation proof does not describe this physical Home');
1256
+ if (relocationDigest(proof) !== digests.relocation)
1257
+ fail('relocation proof digest differs');
1258
+ if (process.platform !== 'linux')
1259
+ fail('Linux native relocation verification requires Linux');
1260
+ const profile = stringField(record, 'profile', 'invalid relocation proof');
1261
+ const moved = new Map();
1262
+ for (const name of ELEMENT_NAMES) {
1263
+ const meta = elements.get(name) ?? fail('invalid relocation proof elements');
1264
+ const suffix = relative(stagedHome, meta.physicalPath);
1265
+ if (suffix === '' || suffix.startsWith('..') || isAbsolute(suffix))
1266
+ fail('relocation element paths differ');
1267
+ moved.set(name, { ...meta, physicalPath: under(physicalHome, suffix) });
1268
+ }
1269
+ const peer = behaviorPeer();
1270
+ await verifyCopies(moved);
1271
+ const family = await readRelocated(physicalHome, profile, peer, moved);
1272
+ await validateRelocatedReceipts(family, logicalHome, physicalHome, profile, moved, peer, digests);
1273
+ return relocatedFamily(family, logicalHome);
1274
+ }
1275
+ const BASE_ELEMENTS = ['runtime', 'release-toolchain', 'release-store', 'release-cache'];
1276
+ async function baseFileDigest(path, signal) {
1277
+ const fd = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1278
+ try {
1279
+ const before = await fd.stat();
1280
+ if (!before.isFile() || before.nlink !== 1 || before.uid !== process.getuid?.()
1281
+ || before.mode & 0o022 || before.size > MAX_BYTES)
1282
+ fail('unsafe base runtime file');
1283
+ const hash = createHash('sha256');
1284
+ for await (const chunk of createReadStream(path, { fd: fd.fd, autoClose: false, highWaterMark: CHUNK, signal })) {
1285
+ checkAbort(signal);
1286
+ hash.update(chunk);
1287
+ }
1288
+ if (stamp(await fd.stat()) !== stamp(before) || stamp(await lstat(path)) !== stamp(before))
1289
+ fail('base runtime file changed');
1290
+ return hash.digest('hex');
1291
+ }
1292
+ finally {
1293
+ await fd.close();
1294
+ }
1295
+ }
1296
+ /** Validate bytes and package pins without executing the recorded toolchain. */
1297
+ async function baseRuntimePins(runtime, logicalHome, physicalHome, signal) {
1298
+ const physical = (path) => rewrite(path, logicalHome, physicalHome);
1299
+ for (const path of [runtime.nodePath, join(runtime.nativePnpmRoot, 'node')]) {
1300
+ if (await pinnedExecutableDigest(physical(path), signal) !== runtime.nodeSha256)
1301
+ fail('source runtime Node pin differs');
1302
+ }
1303
+ if (await baseFileDigest(physical(join(runtime.root.path, 'pnpm-lock.yaml')), signal) !== runtime.lockSha256)
1304
+ fail('source runtime lock differs');
1305
+ const native = await readReceipt(physical(join(runtime.nativePnpmRoot, 'package.json')), false);
1306
+ if (native.name !== '@pnpm/exe' || native.version !== runtime.versions.pnpm)
1307
+ fail('source runtime native manifest differs');
1308
+ for (const [key, name] of [['typescript', 'typescript'], ['cordis', '@deepseek-ai/cordis'],
1309
+ ['tools', '@deepseek-ai/dsh-tools'], ['systemPrompt', '@deepseek-ai/dsh-system-prompt']]) {
1310
+ const manifest = await readReceipt(physical(join(runtime.modulesPath, name, 'package.json')), false);
1311
+ if (manifest.version !== runtime.versions[key])
1312
+ fail('source runtime package differs');
1313
+ }
1314
+ }
1315
+ async function baseRelativeLinks(root, signal) {
1316
+ let entries = 0;
1317
+ const visit = async (path) => {
1318
+ checkAbort(signal);
1319
+ if (++entries > MAX_ENTRIES)
1320
+ fail('base tree entry bound exceeded');
1321
+ const info = await lstat(path);
1322
+ if (info.isSymbolicLink()) {
1323
+ const target = await readlink(path);
1324
+ if (isAbsolute(target) || !within(resolve(dirname(path), target), root)
1325
+ || !within(await realpath(path), root))
1326
+ fail('base runtime link escaped root');
1327
+ }
1328
+ else if (info.isDirectory()) {
1329
+ for (const name of (await readdir(path)).sort())
1330
+ await visit(join(path, name));
1331
+ if (stamp(await lstat(path)) !== stamp(info))
1332
+ fail('base runtime directory changed');
1333
+ }
1334
+ };
1335
+ await visit(root);
1336
+ }
1337
+ async function authenticateBaseAt(logicalHome, physicalHome, profile, signal) {
1338
+ checkAbort(signal);
1339
+ await privateDirectory(under(physicalHome, runtimeFamily(profile)), 'unsafe base runtime family');
1340
+ await privateDirectory(under(physicalHome, releaseRelative(profile)), 'unsafe base release family');
1341
+ await inventory(under(physicalHome, runtimeFamily(profile)), ['bootstrap.json', 'runtime'], 'source runtime family differs');
1342
+ const { runtime, runtimeReceipt, sourceCommit } = await authenticateBaseRuntime(logicalHome, profile, signal, physicalHome);
1343
+ await baseRelativeLinks(under(physicalHome, runtimeRelative(profile)), signal);
1344
+ await baseRuntimePins(runtime, logicalHome, physicalHome, signal);
1345
+ const releaseReceipt = await authenticateReleaseSource(logicalHome, profile, runtime, sourceCommit, signal, physicalHome);
1346
+ // Runtime hashing also rejects hardlinks, writable entries and escaping links
1347
+ // in the release trees; schema-2 pins use a different inventory digest.
1348
+ const releaseTrees = [];
1349
+ for (const name of ['toolchain', 'store', 'cache']) {
1350
+ const path = under(physicalHome, releaseTreeRelative(profile, name)), before = await lstat(path);
1351
+ const sha256 = await hashTree(path, signal, 'unsafe release tree');
1352
+ if (stamp(await lstat(path)) !== stamp(before))
1353
+ fail('source release tree changed during inspection');
1354
+ releaseTrees.push({ path, dev: before.dev, ino: before.ino, mode: before.mode, sha256 });
1355
+ }
1356
+ checkAbort(signal);
1357
+ return { runtime, runtimeReceipt, releaseReceipt, releaseTrees };
1358
+ }
1359
+ function projectBase(state) {
1360
+ const sourceCommit = stringField(state.runtimeReceipt, 'sourceCommit', 'invalid base receipt');
1361
+ return {
1362
+ runtime: { schemaVersion: 3, backend: 'linux-native', sourceCommit,
1363
+ lockSha256: hexField(state.runtimeReceipt, 'lockSha256', 'invalid base receipt'), runtime: state.runtime },
1364
+ release: { schemaVersion: 2, backend: 'linux-native', sourceCommit,
1365
+ runtimeDigest: hexField(state.releaseReceipt, 'runtimeDigest', 'invalid base receipt'),
1366
+ releaseBuild: object(state.releaseReceipt.releaseBuild, 'invalid base receipt') },
1367
+ };
1368
+ }
1369
+ async function inspectBaseHome(home) {
1370
+ const info = await lstat(home);
1371
+ if (!canonical(home) || !info.isDirectory() || info.isSymbolicLink() || info.uid !== process.getuid?.()
1372
+ || info.mode & 0o022 || await realpath(home) !== home)
1373
+ fail('unsafe base Home');
1374
+ }
1375
+ /** Read existing receipts through their canonical logical Home spelling. Only
1376
+ * same-inode moves are supported: ordinary copied runtime roots fail dev/ino.
1377
+ * Release schema 2 carries content pins, not historical root inode fields. */
1378
+ export async function inspectLinuxRsiNativeBaseAt(input) {
1379
+ const { logicalHome, physicalHome, profile, signal } = input;
1380
+ if (!canonical(logicalHome) || !canonical(physicalHome) || !PROFILE.test(profile)
1381
+ || logicalHome !== physicalHome && (within(logicalHome, physicalHome) || within(physicalHome, logicalHome)))
1382
+ fail('invalid base inspection identity');
1383
+ if (process.platform !== 'linux')
1384
+ fail('Linux native base inspection requires Linux');
1385
+ await inspectBaseHome(physicalHome);
1386
+ return projectBase(await authenticateBaseAt(logicalHome, physicalHome, profile, signal));
1387
+ }
1388
+ function baseRelocationDigest(proof) {
1389
+ return digest({ schemaVersion: proof.schemaVersion, backend: proof.backend, purpose: proof.purpose,
1390
+ profile: proof.profile, sourceHome: proof.sourceHome, physicalHome: proof.physicalHome, logicalHome: proof.logicalHome,
1391
+ elements: proof.elements, digests: { runtime: proof.digests.runtime, release: proof.digests.release } });
1392
+ }
1393
+ function baseProofShape(proof) {
1394
+ const record = object(proof, 'invalid base relocation proof');
1395
+ exactKeys(record, ['schemaVersion', 'backend', 'purpose', 'profile', 'sourceHome', 'physicalHome', 'logicalHome', 'elements', 'digests'], 'invalid base relocation proof');
1396
+ if (record.schemaVersion !== 1 || record.backend !== 'linux-native' || record.purpose !== 'pre-owner-base')
1397
+ fail('invalid base relocation proof');
1398
+ const identity = { sourceHome: stringField(record, 'sourceHome', 'invalid base relocation proof'),
1399
+ physicalHome: stringField(record, 'physicalHome', 'invalid base relocation proof'),
1400
+ logicalHome: stringField(record, 'logicalHome', 'invalid base relocation proof'),
1401
+ profile: stringField(record, 'profile', 'invalid base relocation proof') };
1402
+ pureIdentity(identity);
1403
+ const list = record.elements;
1404
+ if (!Array.isArray(list) || list.length !== BASE_ELEMENTS.length)
1405
+ fail('invalid base relocation elements');
1406
+ const elements = new Map();
1407
+ for (const item of list) {
1408
+ const value = object(item, 'invalid base relocation element');
1409
+ exactKeys(value, ['name', 'logicalPath', 'physicalPath', 'dev', 'ino', 'sha256', 'entries', 'bytes'], 'invalid base relocation element');
1410
+ const name = stringField(value, 'name', 'invalid base relocation element');
1411
+ if (!BASE_ELEMENTS.includes(name) || elements.has(name))
1412
+ fail('invalid base relocation element');
1413
+ const logicalPath = stringField(value, 'logicalPath', 'invalid base relocation element');
1414
+ const physicalPath = stringField(value, 'physicalPath', 'invalid base relocation element');
1415
+ const suffix = relativeOf(name, identity.profile);
1416
+ if (logicalPath !== under(identity.logicalHome, suffix) || physicalPath !== under(identity.physicalHome, suffix))
1417
+ fail('base relocation element paths differ');
1418
+ const entries = naturalField(value, 'entries', 'invalid base relocation element');
1419
+ const bytes = naturalField(value, 'bytes', 'invalid base relocation element');
1420
+ if (entries < 1 || entries > MAX_ENTRIES || bytes > MAX_BYTES)
1421
+ fail('invalid base relocation element');
1422
+ elements.set(name, { logicalPath, physicalPath, entries, bytes,
1423
+ dev: naturalField(value, 'dev', 'invalid base relocation element'), ino: naturalField(value, 'ino', 'invalid base relocation element'),
1424
+ sha256: hexField(value, 'sha256', 'invalid base relocation element') });
1425
+ }
1426
+ const digests = object(record.digests, 'invalid base relocation digests');
1427
+ exactKeys(digests, ['runtime', 'release', 'relocation'], 'invalid base relocation digests');
1428
+ for (const key of ['runtime', 'release', 'relocation'])
1429
+ hexField(digests, key, 'invalid base relocation digests');
1430
+ if (baseRelocationDigest(proof) !== digests.relocation)
1431
+ fail('base relocation proof digest differs');
1432
+ return elements;
1433
+ }
1434
+ async function openBase(proof, physicalHome, elements) {
1435
+ if (process.platform !== 'linux')
1436
+ fail('Linux native base relocation verification requires Linux');
1437
+ await inspectBaseHome(physicalHome);
1438
+ for (const name of BASE_ELEMENTS) {
1439
+ const meta = elements.get(name) ?? fail('invalid base relocation elements');
1440
+ const path = under(physicalHome, relativeOf(name, proof.profile));
1441
+ const info = await lstat(path);
1442
+ if (info.dev !== meta.dev || info.ino !== meta.ino
1443
+ || await hashTree(path, undefined, 'relocated base tree differs') !== meta.sha256)
1444
+ fail('relocated base tree differs');
1445
+ }
1446
+ const state = await authenticateBaseAt(proof.logicalHome, physicalHome, proof.profile);
1447
+ // Schema-2 receipts have content pins but no inode fields. Compare the actual
1448
+ // post-authentication observations to the proof as well as the initial scan.
1449
+ for (const tree of state.releaseTrees) {
1450
+ const name = BASE_ELEMENTS.find(value => under(physicalHome, relativeOf(value, proof.profile)) === tree.path);
1451
+ const meta = name && elements.get(name);
1452
+ if (!meta || tree.dev !== meta.dev || tree.ino !== meta.ino || tree.sha256 !== meta.sha256)
1453
+ fail('relocated base tree changed during inspection');
1454
+ }
1455
+ if (state.runtimeReceipt.receiptDigest !== proof.digests.runtime || state.releaseReceipt.receiptDigest !== proof.digests.release)
1456
+ fail('relocated base receipt digest differs');
1457
+ return projectBase(state);
1458
+ }
1459
+ export async function openLinuxRsiNativeRelocatedBase(proof) {
1460
+ const elements = baseProofShape(proof);
1461
+ return openBase(proof, proof.physicalHome, elements);
1462
+ }
1463
+ export async function openLinuxRsiNativeRelocatedBaseAt(proof, physicalHome) {
1464
+ const elements = baseProofShape(proof);
1465
+ if (physicalHome !== proof.physicalHome && physicalHome !== proof.logicalHome)
1466
+ fail('base relocation proof does not describe this physical Home');
1467
+ return openBase(proof, physicalHome, elements);
1468
+ }
1469
+ /** Read-only before any acquisition. Even an absent Home must have an
1470
+ * existing canonical parent; recursive mkdir must never traverse an alias.
1471
+ * The early non-Linux pass checks path traversal only, before the platform gate.
1472
+ * Actual Linux acquisition additionally requires owner-private directories. */
1473
+ async function baseDestinationParents(physicalHome, strict) {
1474
+ const directory = async (path) => {
1475
+ const info = await lstat(path);
1476
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(path) !== path
1477
+ || strict && (info.uid !== process.getuid?.() || (info.mode & 0o077) !== 0))
1478
+ fail('unsafe base destination parent');
1479
+ };
1480
+ if (!await exists(physicalHome)) {
1481
+ await directory(dirname(physicalHome));
1482
+ return;
1483
+ }
1484
+ await directory(physicalHome);
1485
+ // Check both parents before creating either parent or profile family.
1486
+ for (const name of ['rsi-linux-runtimes', 'rsi-release-builds']) {
1487
+ const path = join(physicalHome, name);
1488
+ if (await exists(path))
1489
+ await directory(path);
1490
+ }
1491
+ }
1492
+ async function createBaseDirectory(path) {
1493
+ // No recursive mkdir or chmod: EEXIST (including a concurrent symlink) fails
1494
+ // without modifying an existing target. The outer caller owns the Home lock.
1495
+ await mkdir(path, { mode: 0o700 });
1496
+ await privateDirectory(path, 'unsafe base destination parent');
1497
+ }
1498
+ export async function relocateLinuxRsiNativeBase(input) {
1499
+ pureIdentity(input);
1500
+ const { sourceHome, physicalHome, logicalHome, profile, signal } = input;
1501
+ await baseDestinationParents(physicalHome, false);
1502
+ if (process.platform !== 'linux')
1503
+ fail('Linux native base relocation requires Linux');
1504
+ await baseDestinationParents(physicalHome, true);
1505
+ checkAbort(signal);
1506
+ await privateDirectory(sourceHome, 'unsafe source Home');
1507
+ if (input.existingLogicalHome === undefined) {
1508
+ if (await exists(logicalHome))
1509
+ fail('logical Home already exists');
1510
+ }
1511
+ else
1512
+ await attestedLogicalHome(logicalHome);
1513
+ if (await exists(physicalHome))
1514
+ await privateDirectory(physicalHome, 'unsafe physical Home');
1515
+ else
1516
+ await createBaseDirectory(physicalHome);
1517
+ for (const path of [runtimeFamily(profile), releaseRelative(profile)]) {
1518
+ if (await exists(under(physicalHome, path)))
1519
+ fail('base relocation target already exists');
1520
+ }
1521
+ const source = await authenticateBaseAt(sourceHome, sourceHome, profile, signal);
1522
+ await baseDestinationParents(physicalHome, true);
1523
+ for (const parent of ['rsi-linux-runtimes', 'rsi-release-builds']) {
1524
+ const path = join(physicalHome, parent);
1525
+ if (!await exists(path))
1526
+ await createBaseDirectory(path);
1527
+ else
1528
+ await privateDirectory(path, 'unsafe base destination parent');
1529
+ }
1530
+ await createBaseDirectory(under(physicalHome, runtimeFamily(profile)));
1531
+ await createBaseDirectory(under(physicalHome, releaseRelative(profile)));
1532
+ const runtimeCopy = await copyTree(source.runtime.root.path, under(physicalHome, runtimeRelative(profile)), signal, source.runtime.root.sha256);
1533
+ const copies = new Map([['runtime', runtimeCopy]]);
1534
+ for (const name of BASE_ELEMENTS.slice(1)) {
1535
+ const path = relativeOf(name, profile);
1536
+ copies.set(name, await copyTree(under(sourceHome, path), under(physicalHome, path), signal));
1537
+ }
1538
+ const runtimeReceipt = relocatedRuntimeReceipt(source.runtimeReceipt, sourceHome, logicalHome, runtimeCopy, under(logicalHome, runtimeRelative(profile)));
1539
+ const runtime = validateRuntime(runtimeReceipt.runtime);
1540
+ const releaseReceipt = relocatedReleaseReceipt(source.releaseReceipt, sourceHome, logicalHome, linuxRuntimeDigest(runtime));
1541
+ const proof = { schemaVersion: 1, backend: 'linux-native', purpose: 'pre-owner-base',
1542
+ profile, sourceHome, physicalHome, logicalHome,
1543
+ elements: BASE_ELEMENTS.map(name => ({ ...element(name, under(logicalHome, relativeOf(name, profile)), copies.get(name)), name })),
1544
+ digests: { runtime: stringField(runtimeReceipt, 'receiptDigest', 'invalid base receipt'),
1545
+ release: stringField(releaseReceipt, 'receiptDigest', 'invalid base receipt'), relocation: '' } };
1546
+ proof.digests.relocation = baseRelocationDigest(proof);
1547
+ await writeReceipt(join(under(physicalHome, runtimeFamily(profile)), 'bootstrap.json'), runtimeReceipt);
1548
+ await writeReceipt(join(under(physicalHome, releaseRelative(profile)), 'bootstrap.json'), releaseReceipt);
1549
+ await openLinuxRsiNativeRelocatedBase(proof);
1550
+ if (!isDeepStrictEqual(await authenticateBaseAt(sourceHome, sourceHome, profile, signal), source))
1551
+ fail('source base changed during relocation');
1552
+ checkAbort(signal);
1553
+ return proof;
1554
+ }
1555
+ //# sourceMappingURL=rsi-linux-native-relocation.js.map