arcane-os 0.3.0 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
  5. package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -146
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1050 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +40 -62
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +112 -779
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -187
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1252
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -677
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2960
  150. package/docs/reference/sdk-api.md +0 -6694
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,34 +1,13 @@
1
- import {createHash,randomUUID} from 'node:crypto';
2
- import {constants as FS_CONSTANTS} from 'node:fs';
3
- import {
4
- lstat,
5
- mkdir,
6
- open,
7
- readdir,
8
- realpath,
9
- rename,
10
- rm
11
- } from 'node:fs/promises';
1
+ import {randomUUID} from 'node:crypto';
2
+ import {copyFile,lstat,mkdir,readdir,realpath,rename,rm} from 'node:fs/promises';
12
3
  import path from 'node:path';
13
- import {
14
- authenticateRuntimeReceipt,
15
- getSdkRoot,
16
- readVerifiedRuntimeFile
17
- } from './runtime.mjs';
18
- import {
19
- authenticateSdkBrowserRuntimeReceipt,
20
- getSdkBrowserRuntimeRoot,
21
- readVerifiedSdkBrowserRuntimeFile
22
- } from './sdk-browser-runtime.mjs';
4
+ import {getSdkRoot} from './runtime.mjs';
5
+ import {getSdkBrowserRuntimeRoot} from './sdk-browser-runtime.mjs';
23
6
 
24
- const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
25
- const CREATE_NEW_NO_FOLLOW=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL
26
- |FS_CONSTANTS.O_WRONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
27
- const MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES=64*1024*1024;
28
- const STAGING_PREFIX='.arcane-runtime-stage-';
29
- const issuedReceipts=new WeakSet();
7
+ const STAGING_PREFIX='.arcane-runtime-content-staging-';
8
+ const BACKUP_PREFIX='.arcane-runtime-content-backup-';
30
9
 
31
- function fail(message,code='ARCANE_WORKSPACE_RUNTIME_INTEGRITY_FAILED'){
10
+ function fail(message,code='ARCANE_WORKSPACE_RUNTIME_INVALID'){
32
11
  const error=new Error(message);
33
12
  error.code=code;
34
13
  throw error;
@@ -42,7 +21,7 @@ function throwIfAborted(signal){
42
21
  }
43
22
 
44
23
  async function emit(onEvent,event){
45
- if(typeof onEvent==='function')await onEvent(Object.freeze(event));
24
+ if(typeof onEvent==='function')await onEvent(event);
46
25
  }
47
26
 
48
27
  function compareText(left,right){
@@ -51,791 +30,145 @@ function compareText(left,right){
51
30
  return a<b?-1:a>b?1:0;
52
31
  }
53
32
 
54
- function safeRelativePath(value){
55
- if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
56
- ||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
57
- ||value==='.'||value.startsWith('../')||value.includes('/../')){
58
- fail(`Workspace runtime contains an unsafe path: ${String(value)}.`);
59
- }
60
- return value;
61
- }
62
-
63
- function portableKey(value){
64
- const normalized=value.normalize('NFC');
65
- return process.platform==='win32'?normalized.toLowerCase():normalized;
66
- }
67
-
68
- function resolveContained(root,relativePath){
69
- const safe=safeRelativePath(relativePath);
70
- const absolute=path.resolve(root,...safe.split('/'));
71
- const relative=path.relative(root,absolute);
72
- if(relative.startsWith('..')||path.isAbsolute(relative)){
73
- fail(`Workspace runtime path escapes its root: ${safe}.`);
74
- }
75
- return absolute;
76
- }
77
-
78
- function projectSourcePath(sourcePath){
79
- const safe=safeRelativePath(sourcePath);
80
- if(safe.startsWith('arcane/'))return safe.slice('arcane/'.length);
81
- if(safe.startsWith('strong-type/')){
82
- return `dependencies/strong-type/${safe.slice('strong-type/'.length)}`;
83
- }
84
- fail(`SDK runtime path cannot be projected into a workspace: ${safe}.`);
85
- }
86
-
87
- function projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt){
88
- if(!runtimeReceipt||!Array.isArray(runtimeReceipt.files)){
89
- fail('An authenticated SDK runtime receipt is required for workspace projection.');
90
- }
91
- if(!sdkBrowserRuntimeReceipt||!Array.isArray(sdkBrowserRuntimeReceipt.files)){
92
- fail('An authenticated SDK browser runtime receipt is required for workspace projection.');
93
- }
94
- const seen=new Map();
95
- const files=[];
96
- const add=(sourceFile,projectedPath,authority)=>{
97
- const key=portableKey(projectedPath);
98
- const collision=seen.get(key);
99
- if(collision){
100
- fail(
101
- `SDK runtime paths collide in the workspace projection: ${collision} and ${projectedPath}.`
102
- );
103
- }
104
- seen.set(key,projectedPath);
105
- files.push({
106
- path:projectedPath,
107
- sourcePath:sourceFile.path,
108
- authority,
109
- bytes:sourceFile.bytes,
110
- sha256:sourceFile.sha256
111
- });
112
- };
113
- for(const sourceFile of runtimeReceipt.files){
114
- add(sourceFile,projectSourcePath(sourceFile.path),'arcane-runtime');
115
- }
116
- for(const sourceFile of sdkBrowserRuntimeReceipt.files){
117
- add(sourceFile,`sdk/${safeRelativePath(sourceFile.path)}`,'sdk-browser-runtime');
118
- }
119
- files.sort((left,right)=>compareText(left.path,right.path));
120
- return files;
121
- }
122
-
123
- function projectedDirectories(files){
124
- const directories=new Set();
125
- for(const file of files){
126
- const parts=file.path.split('/');
127
- parts.pop();
128
- let current='';
129
- for(const part of parts){
130
- current=current?`${current}/${part}`:part;
131
- directories.add(current);
132
- }
133
- }
134
- return [...directories].sort(compareText);
135
- }
136
-
137
- function sameIdentity(before,after){
138
- return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
139
- &&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
140
- &&before.nlink===after.nlink;
141
- }
142
-
143
- function fileIdentity(info){
144
- return Object.freeze({
145
- device:String(info.dev),
146
- inode:String(info.ino),
147
- bytes:Number(info.size),
148
- modifiedNanoseconds:String(info.mtimeNs),
149
- changedNanoseconds:String(info.ctimeNs),
150
- links:String(info.nlink)
151
- });
152
- }
153
-
154
- function identityMatches(info,identity){
155
- return String(info.dev)===identity.device
156
- &&String(info.ino)===identity.inode
157
- &&Number(info.size)===identity.bytes
158
- &&String(info.mtimeNs)===identity.modifiedNanoseconds
159
- &&String(info.ctimeNs)===identity.changedNanoseconds
160
- &&String(info.nlink)===identity.links;
161
- }
162
-
163
- function locationIdentityMatches(info,identity){
164
- return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
165
- }
166
-
167
- async function workspaceLocation(workspaceRoot){
168
- const requestedRoot=path.resolve(workspaceRoot);
33
+ async function realDirectory(location,label){
34
+ const requested=path.resolve(location);
169
35
  let info;
170
- try{info=await lstat(requestedRoot,{bigint:true});}
36
+ try{info=await lstat(requested);}
171
37
  catch(error){
172
- if(error?.code==='ENOENT')fail(`Workspace root does not exist: ${requestedRoot}.`);
173
- throw error;
174
- }
175
- if(info.isSymbolicLink()||!info.isDirectory()){
176
- fail('Workspace root must be a real directory.');
177
- }
178
- const canonicalRoot=await realpath(requestedRoot);
179
- const canonicalInfo=await lstat(canonicalRoot,{bigint:true});
180
- if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()
181
- ||!sameIdentity(info,canonicalInfo)){
182
- fail('Workspace root changed while its location was being resolved.');
183
- }
184
- return {
185
- canonicalRoot,
186
- identity:fileIdentity(canonicalInfo)
187
- };
188
- }
189
-
190
- async function assertRealDirectoryLocation(directory,identity,label){
191
- let before;
192
- try{before=await lstat(directory,{bigint:true});}
193
- catch(error){
194
- if(error?.code==='ENOENT')fail(`${label} is missing.`);
195
- throw error;
196
- }
197
- if(before.isSymbolicLink()||!before.isDirectory()
198
- ||!locationIdentityMatches(before,identity)){
199
- fail(`${label} changed while workspace runtime materialization was active.`);
200
- }
201
- const canonical=await realpath(directory);
202
- if(canonical!==directory){
203
- fail(`${label} became a symbolic link or junction.`);
204
- }
205
- const after=await lstat(directory,{bigint:true});
206
- if(after.isSymbolicLink()||!after.isDirectory()
207
- ||!locationIdentityMatches(after,identity)
208
- ||!locationIdentityMatches(before,fileIdentity(after))){
209
- fail(`${label} changed while its location was being authenticated.`);
210
- }
211
- }
212
-
213
- async function inspectTree(root,{signal}={}){
214
- throwIfAborted(signal);
215
- const rootInfo=await lstat(root,{bigint:true});
216
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
217
- fail('Workspace arcane runtime root must be a real directory.');
218
- }
219
- const files=[];
220
- const directories=[];
221
- async function visit(directory,relativeRoot=''){
222
- throwIfAborted(signal);
223
- const entries=await readdir(directory,{withFileTypes:true});
224
- entries.sort((left,right)=>compareText(left.name,right.name));
225
- for(const entry of entries){
226
- throwIfAborted(signal);
227
- const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
228
- safeRelativePath(relative);
229
- const absolute=path.join(directory,entry.name);
230
- const details=await lstat(absolute,{bigint:true});
231
- if(details.isSymbolicLink()){
232
- fail(`Workspace arcane runtime contains a symbolic link or junction: ${relative}.`);
233
- }
234
- if(details.isDirectory()){
235
- directories.push({path:relative,identity:fileIdentity(details)});
236
- await visit(absolute,relative);
237
- }else if(details.isFile()){
238
- files.push(relative);
239
- }else{
240
- fail(`Workspace arcane runtime contains a non-file entry: ${relative}.`);
241
- }
242
- }
243
- }
244
- await visit(root);
245
- files.sort(compareText);
246
- directories.sort((left,right)=>compareText(left.path,right.path));
247
- return {rootIdentity:fileIdentity(rootInfo),files,directories};
248
- }
249
-
250
- async function hashExactFile(filePath,expectedBytes,signal){
251
- throwIfAborted(signal);
252
- let handle;
253
- try{
254
- handle=await open(filePath,READ_ONLY_NO_FOLLOW);
255
- }catch(error){
256
- if(error?.code==='ELOOP')fail(`Workspace runtime file became a symbolic link: ${filePath}.`);
38
+ if(error?.code==='ENOENT')fail(`${label} does not exist: ${requested}.`);
257
39
  throw error;
258
40
  }
259
- const hash=createHash('sha256');
260
- const buffer=Buffer.allocUnsafe(1024*1024);
261
- try{
262
- const before=await handle.stat({bigint:true});
263
- if(!before.isFile()||before.size!==BigInt(expectedBytes)){
264
- fail(`Workspace runtime file size is invalid: ${filePath}.`);
265
- }
266
- while(true){
267
- throwIfAborted(signal);
268
- const {bytesRead}=await handle.read(buffer,0,buffer.length,null);
269
- if(bytesRead===0)break;
270
- hash.update(buffer.subarray(0,bytesRead));
271
- }
272
- const after=await handle.stat({bigint:true});
273
- if(!sameIdentity(before,after)){
274
- fail(`Workspace runtime file changed while it was being verified: ${filePath}.`);
275
- }
276
- return {sha256:hash.digest('hex'),identity:fileIdentity(after)};
277
- }finally{
278
- await handle.close();
41
+ if(info.isSymbolicLink()||!info.isDirectory())fail(`${label} must be a real directory.`);
42
+ const canonical=await realpath(requested);
43
+ const canonicalInfo=await lstat(canonical);
44
+ if(canonicalInfo.isSymbolicLink()||!canonicalInfo.isDirectory()){
45
+ fail(`${label} must be a real directory.`);
279
46
  }
47
+ return canonical;
280
48
  }
281
49
 
282
- async function assertIdentityAt(root,entry,{directory}){
283
- const absolute=resolveContained(root,entry.path);
284
- const info=await lstat(absolute,{bigint:true});
285
- if(info.isSymbolicLink()
286
- ||(directory?!info.isDirectory():!info.isFile())
287
- ||!identityMatches(info,entry)){
288
- fail(`Workspace runtime ${directory?'directory':'file'} changed after verification: ${entry.path}.`);
289
- }
290
- }
291
-
292
- async function verifyProjectedTree(root,expectedFiles,{signal,onProgress}={}){
293
- const expectedPaths=expectedFiles.map(file=>file.path);
294
- const expectedDirectoryPaths=projectedDirectories(expectedFiles);
295
- const before=await inspectTree(root,{signal});
296
- if(JSON.stringify(before.files)!==JSON.stringify(expectedPaths)){
297
- fail('Workspace arcane runtime file inventory does not match the authenticated SDK runtime.');
298
- }
299
- if(JSON.stringify(before.directories.map(entry=>entry.path))
300
- !==JSON.stringify(expectedDirectoryPaths)){
301
- fail('Workspace arcane runtime directory inventory does not match the authenticated SDK runtime.');
302
- }
303
-
304
- const identities=[];
305
- let verifiedBytes=0;
306
- for(const [index,file] of expectedFiles.entries()){
307
- throwIfAborted(signal);
308
- const absolute=resolveContained(root,file.path);
309
- const result=await hashExactFile(absolute,file.bytes,signal);
310
- if(result.sha256!==file.sha256){
311
- fail(`Workspace runtime integrity check failed for ${file.path}.`);
312
- }
313
- verifiedBytes+=file.bytes;
314
- identities.push({path:file.path,...result.identity});
315
- if(onProgress){
316
- await onProgress({
317
- current:index+1,
318
- total:expectedFiles.length,
319
- verifiedBytes,
320
- totalBytes:expectedFiles.reduce((total,entry)=>total+entry.bytes,0),
321
- path:file.path
322
- });
323
- }
324
- }
325
-
326
- const rootAfter=await lstat(root,{bigint:true});
327
- if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
328
- ||!identityMatches(rootAfter,before.rootIdentity)){
329
- fail('Workspace arcane runtime root changed while it was being verified.');
330
- }
331
- for(const directory of before.directories){
332
- throwIfAborted(signal);
333
- await assertIdentityAt(root,{path:directory.path,...directory.identity},{directory:true});
334
- }
335
- for(const identity of identities){
336
- throwIfAborted(signal);
337
- await assertIdentityAt(root,identity,{directory:false});
338
- }
339
- return {
340
- rootIdentity:before.rootIdentity,
341
- directoryIdentities:before.directories.map(entry=>Object.freeze({
342
- path:entry.path,
343
- ...entry.identity
344
- })),
345
- identities:identities.map(Object.freeze)
346
- };
347
- }
348
-
349
- async function assertRequestedWorkspace(receipt,workspaceRoot){
350
- const requested=await workspaceLocation(workspaceRoot);
351
- if(requested.canonicalRoot!==receipt.canonicalWorkspaceLocation
352
- ||!locationIdentityMatches(
353
- {
354
- dev:BigInt(requested.identity.device),
355
- ino:BigInt(requested.identity.inode)
356
- },
357
- receipt.workspaceIdentity
358
- )){
359
- fail('Workspace runtime receipt belongs to a different workspace location.');
360
- }
361
- const expectedRoot=path.join(requested.canonicalRoot,'arcane');
362
- const canonicalRoot=await realpath(expectedRoot);
363
- if(canonicalRoot!==receipt.canonicalLocation||canonicalRoot!==expectedRoot){
364
- fail('Workspace runtime receipt belongs to a different arcane runtime location.');
365
- }
366
- return {workspace:requested,root:canonicalRoot};
367
- }
368
-
369
- async function assertWorkspaceRuntimeState(receipt,{workspaceRoot,signal}){
50
+ async function copyCompleteEntry(source,destination,label,signal){
370
51
  throwIfAborted(signal);
371
- const {root}=await assertRequestedWorkspace(receipt,workspaceRoot);
372
- const actual=await inspectTree(root,{signal});
373
- if(!identityMatches(
374
- {
375
- dev:BigInt(actual.rootIdentity.device),
376
- ino:BigInt(actual.rootIdentity.inode),
377
- size:BigInt(actual.rootIdentity.bytes),
378
- mtimeNs:BigInt(actual.rootIdentity.modifiedNanoseconds),
379
- ctimeNs:BigInt(actual.rootIdentity.changedNanoseconds),
380
- nlink:BigInt(actual.rootIdentity.links)
381
- },
382
- receipt.rootIdentity
383
- )){
384
- fail('Workspace arcane runtime root changed after verification.');
385
- }
386
- const actualPaths=actual.files;
387
- const expectedPaths=receipt.files.map(file=>file.path);
388
- if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
389
- fail('Workspace arcane runtime file inventory changed after verification.');
390
- }
391
- const actualDirectories=actual.directories.map(entry=>entry.path);
392
- const expectedDirectories=receipt.directoryIdentities.map(entry=>entry.path);
393
- if(JSON.stringify(actualDirectories)!==JSON.stringify(expectedDirectories)){
394
- fail('Workspace arcane runtime directory inventory changed after verification.');
395
- }
396
- for(const directory of receipt.directoryIdentities){
397
- throwIfAborted(signal);
398
- await assertIdentityAt(root,directory,{directory:true});
399
- }
400
- for(const identity of receipt.identities){
52
+ const info=await lstat(source);
53
+ if(info.isSymbolicLink())fail(`${label} must not contain a symbolic link or junction.`);
54
+ if(info.isFile()){
55
+ await copyFile(source,destination);
56
+ return;
57
+ }
58
+ if(!info.isDirectory())fail(`${label} contains a non-file entry.`);
59
+ await mkdir(destination,{recursive:true});
60
+ const entries=await readdir(source,{withFileTypes:true});
61
+ entries.sort((left,right)=>compareText(left.name,right.name));
62
+ for(const entry of entries){
401
63
  throwIfAborted(signal);
402
- await assertIdentityAt(root,identity,{directory:false});
403
- }
404
- return receipt;
405
- }
406
-
407
- async function writeNewFile(filePath,bytes){
408
- let handle;
409
- try{
410
- handle=await open(filePath,CREATE_NEW_NO_FOLLOW,0o644);
411
- }catch(error){
412
- if(error?.code==='ELOOP')fail(`Workspace runtime staging path became a symbolic link: ${filePath}.`);
413
- throw error;
414
- }
415
- try{
416
- await handle.writeFile(bytes);
417
- await handle.sync();
418
- }finally{
419
- await handle.close();
420
- }
421
- }
422
-
423
- async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
424
- const stagingParent=path.dirname(stagingRoot);
425
- const stagingName=path.basename(stagingRoot);
426
- if(stagingParent!==workspace.canonicalRoot||!stagingName.startsWith(STAGING_PREFIX)){
427
- fail(
428
- `Refusing to clean an unowned workspace runtime staging path: ${stagingRoot} `
429
- +`(parent ${stagingParent}; expected ${workspace.canonicalRoot}).`
64
+ await copyCompleteEntry(
65
+ path.join(source,entry.name),
66
+ path.join(destination,entry.name),
67
+ `${label}/${entry.name}`,
68
+ signal
430
69
  );
431
70
  }
432
- await assertRealDirectoryLocation(
433
- workspace.canonicalRoot,
434
- workspace.identity,
435
- 'Workspace root'
436
- );
437
- let stagingInfo;
438
- try{stagingInfo=await lstat(stagingRoot,{bigint:true});}
439
- catch(error){
440
- if(error?.code==='ENOENT')return;
441
- throw error;
442
- }
443
- if(stagingInfo.isSymbolicLink()||!stagingInfo.isDirectory()
444
- ||!locationIdentityMatches(stagingInfo,stagingIdentity)){
445
- fail('Refusing to clean a workspace runtime staging path whose identity changed.');
446
- }
447
- await assertRealDirectoryLocation(
448
- stagingRoot,
449
- stagingIdentity,
450
- 'Workspace runtime staging directory'
451
- );
452
- try{await inspectTree(stagingRoot);}
453
- catch(error){
454
- fail(`Refusing to clean an unauthenticated workspace runtime staging tree: ${error.message}`);
455
- }
456
- await assertRealDirectoryLocation(
457
- workspace.canonicalRoot,
458
- workspace.identity,
459
- 'Workspace root'
460
- );
461
- await assertRealDirectoryLocation(
462
- stagingRoot,
463
- stagingIdentity,
464
- 'Workspace runtime staging directory'
465
- );
466
- await rm(stagingRoot,{recursive:true,force:true});
467
- try{
468
- await lstat(stagingRoot,{bigint:true});
469
- fail('Workspace runtime staging path remained after cleanup.');
470
- }catch(error){
471
- if(error?.code!=='ENOENT')throw error;
472
- }
473
- await assertRealDirectoryLocation(
474
- workspace.canonicalRoot,
475
- workspace.identity,
476
- 'Workspace root'
477
- );
478
71
  }
479
72
 
480
- export async function verifyWorkspaceRuntime({
481
- workspaceRoot,
482
- runtimeRoot=path.join(getSdkRoot(),'runtime'),
483
- runtimeReceipt,
484
- browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
485
- sdkBrowserRuntimeReceipt,
486
- signal,
487
- onEvent
488
- }={}){
489
- if(!workspaceRoot)fail('workspaceRoot is required to verify a workspace runtime.');
490
- await authenticateRuntimeReceipt(runtimeReceipt,{runtimeRoot,signal});
491
- await authenticateSdkBrowserRuntimeReceipt(sdkBrowserRuntimeReceipt,{
492
- browserRuntimeRoot,
493
- signal
494
- });
495
- const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
496
- const workspace=await workspaceLocation(workspaceRoot);
497
- const projectedRoot=path.join(workspace.canonicalRoot,'arcane');
498
- let rootInfo;
499
- try{rootInfo=await lstat(projectedRoot,{bigint:true});}
500
- catch(error){
501
- if(error?.code==='ENOENT'){
502
- fail(`Workspace arcane runtime is missing: ${projectedRoot}.`);
503
- }
504
- throw error;
505
- }
506
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
507
- fail('Workspace arcane runtime root must be a real directory.');
508
- }
509
- const canonicalRoot=await realpath(projectedRoot);
510
- if(canonicalRoot!==projectedRoot){
511
- fail('Workspace arcane runtime root must not resolve outside its direct workspace location.');
512
- }
513
-
514
- const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
515
- await emit(onEvent,{
516
- type:'workspace.runtime.verify.started',
517
- fileCount:expectedFiles.length,
518
- totalBytes
519
- });
520
- const verified=await verifyProjectedTree(canonicalRoot,expectedFiles,{
521
- signal,
522
- onProgress:event=>emit(onEvent,{type:'workspace.runtime.verify.progress',...event})
523
- });
524
- const inventory=expectedFiles.map(file=>Object.freeze({...file}));
525
- const contentInventory=expectedFiles.map(file=>({
526
- path:file.path,
527
- bytes:file.bytes,
528
- sha256:file.sha256
529
- }));
530
- const receipt=Object.freeze({
531
- schemaVersion:1,
532
- kind:'arcane-workspace-runtime-verification',
533
- canonicalWorkspaceLocation:workspace.canonicalRoot,
534
- workspaceIdentity:workspace.identity,
535
- canonicalLocation:canonicalRoot,
536
- rootIdentity:verified.rootIdentity,
537
- sourceRuntimeLocation:runtimeReceipt.canonicalLocation,
538
- sourceManifestSha256:runtimeReceipt.manifestSha256,
539
- sourceContentSha256:runtimeReceipt.contentSha256,
540
- sourceBrowserRuntimeLocation:sdkBrowserRuntimeReceipt.canonicalLocation,
541
- sourceBrowserManifestSha256:sdkBrowserRuntimeReceipt.manifestSha256,
542
- sourceBrowserContentSha256:sdkBrowserRuntimeReceipt.contentSha256,
543
- sdkVersion:runtimeReceipt.sdkVersion,
544
- sources:Object.freeze({
545
- arcane:Object.freeze({
546
- authority:'arcane-os-sdk',
547
- location:runtimeReceipt.canonicalLocation,
548
- manifestSha256:runtimeReceipt.manifestSha256,
549
- contentSha256:runtimeReceipt.contentSha256,
550
- source:runtimeReceipt.source
551
- }),
552
- sdkBrowser:Object.freeze({
553
- authority:'arcane-os-sdk',
554
- location:sdkBrowserRuntimeReceipt.canonicalLocation,
555
- manifestSha256:sdkBrowserRuntimeReceipt.manifestSha256,
556
- contentSha256:sdkBrowserRuntimeReceipt.contentSha256,
557
- source:sdkBrowserRuntimeReceipt.source
558
- })
559
- }),
560
- files:Object.freeze(inventory),
561
- fileCount:inventory.length,
562
- totalBytes,
563
- contentSha256:createHash('sha256')
564
- .update(JSON.stringify(contentInventory))
565
- .digest('hex'),
566
- directoryIdentities:Object.freeze(verified.directoryIdentities),
567
- identities:Object.freeze(verified.identities)
568
- });
569
- issuedReceipts.add(receipt);
570
- await emit(onEvent,{
571
- type:'workspace.runtime.verify.completed',
572
- sourceContentSha256:receipt.sourceContentSha256,
573
- sourceBrowserContentSha256:receipt.sourceBrowserContentSha256,
574
- contentSha256:receipt.contentSha256,
575
- fileCount:receipt.fileCount,
576
- totalBytes:receipt.totalBytes
577
- });
578
- return receipt;
73
+ async function removeTemporaryTree(location){
74
+ await rm(location,{recursive:true,force:true}).catch(()=>{});
579
75
  }
580
76
 
581
- export async function materializeWorkspaceRuntime({
77
+ export async function materializeWorkspaceRuntimeContent({
582
78
  workspaceRoot,
583
79
  runtimeRoot=path.join(getSdkRoot(),'runtime'),
584
- runtimeReceipt,
585
80
  browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
586
- sdkBrowserRuntimeReceipt,
587
81
  signal,
588
82
  onEvent
589
83
  }={}){
590
84
  if(!workspaceRoot)fail('workspaceRoot is required to materialize a workspace runtime.');
591
- await authenticateRuntimeReceipt(runtimeReceipt,{runtimeRoot,signal});
592
- await authenticateSdkBrowserRuntimeReceipt(sdkBrowserRuntimeReceipt,{
593
- browserRuntimeRoot,
594
- signal
595
- });
596
- const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
597
- const workspace=await workspaceLocation(workspaceRoot);
598
- const destinationRoot=path.join(workspace.canonicalRoot,'arcane');
599
- try{
600
- const existing=await lstat(destinationRoot,{bigint:true});
601
- if(existing.isSymbolicLink()||!existing.isDirectory()){
602
- fail('Existing workspace arcane runtime path must be a real directory.');
603
- }
604
- await emit(onEvent,{type:'workspace.runtime.materialize.reused'});
605
- return verifyWorkspaceRuntime({
606
- workspaceRoot,
607
- runtimeRoot,
608
- runtimeReceipt,
609
- browserRuntimeRoot,
610
- sdkBrowserRuntimeReceipt,
611
- signal,
612
- onEvent
613
- });
614
- }catch(error){
615
- if(error?.code!=='ENOENT')throw error;
616
- }
617
-
618
- const stagingRoot=path.join(
619
- workspace.canonicalRoot,
620
- `${STAGING_PREFIX}${String(process.pid)}-${randomUUID()}`
621
- );
622
- await mkdir(stagingRoot,{mode:0o700});
623
- const stagingInfo=await lstat(stagingRoot,{bigint:true});
624
- if(stagingInfo.isSymbolicLink()||!stagingInfo.isDirectory()){
625
- fail('Workspace runtime staging path must be a real directory.');
626
- }
627
- const stagingIdentity=fileIdentity(stagingInfo);
628
- await assertRealDirectoryLocation(
629
- workspace.canonicalRoot,
630
- workspace.identity,
631
- 'Workspace root'
632
- );
633
- await assertRealDirectoryLocation(
634
- stagingRoot,
635
- stagingIdentity,
636
- 'Workspace runtime staging directory'
85
+ throwIfAborted(signal);
86
+ const workspace=await realDirectory(workspaceRoot,'Workspace root');
87
+ const runtime=await realDirectory(runtimeRoot,'SDK runtime root');
88
+ const browserRuntime=await realDirectory(browserRuntimeRoot,'SDK browser runtime root');
89
+ const runtimeArcane=await realDirectory(path.join(runtime,'arcane'),'SDK Arcane runtime');
90
+ const runtimeStrongType=await realDirectory(
91
+ path.join(runtime,'strong-type'),
92
+ 'SDK strong-type runtime'
637
93
  );
638
- const bufferedEvents=[];
639
- let stagingCleaned=false;
94
+ const destinationRoot=path.join(workspace,'arcane');
95
+ const token=randomUUID();
96
+ const stagingRoot=path.join(workspace,`${STAGING_PREFIX}${token}`);
97
+ const backupRoot=path.join(workspace,`${BACKUP_PREFIX}${token}`);
98
+ let backedUp=false;
99
+ let promoted=false;
100
+
101
+ await mkdir(stagingRoot);
640
102
  try{
641
- const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
642
- bufferedEvents.push({
643
- type:'workspace.runtime.materialize.started',
644
- fileCount:expectedFiles.length,
645
- totalBytes
646
- });
647
- let writtenBytes=0;
648
- for(const [index,file] of expectedFiles.entries()){
103
+ await copyCompleteEntry(runtimeArcane,stagingRoot,'SDK Arcane runtime',signal);
104
+ await mkdir(path.join(stagingRoot,'dependencies'),{recursive:true});
105
+ await copyCompleteEntry(
106
+ runtimeStrongType,
107
+ path.join(stagingRoot,'dependencies','strong-type'),
108
+ 'SDK strong-type runtime',
109
+ signal
110
+ );
111
+ const sdkDestination=path.join(stagingRoot,'sdk');
112
+ await mkdir(sdkDestination,{recursive:true});
113
+ const browserEntries=await readdir(browserRuntime,{withFileTypes:true});
114
+ browserEntries.sort((left,right)=>compareText(left.name,right.name));
115
+ for(const entry of browserEntries){
649
116
  throwIfAborted(signal);
650
- const bytes=file.authority==='sdk-browser-runtime'
651
- ?await readVerifiedSdkBrowserRuntimeFile(sdkBrowserRuntimeReceipt,{
652
- browserRuntimeRoot,
653
- relativePath:file.sourcePath,
654
- signal
655
- })
656
- :await readVerifiedRuntimeFile(runtimeReceipt,{
657
- runtimeRoot,
658
- relativePath:file.sourcePath,
659
- signal
660
- });
661
- const destination=resolveContained(stagingRoot,file.path);
662
- await mkdir(path.dirname(destination),{recursive:true,mode:0o755});
663
- await writeNewFile(destination,bytes);
664
- writtenBytes+=bytes.length;
665
- bufferedEvents.push({
666
- type:'workspace.runtime.materialize.progress',
667
- current:index+1,
668
- total:expectedFiles.length,
669
- writtenBytes,
670
- totalBytes,
671
- path:file.path
672
- });
117
+ await copyCompleteEntry(
118
+ path.join(browserRuntime,entry.name),
119
+ path.join(sdkDestination,entry.name),
120
+ `SDK browser runtime/${entry.name}`,
121
+ signal
122
+ );
673
123
  }
674
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
675
-
676
- // Materialization callbacks are held until every staged byte authenticates.
677
- // They can still veto the commit, but no callback runs between staged writes.
678
- for(const event of bufferedEvents)await emit(onEvent,event);
679
- throwIfAborted(signal);
680
- await assertRealDirectoryLocation(
681
- workspace.canonicalRoot,
682
- workspace.identity,
683
- 'Workspace root'
684
- );
685
- await assertRealDirectoryLocation(
686
- stagingRoot,
687
- stagingIdentity,
688
- 'Workspace runtime staging directory'
689
- );
690
- await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
691
- await assertRealDirectoryLocation(
692
- workspace.canonicalRoot,
693
- workspace.identity,
694
- 'Workspace root'
695
- );
696
- await assertRealDirectoryLocation(
697
- stagingRoot,
698
- stagingIdentity,
699
- 'Workspace runtime staging directory'
700
- );
701
124
 
702
125
  throwIfAborted(signal);
703
126
  try{
704
- await rename(stagingRoot,destinationRoot);
705
- }catch(error){
706
- let destinationExists=false;
707
- try{
708
- const existing=await lstat(destinationRoot,{bigint:true});
709
- destinationExists=existing.isDirectory()&&!existing.isSymbolicLink();
710
- }catch(inspectError){
711
- if(inspectError?.code!=='ENOENT')throw inspectError;
127
+ const existing=await lstat(destinationRoot);
128
+ if(existing.isSymbolicLink()||!existing.isDirectory()){
129
+ fail('Workspace Arcane runtime destination must be a real directory when present.');
712
130
  }
713
- if(!destinationExists)throw error;
714
- await cleanupStaging(stagingRoot,workspace,stagingIdentity);
715
- stagingCleaned=true;
716
- await emit(onEvent,{type:'workspace.runtime.materialize.reused'});
717
- return await verifyWorkspaceRuntime({
718
- workspaceRoot,
719
- runtimeRoot,
720
- runtimeReceipt,
721
- browserRuntimeRoot,
722
- sdkBrowserRuntimeReceipt,
723
- signal,
724
- onEvent
725
- });
131
+ await rename(destinationRoot,backupRoot);
132
+ backedUp=true;
133
+ }catch(error){
134
+ if(error?.code!=='ENOENT')throw error;
726
135
  }
727
- await cleanupStaging(stagingRoot,workspace,stagingIdentity);
728
- stagingCleaned=true;
729
- const receipt=await verifyWorkspaceRuntime({
730
- workspaceRoot,
731
- runtimeRoot,
732
- runtimeReceipt,
733
- browserRuntimeRoot,
734
- sdkBrowserRuntimeReceipt,
735
- signal,
736
- onEvent
737
- });
136
+
137
+ await rename(stagingRoot,destinationRoot);
138
+ promoted=true;
738
139
  await emit(onEvent,{
739
- type:'workspace.runtime.materialize.completed',
740
- contentSha256:receipt.contentSha256,
741
- sourceContentSha256:receipt.sourceContentSha256,
742
- sourceBrowserContentSha256:receipt.sourceBrowserContentSha256,
743
- fileCount:receipt.fileCount,
744
- totalBytes:receipt.totalBytes
140
+ type:'workspace.runtime.materialized',
141
+ workspaceRoot:workspace,
142
+ runtimeRoot:destinationRoot
745
143
  });
746
- return receipt;
747
- }finally{
748
- if(!stagingCleaned)await cleanupStaging(stagingRoot,workspace,stagingIdentity);
749
- }
750
- }
751
-
752
- export async function authenticateWorkspaceRuntimeReceipt(receipt,{
753
- workspaceRoot,
754
- signal
755
- }={}){
756
- if(!receipt||!issuedReceipts.has(receipt)){
757
- fail('Workspace runtime verification receipt was not issued by this SDK process.');
758
- }
759
- if(!workspaceRoot)fail('workspaceRoot is required to authenticate a workspace runtime receipt.');
760
- return assertWorkspaceRuntimeState(receipt,{workspaceRoot,signal});
761
- }
762
-
763
- export async function readVerifiedWorkspaceRuntimeFile(receipt,{
764
- workspaceRoot,
765
- relativePath,
766
- signal
767
- }={}){
768
- if(!receipt||!issuedReceipts.has(receipt)){
769
- fail('Workspace runtime verification receipt was not issued by this SDK process.');
770
- }
771
- if(!workspaceRoot)fail('workspaceRoot is required to read a verified workspace runtime file.');
772
- throwIfAborted(signal);
773
- const normalized=safeRelativePath(relativePath);
774
- const file=receipt.files.find(candidate=>portableKey(candidate.path)===portableKey(normalized));
775
- if(!file)fail(`Path is not in the verified workspace runtime inventory: ${normalized}.`);
776
- if(file.bytes>MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES){
777
- fail(
778
- `Verified workspace runtime file exceeds the ${MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES}-byte serving limit: ${file.path}.`
779
- );
780
- }
781
- const identity=receipt.identities.find(
782
- candidate=>portableKey(candidate.path)===portableKey(file.path)
783
- );
784
- if(!identity)fail(`Verified workspace runtime identity is missing for ${file.path}.`);
785
-
786
- const {root}=await assertRequestedWorkspace(receipt,workspaceRoot);
787
- const rootInfo=await lstat(root,{bigint:true});
788
- if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
789
- ||!identityMatches(rootInfo,receipt.rootIdentity)){
790
- fail('Workspace arcane runtime root changed after verification.');
791
- }
792
- const ancestors=[];
793
- let current='';
794
- for(const part of file.path.split('/').slice(0,-1)){
795
- current=current?`${current}/${part}`:part;
796
- ancestors.push(current);
797
- }
798
- for(const ancestor of ancestors){
799
- const directory=receipt.directoryIdentities.find(entry=>entry.path===ancestor);
800
- if(!directory)fail(`Verified workspace runtime directory identity is missing for ${ancestor}.`);
801
- await assertIdentityAt(root,directory,{directory:true});
802
- }
803
- await assertIdentityAt(root,identity,{directory:false});
804
-
805
- const filePath=resolveContained(root,file.path);
806
- let handle;
807
- try{
808
- handle=await open(filePath,READ_ONLY_NO_FOLLOW);
144
+ if(backedUp){
145
+ await rm(backupRoot,{recursive:true});
146
+ backedUp=false;
147
+ }
148
+ return {
149
+ kind:'arcane-workspace-runtime-content',
150
+ workspaceRoot:workspace,
151
+ runtimeRoot:destinationRoot
152
+ };
809
153
  }catch(error){
810
- if(error?.code==='ELOOP')fail(`Workspace runtime file became a symbolic link: ${file.path}.`);
811
- throw error;
812
- }
813
- try{
814
- const opened=await handle.stat({bigint:true});
815
- if(!opened.isFile()||!identityMatches(opened,identity)){
816
- fail(`Workspace runtime file changed while it was being opened: ${file.path}.`);
817
- }
818
- const bytes=await handle.readFile();
819
- throwIfAborted(signal);
820
- const after=await handle.stat({bigint:true});
821
- if(!identityMatches(after,identity)||bytes.length!==file.bytes){
822
- fail(`Workspace runtime file changed while it was being read: ${file.path}.`);
823
- }
824
- if(createHash('sha256').update(bytes).digest('hex')!==file.sha256){
825
- fail(`Workspace runtime file hash changed: ${file.path}.`);
826
- }
827
- await assertIdentityAt(root,identity,{directory:false});
828
- for(const ancestor of ancestors){
829
- const directory=receipt.directoryIdentities.find(entry=>entry.path===ancestor);
830
- await assertIdentityAt(root,directory,{directory:true});
831
- }
832
- const canonicalFile=await realpath(filePath);
833
- const canonicalRelative=path.relative(root,canonicalFile);
834
- if(canonicalRelative.startsWith('..')||path.isAbsolute(canonicalRelative)){
835
- fail(`Workspace runtime path left its root: ${file.path}.`);
154
+ if(promoted)await removeTemporaryTree(destinationRoot);
155
+ if(backedUp){
156
+ try{await rename(backupRoot,destinationRoot);}
157
+ catch(rollbackError){
158
+ throw new AggregateError(
159
+ [error,rollbackError],
160
+ 'Workspace runtime materialization and rollback both failed.',
161
+ {cause:error}
162
+ );
163
+ }
836
164
  }
837
- return bytes;
165
+ throw error;
838
166
  }finally{
839
- await handle.close();
167
+ await removeTemporaryTree(stagingRoot);
168
+ if(!backedUp||promoted)await removeTemporaryTree(backupRoot);
840
169
  }
841
170
  }
171
+
172
+ export async function materializeWorkspaceRuntime(options={}){
173
+ return materializeWorkspaceRuntimeContent(options);
174
+ }