arcane-os 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +11 -11
- package/browser-runtime/ai/browser-speech-artifacts.mjs +50 -32
- package/browser-runtime/ai/browser-speech-providers.mjs +77 -9
- package/browser-runtime/ai/speech-worker-client.mjs +6 -4
- package/browser-runtime/ai/speech-worker-runtime.mjs +20 -8
- package/docs/architecture.md +2 -2
- package/docs/reference/README.md +12 -14
- package/docs/reference/ai/browser-speech-package-authority.json +1 -1
- package/docs/reference/ai/browser-speech.md +113 -70
- package/docs/reference/ai/browser-wasm.md +1 -1
- package/docs/reference/availability-and-normalization.md +2 -2
- package/docs/reference/cli.md +1 -1
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +1 -1
- package/docs/reference/mail.md +1 -1
- package/docs/reference/protocols.md +55 -13
- package/docs/reference/runtime-modules.md +6 -1
- package/docs/reference/sdk-api.md +14 -10
- package/package.json +1 -1
- package/runtime/ARCANE_RUNTIME_RELEASE.json +7 -7
- package/runtime/arcane/modules/AI.js +8 -0
- package/runtime/arcane/modules/AIProviderRuntime.js +40 -4
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/installed-sdk-runtime.mjs +46 -28
- package/src/workspace-runtime.mjs +804 -22
|
@@ -25,7 +25,18 @@ const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
|
25
25
|
const CREATE_NEW_NO_FOLLOW=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL
|
|
26
26
|
|FS_CONSTANTS.O_WRONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
27
27
|
const MAX_VERIFIED_WORKSPACE_RUNTIME_FILE_BYTES=64*1024*1024;
|
|
28
|
+
const MAX_WORKSPACE_RUNTIME_RECEIPT_BYTES=4*1024*1024;
|
|
28
29
|
const STAGING_PREFIX='.arcane-runtime-stage-';
|
|
30
|
+
const BACKUP_PREFIX='.arcane-runtime-backup-';
|
|
31
|
+
const MATERIALIZATION_RECEIPT_DIRECTORY='.arcane';
|
|
32
|
+
const MATERIALIZATION_RECEIPT_NAME='installed-sdk-runtime.json';
|
|
33
|
+
const RECEIPT_STAGING_PREFIX='.installed-sdk-runtime-stage-';
|
|
34
|
+
const RECEIPT_BACKUP_PREFIX='.installed-sdk-runtime-backup-';
|
|
35
|
+
const MATERIALIZATION_RECEIPT_KIND='arcane-installed-sdk-runtime-projection';
|
|
36
|
+
const MATERIALIZATION_RECEIPT_PATH=
|
|
37
|
+
`${MATERIALIZATION_RECEIPT_DIRECTORY}/${MATERIALIZATION_RECEIPT_NAME}`;
|
|
38
|
+
const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
|
|
39
|
+
const UUID_PATTERN=/^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u;
|
|
29
40
|
const issuedReceipts=new WeakSet();
|
|
30
41
|
|
|
31
42
|
function fail(message,code='ARCANE_WORKSPACE_RUNTIME_INTEGRITY_FAILED'){
|
|
@@ -51,6 +62,33 @@ function compareText(left,right){
|
|
|
51
62
|
return a<b?-1:a>b?1:0;
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
function isRecord(value){
|
|
66
|
+
return value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function exactKeys(value,keys){
|
|
70
|
+
return isRecord(value)
|
|
71
|
+
&&Object.keys(value).sort(compareText).join('\0')===[...keys].sort(compareText).join('\0');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function canonicalJson(value){
|
|
75
|
+
return `${JSON.stringify(value,null,2)}\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function deepFreeze(value){
|
|
79
|
+
if(!value||typeof value!=='object'||Object.isFrozen(value))return value;
|
|
80
|
+
for(const child of Object.values(value))deepFreeze(child);
|
|
81
|
+
return Object.freeze(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inventoryContentSha256(files){
|
|
85
|
+
return createHash('sha256').update(JSON.stringify(files.map(file=>({
|
|
86
|
+
path:file.path,
|
|
87
|
+
bytes:file.bytes,
|
|
88
|
+
sha256:file.sha256
|
|
89
|
+
})))).digest('hex');
|
|
90
|
+
}
|
|
91
|
+
|
|
54
92
|
function safeRelativePath(value){
|
|
55
93
|
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
56
94
|
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
@@ -164,6 +202,220 @@ function locationIdentityMatches(info,identity){
|
|
|
164
202
|
return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
|
|
165
203
|
}
|
|
166
204
|
|
|
205
|
+
function locationIdentity(identity){
|
|
206
|
+
return Object.freeze({device:identity.device,inode:identity.inode});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertPersistentIdentity(value,label,{locationOnly=false}={}){
|
|
210
|
+
const keys=locationOnly
|
|
211
|
+
?['device','inode']
|
|
212
|
+
:['device','inode','bytes','modifiedNanoseconds','changedNanoseconds','links'];
|
|
213
|
+
if(!exactKeys(value,keys)
|
|
214
|
+
||!keys.filter(key=>key!=='bytes')
|
|
215
|
+
.every(key=>typeof value[key]==='string'&&/^[0-9]+$/u.test(value[key]))
|
|
216
|
+
||(!locationOnly&&(!Number.isSafeInteger(value.bytes)||value.bytes<0))){
|
|
217
|
+
fail(`Installed SDK runtime receipt ${label} is invalid.`,
|
|
218
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertPersistentSourceReceipt(value,label,{browser=false}={}){
|
|
223
|
+
const keys=[
|
|
224
|
+
'canonicalLocation','rootIdentity','manifestPath','manifestSha256',
|
|
225
|
+
'manifestIdentity','sdkVersion','contentSha256','fileCount','totalBytes'
|
|
226
|
+
];
|
|
227
|
+
if(browser)keys.push('builder');
|
|
228
|
+
if(!exactKeys(value,keys)
|
|
229
|
+
||typeof value.canonicalLocation!=='string'||!path.isAbsolute(value.canonicalLocation)
|
|
230
|
+
||typeof value.manifestPath!=='string'||!path.isAbsolute(value.manifestPath)
|
|
231
|
+
||!SHA256_PATTERN.test(value.manifestSha256)
|
|
232
|
+
||typeof value.sdkVersion!=='string'||!value.sdkVersion
|
|
233
|
+
||!SHA256_PATTERN.test(value.contentSha256)
|
|
234
|
+
||!Number.isSafeInteger(value.fileCount)||value.fileCount<1
|
|
235
|
+
||!Number.isSafeInteger(value.totalBytes)||value.totalBytes<1
|
|
236
|
+
||(browser&&(typeof value.builder!=='string'||!value.builder))){
|
|
237
|
+
fail(`Installed SDK runtime receipt ${label} is invalid.`,
|
|
238
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
239
|
+
}
|
|
240
|
+
assertPersistentIdentity(value.rootIdentity,`${label} root identity`);
|
|
241
|
+
assertPersistentIdentity(value.manifestIdentity,`${label} manifest identity`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validatePersistentMaterializationReceipt(document){
|
|
245
|
+
if(!exactKeys(document,[
|
|
246
|
+
'schemaVersion','kind','generation','workspace','receiptPath',
|
|
247
|
+
'installedPackage','runtimeReceipt','sdkBrowserRuntimeReceipt','projection'
|
|
248
|
+
])||document.schemaVersion!==1||document.kind!==MATERIALIZATION_RECEIPT_KIND
|
|
249
|
+
||!UUID_PATTERN.test(document.generation)
|
|
250
|
+
||document.receiptPath!==MATERIALIZATION_RECEIPT_PATH){
|
|
251
|
+
fail('Installed SDK runtime receipt envelope is invalid.',
|
|
252
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
253
|
+
}
|
|
254
|
+
if(!exactKeys(document.workspace,['canonicalLocation','identity'])
|
|
255
|
+
||typeof document.workspace.canonicalLocation!=='string'
|
|
256
|
+
||!path.isAbsolute(document.workspace.canonicalLocation)){
|
|
257
|
+
fail('Installed SDK runtime receipt workspace is invalid.',
|
|
258
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
259
|
+
}
|
|
260
|
+
assertPersistentIdentity(document.workspace.identity,'workspace identity',{locationOnly:true});
|
|
261
|
+
const installed=document.installedPackage;
|
|
262
|
+
if(!exactKeys(installed,[
|
|
263
|
+
'dependencyName','dependencyGroup','specifier','packageSource','canonicalLocation',
|
|
264
|
+
'rootIdentity','packageName','packageVersion'
|
|
265
|
+
])||![
|
|
266
|
+
installed.dependencyName,installed.dependencyGroup,installed.specifier,
|
|
267
|
+
installed.packageSource,installed.canonicalLocation,installed.packageName,
|
|
268
|
+
installed.packageVersion
|
|
269
|
+
].every(value=>typeof value==='string'&&value)
|
|
270
|
+
||!path.isAbsolute(installed.canonicalLocation)){
|
|
271
|
+
fail('Installed SDK runtime receipt package identity is invalid.',
|
|
272
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
273
|
+
}
|
|
274
|
+
safeRelativePath(installed.packageSource);
|
|
275
|
+
assertPersistentIdentity(installed.rootIdentity,'installed package root identity');
|
|
276
|
+
assertPersistentSourceReceipt(document.runtimeReceipt,'runtime source receipt');
|
|
277
|
+
assertPersistentSourceReceipt(
|
|
278
|
+
document.sdkBrowserRuntimeReceipt,
|
|
279
|
+
'browser runtime source receipt',
|
|
280
|
+
{browser:true}
|
|
281
|
+
);
|
|
282
|
+
if(document.runtimeReceipt.sdkVersion!==installed.packageVersion
|
|
283
|
+
||document.sdkBrowserRuntimeReceipt.sdkVersion!==installed.packageVersion
|
|
284
|
+
||path.resolve(installed.canonicalLocation,'runtime')
|
|
285
|
+
!==path.resolve(document.runtimeReceipt.canonicalLocation)
|
|
286
|
+
||path.resolve(installed.canonicalLocation,'browser-runtime')
|
|
287
|
+
!==path.resolve(document.sdkBrowserRuntimeReceipt.canonicalLocation)){
|
|
288
|
+
fail('Installed SDK runtime receipt source binding is inconsistent.',
|
|
289
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
290
|
+
}
|
|
291
|
+
const projection=document.projection;
|
|
292
|
+
if(!exactKeys(projection,[
|
|
293
|
+
'relativePath','fileCount','totalBytes','contentSha256','files'
|
|
294
|
+
])||projection.relativePath!=='arcane'||!Array.isArray(projection.files)
|
|
295
|
+
||!Number.isSafeInteger(projection.fileCount)||projection.fileCount<1
|
|
296
|
+
||!Number.isSafeInteger(projection.totalBytes)||projection.totalBytes<1
|
|
297
|
+
||!SHA256_PATTERN.test(projection.contentSha256)
|
|
298
|
+
||projection.fileCount!==projection.files.length){
|
|
299
|
+
fail('Installed SDK runtime receipt projection is invalid.',
|
|
300
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
301
|
+
}
|
|
302
|
+
const seen=new Set();
|
|
303
|
+
let totalBytes=0;
|
|
304
|
+
let previous='';
|
|
305
|
+
for(const [index,file] of projection.files.entries()){
|
|
306
|
+
if(!exactKeys(file,['path','sourcePath','authority','bytes','sha256'])
|
|
307
|
+
||!['arcane-runtime','sdk-browser-runtime'].includes(file.authority)
|
|
308
|
+
||!Number.isSafeInteger(file.bytes)||file.bytes<0
|
|
309
|
+
||!SHA256_PATTERN.test(file.sha256)){
|
|
310
|
+
fail(`Installed SDK runtime receipt projection file ${String(index)} is invalid.`,
|
|
311
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
312
|
+
}
|
|
313
|
+
const projectedPath=safeRelativePath(file.path);
|
|
314
|
+
safeRelativePath(file.sourcePath);
|
|
315
|
+
const key=portableKey(projectedPath);
|
|
316
|
+
if(seen.has(key)||(index>0&&compareText(previous,projectedPath)>=0)){
|
|
317
|
+
fail('Installed SDK runtime receipt projection inventory is not unique and sorted.',
|
|
318
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
319
|
+
}
|
|
320
|
+
seen.add(key);
|
|
321
|
+
previous=projectedPath;
|
|
322
|
+
totalBytes+=file.bytes;
|
|
323
|
+
if(!Number.isSafeInteger(totalBytes)){
|
|
324
|
+
fail('Installed SDK runtime receipt projection byte total is invalid.',
|
|
325
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if(totalBytes!==projection.totalBytes
|
|
329
|
+
||inventoryContentSha256(projection.files)!==projection.contentSha256){
|
|
330
|
+
fail('Installed SDK runtime receipt projection inventory digest is invalid.',
|
|
331
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
332
|
+
}
|
|
333
|
+
return document;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function persistentSourceReceipt(receipt,{browser=false}={}){
|
|
337
|
+
const result={
|
|
338
|
+
canonicalLocation:receipt.canonicalLocation,
|
|
339
|
+
rootIdentity:receipt.rootIdentity,
|
|
340
|
+
manifestPath:receipt.manifestPath,
|
|
341
|
+
manifestSha256:receipt.manifestSha256,
|
|
342
|
+
manifestIdentity:receipt.manifestIdentity,
|
|
343
|
+
sdkVersion:receipt.sdkVersion,
|
|
344
|
+
contentSha256:receipt.contentSha256,
|
|
345
|
+
fileCount:receipt.fileCount,
|
|
346
|
+
totalBytes:receipt.totalBytes
|
|
347
|
+
};
|
|
348
|
+
if(browser)result.builder=receipt.builder;
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function createPersistentMaterializationReceipt({
|
|
353
|
+
generation,
|
|
354
|
+
workspace,
|
|
355
|
+
installedSdkAuthority,
|
|
356
|
+
runtimeReceipt,
|
|
357
|
+
sdkBrowserRuntimeReceipt,
|
|
358
|
+
expectedFiles
|
|
359
|
+
}){
|
|
360
|
+
const {declaration,installation}=installedSdkAuthority??{};
|
|
361
|
+
if(!declaration||!installation){
|
|
362
|
+
fail('Installed SDK authority is required for a persistent workspace projection receipt.');
|
|
363
|
+
}
|
|
364
|
+
const packageInfo=await lstat(installation.canonicalPackageRoot,{bigint:true});
|
|
365
|
+
if(packageInfo.isSymbolicLink()||!packageInfo.isDirectory()){
|
|
366
|
+
fail('Installed SDK package root must remain a real directory.');
|
|
367
|
+
}
|
|
368
|
+
const canonicalPackageRoot=await realpath(installation.canonicalPackageRoot);
|
|
369
|
+
if(canonicalPackageRoot!==installation.canonicalPackageRoot){
|
|
370
|
+
fail('Installed SDK package root changed while materialization was active.');
|
|
371
|
+
}
|
|
372
|
+
if(runtimeReceipt.sdkVersion!==installation.packageVersion
|
|
373
|
+
||sdkBrowserRuntimeReceipt.sdkVersion!==installation.packageVersion){
|
|
374
|
+
fail('Installed SDK package and source receipt versions do not match.');
|
|
375
|
+
}
|
|
376
|
+
const files=expectedFiles.map(file=>({
|
|
377
|
+
path:file.path,
|
|
378
|
+
sourcePath:file.sourcePath,
|
|
379
|
+
authority:file.authority,
|
|
380
|
+
bytes:file.bytes,
|
|
381
|
+
sha256:file.sha256
|
|
382
|
+
}));
|
|
383
|
+
const document={
|
|
384
|
+
schemaVersion:1,
|
|
385
|
+
kind:MATERIALIZATION_RECEIPT_KIND,
|
|
386
|
+
generation,
|
|
387
|
+
workspace:{
|
|
388
|
+
canonicalLocation:workspace.canonicalRoot,
|
|
389
|
+
identity:locationIdentity(workspace.identity)
|
|
390
|
+
},
|
|
391
|
+
receiptPath:MATERIALIZATION_RECEIPT_PATH,
|
|
392
|
+
installedPackage:{
|
|
393
|
+
dependencyName:declaration.dependencyName,
|
|
394
|
+
dependencyGroup:declaration.dependencyGroup,
|
|
395
|
+
specifier:declaration.specifier,
|
|
396
|
+
packageSource:declaration.packageSource,
|
|
397
|
+
canonicalLocation:canonicalPackageRoot,
|
|
398
|
+
rootIdentity:fileIdentity(packageInfo),
|
|
399
|
+
packageName:installation.packageName,
|
|
400
|
+
packageVersion:installation.packageVersion
|
|
401
|
+
},
|
|
402
|
+
runtimeReceipt:persistentSourceReceipt(runtimeReceipt),
|
|
403
|
+
sdkBrowserRuntimeReceipt:persistentSourceReceipt(
|
|
404
|
+
sdkBrowserRuntimeReceipt,
|
|
405
|
+
{browser:true}
|
|
406
|
+
),
|
|
407
|
+
projection:{
|
|
408
|
+
relativePath:'arcane',
|
|
409
|
+
fileCount:files.length,
|
|
410
|
+
totalBytes:files.reduce((total,file)=>total+file.bytes,0),
|
|
411
|
+
contentSha256:inventoryContentSha256(files),
|
|
412
|
+
files
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
validatePersistentMaterializationReceipt(document);
|
|
416
|
+
return deepFreeze(document);
|
|
417
|
+
}
|
|
418
|
+
|
|
167
419
|
async function workspaceLocation(workspaceRoot){
|
|
168
420
|
const requestedRoot=path.resolve(workspaceRoot);
|
|
169
421
|
let info;
|
|
@@ -420,13 +672,108 @@ async function writeNewFile(filePath,bytes){
|
|
|
420
672
|
}
|
|
421
673
|
}
|
|
422
674
|
|
|
423
|
-
async function
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
675
|
+
async function readPersistentMaterializationReceipt(receiptPath,{signal}={}){
|
|
676
|
+
throwIfAborted(signal);
|
|
677
|
+
let handle;
|
|
678
|
+
try{
|
|
679
|
+
handle=await open(receiptPath,READ_ONLY_NO_FOLLOW);
|
|
680
|
+
}catch(error){
|
|
681
|
+
if(error?.code==='ELOOP'){
|
|
682
|
+
fail('Installed SDK runtime receipt must not be a symbolic link.',
|
|
683
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
684
|
+
}
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
try{
|
|
688
|
+
const before=await handle.stat({bigint:true});
|
|
689
|
+
if(!before.isFile()||before.size>BigInt(MAX_WORKSPACE_RUNTIME_RECEIPT_BYTES)){
|
|
690
|
+
fail('Installed SDK runtime receipt is not a bounded regular file.',
|
|
691
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
692
|
+
}
|
|
693
|
+
const bytes=await handle.readFile();
|
|
694
|
+
throwIfAborted(signal);
|
|
695
|
+
const after=await handle.stat({bigint:true});
|
|
696
|
+
if(!sameIdentity(before,after)){
|
|
697
|
+
fail('Installed SDK runtime receipt changed while it was being read.',
|
|
698
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
699
|
+
}
|
|
700
|
+
let document;
|
|
701
|
+
try{document=JSON.parse(bytes.toString('utf8'));}
|
|
702
|
+
catch(error){
|
|
703
|
+
fail(`Installed SDK runtime receipt is not valid JSON: ${error.message}`,
|
|
704
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
705
|
+
}
|
|
706
|
+
validatePersistentMaterializationReceipt(document);
|
|
707
|
+
if(!bytes.equals(Buffer.from(canonicalJson(document)))){
|
|
708
|
+
fail('Installed SDK runtime receipt bytes are not canonical.',
|
|
709
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
710
|
+
}
|
|
711
|
+
const named=await lstat(receiptPath,{bigint:true});
|
|
712
|
+
if(named.isSymbolicLink()||!named.isFile()||!sameIdentity(named,after)){
|
|
713
|
+
fail('Installed SDK runtime receipt path changed while it was being read.',
|
|
714
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze({
|
|
717
|
+
document:deepFreeze(document),
|
|
718
|
+
identity:fileIdentity(after)
|
|
719
|
+
});
|
|
720
|
+
}finally{
|
|
721
|
+
await handle.close();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function assertOwnedFileLocation(filePath,identity,label){
|
|
726
|
+
const info=await lstat(filePath,{bigint:true});
|
|
727
|
+
if(info.isSymbolicLink()||!info.isFile()||!locationIdentityMatches(info,identity)){
|
|
728
|
+
fail(`${label} changed while workspace runtime materialization was active.`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
async function removeOwnedFile(filePath,identity,label){
|
|
733
|
+
await assertOwnedFileLocation(filePath,identity,label);
|
|
734
|
+
await rm(filePath,{force:true});
|
|
735
|
+
try{
|
|
736
|
+
await lstat(filePath,{bigint:true});
|
|
737
|
+
fail(`${label} remained after cleanup.`);
|
|
738
|
+
}catch(error){
|
|
739
|
+
if(error?.code!=='ENOENT')throw error;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function materializationMetadataLocation(workspace){
|
|
744
|
+
const metadataRoot=path.join(workspace.canonicalRoot,MATERIALIZATION_RECEIPT_DIRECTORY);
|
|
745
|
+
let info;
|
|
746
|
+
try{info=await lstat(metadataRoot,{bigint:true});}
|
|
747
|
+
catch(error){
|
|
748
|
+
if(error?.code!=='ENOENT')throw error;
|
|
749
|
+
await mkdir(metadataRoot,{mode:0o700});
|
|
750
|
+
info=await lstat(metadataRoot,{bigint:true});
|
|
751
|
+
}
|
|
752
|
+
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
753
|
+
fail('Workspace materialization metadata root must be a real directory.');
|
|
754
|
+
}
|
|
755
|
+
const canonical=await realpath(metadataRoot);
|
|
756
|
+
if(canonical!==metadataRoot){
|
|
757
|
+
fail('Workspace materialization metadata root must not resolve through another path.');
|
|
758
|
+
}
|
|
759
|
+
return Object.freeze({root:metadataRoot,identity:fileIdentity(info)});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function optionalPathIdentity(target){
|
|
763
|
+
try{return fileIdentity(await lstat(target,{bigint:true}));}
|
|
764
|
+
catch(error){
|
|
765
|
+
if(error?.code==='ENOENT')return null;
|
|
766
|
+
throw error;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function cleanupOwnedTree(ownedRoot,workspace,ownedIdentity,{prefix,label}){
|
|
771
|
+
const ownedParent=path.dirname(ownedRoot);
|
|
772
|
+
const ownedName=path.basename(ownedRoot);
|
|
773
|
+
if(ownedParent!==workspace.canonicalRoot||!ownedName.startsWith(prefix)){
|
|
427
774
|
fail(
|
|
428
|
-
`Refusing to clean an unowned workspace runtime
|
|
429
|
-
+`(parent ${
|
|
775
|
+
`Refusing to clean an unowned workspace runtime ${label} path: ${ownedRoot} `
|
|
776
|
+
+`(parent ${ownedParent}; expected ${workspace.canonicalRoot}).`
|
|
430
777
|
);
|
|
431
778
|
}
|
|
432
779
|
await assertRealDirectoryLocation(
|
|
@@ -434,24 +781,24 @@ async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
|
|
|
434
781
|
workspace.identity,
|
|
435
782
|
'Workspace root'
|
|
436
783
|
);
|
|
437
|
-
let
|
|
438
|
-
try{
|
|
784
|
+
let ownedInfo;
|
|
785
|
+
try{ownedInfo=await lstat(ownedRoot,{bigint:true});}
|
|
439
786
|
catch(error){
|
|
440
787
|
if(error?.code==='ENOENT')return;
|
|
441
788
|
throw error;
|
|
442
789
|
}
|
|
443
|
-
if(
|
|
444
|
-
||!locationIdentityMatches(
|
|
445
|
-
fail(
|
|
790
|
+
if(ownedInfo.isSymbolicLink()||!ownedInfo.isDirectory()
|
|
791
|
+
||!locationIdentityMatches(ownedInfo,ownedIdentity)){
|
|
792
|
+
fail(`Refusing to clean a workspace runtime ${label} path whose identity changed.`);
|
|
446
793
|
}
|
|
447
794
|
await assertRealDirectoryLocation(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
795
|
+
ownedRoot,
|
|
796
|
+
ownedIdentity,
|
|
797
|
+
`Workspace runtime ${label} directory`
|
|
451
798
|
);
|
|
452
|
-
try{await inspectTree(
|
|
799
|
+
try{await inspectTree(ownedRoot);}
|
|
453
800
|
catch(error){
|
|
454
|
-
fail(`Refusing to clean an unauthenticated workspace runtime
|
|
801
|
+
fail(`Refusing to clean an unauthenticated workspace runtime ${label} tree: ${error.message}`);
|
|
455
802
|
}
|
|
456
803
|
await assertRealDirectoryLocation(
|
|
457
804
|
workspace.canonicalRoot,
|
|
@@ -459,14 +806,14 @@ async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
|
|
|
459
806
|
'Workspace root'
|
|
460
807
|
);
|
|
461
808
|
await assertRealDirectoryLocation(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
809
|
+
ownedRoot,
|
|
810
|
+
ownedIdentity,
|
|
811
|
+
`Workspace runtime ${label} directory`
|
|
465
812
|
);
|
|
466
|
-
await rm(
|
|
813
|
+
await rm(ownedRoot,{recursive:true,force:true});
|
|
467
814
|
try{
|
|
468
|
-
await lstat(
|
|
469
|
-
fail(
|
|
815
|
+
await lstat(ownedRoot,{bigint:true});
|
|
816
|
+
fail(`Workspace runtime ${label} path remained after cleanup.`);
|
|
470
817
|
}catch(error){
|
|
471
818
|
if(error?.code!=='ENOENT')throw error;
|
|
472
819
|
}
|
|
@@ -477,6 +824,13 @@ async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
|
|
|
477
824
|
);
|
|
478
825
|
}
|
|
479
826
|
|
|
827
|
+
async function cleanupStaging(stagingRoot,workspace,stagingIdentity){
|
|
828
|
+
return cleanupOwnedTree(stagingRoot,workspace,stagingIdentity,{
|
|
829
|
+
prefix:STAGING_PREFIX,
|
|
830
|
+
label:'staging'
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
480
834
|
export async function verifyWorkspaceRuntime({
|
|
481
835
|
workspaceRoot,
|
|
482
836
|
runtimeRoot=path.join(getSdkRoot(),'runtime'),
|
|
@@ -578,12 +932,428 @@ export async function verifyWorkspaceRuntime({
|
|
|
578
932
|
return receipt;
|
|
579
933
|
}
|
|
580
934
|
|
|
935
|
+
function attachMaterialization(receipt,{
|
|
936
|
+
status,
|
|
937
|
+
persistentReceipt,
|
|
938
|
+
receiptPath,
|
|
939
|
+
cleanupWarnings=[]
|
|
940
|
+
}){
|
|
941
|
+
const result=Object.freeze({
|
|
942
|
+
...receipt,
|
|
943
|
+
materialization:Object.freeze({
|
|
944
|
+
status,
|
|
945
|
+
generation:persistentReceipt.generation,
|
|
946
|
+
receiptPath,
|
|
947
|
+
persistentReceipt,
|
|
948
|
+
cleanupWarnings:Object.freeze([...cleanupWarnings])
|
|
949
|
+
})
|
|
950
|
+
});
|
|
951
|
+
issuedReceipts.add(result);
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async function inspectInstalledMaterialization({
|
|
956
|
+
workspace,
|
|
957
|
+
destinationRoot,
|
|
958
|
+
receiptPath,
|
|
959
|
+
desiredReceipt,
|
|
960
|
+
signal
|
|
961
|
+
}){
|
|
962
|
+
const destinationIdentity=await optionalPathIdentity(destinationRoot);
|
|
963
|
+
const receiptIdentity=await optionalPathIdentity(receiptPath);
|
|
964
|
+
if(!destinationIdentity&&!receiptIdentity)return Object.freeze({kind:'absent'});
|
|
965
|
+
if(!destinationIdentity){
|
|
966
|
+
fail('Installed SDK runtime receipt exists without its workspace projection.',
|
|
967
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
968
|
+
}
|
|
969
|
+
const destinationInfo=await lstat(destinationRoot,{bigint:true});
|
|
970
|
+
if(destinationInfo.isSymbolicLink()||!destinationInfo.isDirectory()){
|
|
971
|
+
fail('Existing workspace arcane runtime path must be a real directory.');
|
|
972
|
+
}
|
|
973
|
+
const canonicalDestination=await realpath(destinationRoot);
|
|
974
|
+
if(canonicalDestination!==destinationRoot){
|
|
975
|
+
fail('Existing workspace arcane runtime path must not resolve through another path.');
|
|
976
|
+
}
|
|
977
|
+
if(!receiptIdentity){
|
|
978
|
+
await inspectTree(destinationRoot,{signal});
|
|
979
|
+
return Object.freeze({
|
|
980
|
+
kind:'legacy',
|
|
981
|
+
destinationIdentity:fileIdentity(destinationInfo)
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const recorded=await readPersistentMaterializationReceipt(receiptPath,{signal});
|
|
985
|
+
if(recorded.document.workspace.canonicalLocation!==workspace.canonicalRoot
|
|
986
|
+
||recorded.document.workspace.identity.device!==workspace.identity.device
|
|
987
|
+
||recorded.document.workspace.identity.inode!==workspace.identity.inode){
|
|
988
|
+
fail('Installed SDK runtime receipt belongs to a different workspace.',
|
|
989
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
990
|
+
}
|
|
991
|
+
if(canonicalJson(recorded.document)===canonicalJson(desiredReceipt)){
|
|
992
|
+
return Object.freeze({
|
|
993
|
+
kind:'current',
|
|
994
|
+
document:recorded.document,
|
|
995
|
+
destinationIdentity:fileIdentity(destinationInfo),
|
|
996
|
+
receiptIdentity:recorded.identity
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
await verifyProjectedTree(
|
|
1000
|
+
destinationRoot,
|
|
1001
|
+
recorded.document.projection.files,
|
|
1002
|
+
{signal}
|
|
1003
|
+
);
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
kind:'stale',
|
|
1006
|
+
document:recorded.document,
|
|
1007
|
+
destinationIdentity:fileIdentity(destinationInfo),
|
|
1008
|
+
receiptIdentity:recorded.identity
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function rollbackInstalledReplacement(state){
|
|
1013
|
+
const errors=[];
|
|
1014
|
+
const attempt=async operation=>{
|
|
1015
|
+
try{await operation();}
|
|
1016
|
+
catch(error){errors.push(error);}
|
|
1017
|
+
};
|
|
1018
|
+
if(state.newReceiptMoved){
|
|
1019
|
+
await attempt(()=>rename(state.receiptPath,state.receiptStagingPath));
|
|
1020
|
+
}
|
|
1021
|
+
if(state.newRootMoved){
|
|
1022
|
+
await attempt(()=>rename(state.destinationRoot,state.stagingRoot));
|
|
1023
|
+
}
|
|
1024
|
+
if(state.oldReceiptMoved){
|
|
1025
|
+
await attempt(()=>rename(state.receiptBackupPath,state.receiptPath));
|
|
1026
|
+
}
|
|
1027
|
+
if(state.oldRootMoved){
|
|
1028
|
+
await attempt(()=>rename(state.backupRoot,state.destinationRoot));
|
|
1029
|
+
}
|
|
1030
|
+
if(errors.length){
|
|
1031
|
+
throw new AggregateError(
|
|
1032
|
+
errors,
|
|
1033
|
+
'Installed SDK runtime replacement failed and its prior projection could not be fully restored.'
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
state.newReceiptMoved=false;
|
|
1037
|
+
state.newRootMoved=false;
|
|
1038
|
+
state.oldReceiptMoved=false;
|
|
1039
|
+
state.oldRootMoved=false;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function materializeInstalledWorkspaceRuntime({
|
|
1043
|
+
workspaceRoot,
|
|
1044
|
+
runtimeRoot,
|
|
1045
|
+
runtimeReceipt,
|
|
1046
|
+
browserRuntimeRoot,
|
|
1047
|
+
sdkBrowserRuntimeReceipt,
|
|
1048
|
+
installedSdkAuthority,
|
|
1049
|
+
signal,
|
|
1050
|
+
onEvent
|
|
1051
|
+
}){
|
|
1052
|
+
const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
|
|
1053
|
+
const workspace=await workspaceLocation(workspaceRoot);
|
|
1054
|
+
const metadata=await materializationMetadataLocation(workspace);
|
|
1055
|
+
const destinationRoot=path.join(workspace.canonicalRoot,'arcane');
|
|
1056
|
+
const receiptPath=path.join(metadata.root,MATERIALIZATION_RECEIPT_NAME);
|
|
1057
|
+
const generation=randomUUID();
|
|
1058
|
+
let desiredReceipt=await createPersistentMaterializationReceipt({
|
|
1059
|
+
generation,
|
|
1060
|
+
workspace,
|
|
1061
|
+
installedSdkAuthority,
|
|
1062
|
+
runtimeReceipt,
|
|
1063
|
+
sdkBrowserRuntimeReceipt,
|
|
1064
|
+
expectedFiles
|
|
1065
|
+
});
|
|
1066
|
+
let existingReceipt=null;
|
|
1067
|
+
try{
|
|
1068
|
+
existingReceipt=(await readPersistentMaterializationReceipt(receiptPath,{signal})).document;
|
|
1069
|
+
desiredReceipt=await createPersistentMaterializationReceipt({
|
|
1070
|
+
generation:existingReceipt.generation,
|
|
1071
|
+
workspace,
|
|
1072
|
+
installedSdkAuthority,
|
|
1073
|
+
runtimeReceipt,
|
|
1074
|
+
sdkBrowserRuntimeReceipt,
|
|
1075
|
+
expectedFiles
|
|
1076
|
+
});
|
|
1077
|
+
}catch(error){
|
|
1078
|
+
if(error?.code!=='ENOENT')throw error;
|
|
1079
|
+
}
|
|
1080
|
+
const existing=await inspectInstalledMaterialization({
|
|
1081
|
+
workspace,
|
|
1082
|
+
destinationRoot,
|
|
1083
|
+
receiptPath,
|
|
1084
|
+
desiredReceipt,
|
|
1085
|
+
signal
|
|
1086
|
+
});
|
|
1087
|
+
if(existing.kind==='current'){
|
|
1088
|
+
const verified=await verifyWorkspaceRuntime({
|
|
1089
|
+
workspaceRoot,
|
|
1090
|
+
runtimeRoot,
|
|
1091
|
+
runtimeReceipt,
|
|
1092
|
+
browserRuntimeRoot,
|
|
1093
|
+
sdkBrowserRuntimeReceipt,
|
|
1094
|
+
signal,
|
|
1095
|
+
onEvent
|
|
1096
|
+
});
|
|
1097
|
+
const result=attachMaterialization(verified,{
|
|
1098
|
+
status:'reused',
|
|
1099
|
+
persistentReceipt:existing.document,
|
|
1100
|
+
receiptPath
|
|
1101
|
+
});
|
|
1102
|
+
await emit(onEvent,{
|
|
1103
|
+
type:'workspace.runtime.materialize.reused',
|
|
1104
|
+
status:'reused',
|
|
1105
|
+
generation:existing.document.generation,
|
|
1106
|
+
receiptPath
|
|
1107
|
+
});
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
desiredReceipt=await createPersistentMaterializationReceipt({
|
|
1112
|
+
generation,
|
|
1113
|
+
workspace,
|
|
1114
|
+
installedSdkAuthority,
|
|
1115
|
+
runtimeReceipt,
|
|
1116
|
+
sdkBrowserRuntimeReceipt,
|
|
1117
|
+
expectedFiles
|
|
1118
|
+
});
|
|
1119
|
+
const operationSuffix=`${String(process.pid)}-${generation}`;
|
|
1120
|
+
const stagingRoot=path.join(workspace.canonicalRoot,`${STAGING_PREFIX}${operationSuffix}`);
|
|
1121
|
+
const backupRoot=path.join(workspace.canonicalRoot,`${BACKUP_PREFIX}${operationSuffix}`);
|
|
1122
|
+
const receiptStagingPath=path.join(
|
|
1123
|
+
metadata.root,
|
|
1124
|
+
`${RECEIPT_STAGING_PREFIX}${operationSuffix}.json`
|
|
1125
|
+
);
|
|
1126
|
+
const receiptBackupPath=path.join(
|
|
1127
|
+
metadata.root,
|
|
1128
|
+
`${RECEIPT_BACKUP_PREFIX}${operationSuffix}.json`
|
|
1129
|
+
);
|
|
1130
|
+
await mkdir(stagingRoot,{mode:0o700});
|
|
1131
|
+
const stagingInfo=await lstat(stagingRoot,{bigint:true});
|
|
1132
|
+
if(stagingInfo.isSymbolicLink()||!stagingInfo.isDirectory()){
|
|
1133
|
+
fail('Workspace runtime staging path must be a real directory.');
|
|
1134
|
+
}
|
|
1135
|
+
const stagingIdentity=fileIdentity(stagingInfo);
|
|
1136
|
+
const transaction={
|
|
1137
|
+
destinationRoot,
|
|
1138
|
+
stagingRoot,
|
|
1139
|
+
backupRoot,
|
|
1140
|
+
receiptPath,
|
|
1141
|
+
receiptStagingPath,
|
|
1142
|
+
receiptBackupPath,
|
|
1143
|
+
newReceiptMoved:false,
|
|
1144
|
+
newRootMoved:false,
|
|
1145
|
+
oldReceiptMoved:false,
|
|
1146
|
+
oldRootMoved:false
|
|
1147
|
+
};
|
|
1148
|
+
let stagingCleaned=false;
|
|
1149
|
+
let receiptStagingIdentity;
|
|
1150
|
+
let uncertainCommit=false;
|
|
1151
|
+
try{
|
|
1152
|
+
const totalBytes=expectedFiles.reduce((total,file)=>total+file.bytes,0);
|
|
1153
|
+
const bufferedEvents=[{
|
|
1154
|
+
type:'workspace.runtime.materialize.started',
|
|
1155
|
+
status:existing.kind==='absent'?'created':'refreshed',
|
|
1156
|
+
generation,
|
|
1157
|
+
fileCount:expectedFiles.length,
|
|
1158
|
+
totalBytes
|
|
1159
|
+
}];
|
|
1160
|
+
let writtenBytes=0;
|
|
1161
|
+
for(const [index,file] of expectedFiles.entries()){
|
|
1162
|
+
throwIfAborted(signal);
|
|
1163
|
+
const bytes=file.authority==='sdk-browser-runtime'
|
|
1164
|
+
?await readVerifiedSdkBrowserRuntimeFile(sdkBrowserRuntimeReceipt,{
|
|
1165
|
+
browserRuntimeRoot,
|
|
1166
|
+
relativePath:file.sourcePath,
|
|
1167
|
+
signal
|
|
1168
|
+
})
|
|
1169
|
+
:await readVerifiedRuntimeFile(runtimeReceipt,{
|
|
1170
|
+
runtimeRoot,
|
|
1171
|
+
relativePath:file.sourcePath,
|
|
1172
|
+
signal
|
|
1173
|
+
});
|
|
1174
|
+
const destination=resolveContained(stagingRoot,file.path);
|
|
1175
|
+
await mkdir(path.dirname(destination),{recursive:true,mode:0o755});
|
|
1176
|
+
await writeNewFile(destination,bytes);
|
|
1177
|
+
writtenBytes+=bytes.length;
|
|
1178
|
+
bufferedEvents.push({
|
|
1179
|
+
type:'workspace.runtime.materialize.progress',
|
|
1180
|
+
current:index+1,
|
|
1181
|
+
total:expectedFiles.length,
|
|
1182
|
+
writtenBytes,
|
|
1183
|
+
totalBytes,
|
|
1184
|
+
path:file.path
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
|
|
1188
|
+
await writeNewFile(receiptStagingPath,Buffer.from(canonicalJson(desiredReceipt)));
|
|
1189
|
+
const receiptStagingInfo=await lstat(receiptStagingPath,{bigint:true});
|
|
1190
|
+
if(receiptStagingInfo.isSymbolicLink()||!receiptStagingInfo.isFile()){
|
|
1191
|
+
fail('Workspace runtime staged receipt must be a real file.');
|
|
1192
|
+
}
|
|
1193
|
+
receiptStagingIdentity=fileIdentity(receiptStagingInfo);
|
|
1194
|
+
await readPersistentMaterializationReceipt(receiptStagingPath,{signal});
|
|
1195
|
+
|
|
1196
|
+
for(const event of bufferedEvents)await emit(onEvent,event);
|
|
1197
|
+
throwIfAborted(signal);
|
|
1198
|
+
await assertRealDirectoryLocation(
|
|
1199
|
+
workspace.canonicalRoot,
|
|
1200
|
+
workspace.identity,
|
|
1201
|
+
'Workspace root'
|
|
1202
|
+
);
|
|
1203
|
+
await assertRealDirectoryLocation(
|
|
1204
|
+
metadata.root,
|
|
1205
|
+
metadata.identity,
|
|
1206
|
+
'Workspace materialization metadata root'
|
|
1207
|
+
);
|
|
1208
|
+
await assertRealDirectoryLocation(
|
|
1209
|
+
stagingRoot,
|
|
1210
|
+
stagingIdentity,
|
|
1211
|
+
'Workspace runtime staging directory'
|
|
1212
|
+
);
|
|
1213
|
+
await assertOwnedFileLocation(
|
|
1214
|
+
receiptStagingPath,
|
|
1215
|
+
receiptStagingIdentity,
|
|
1216
|
+
'Workspace runtime staged receipt'
|
|
1217
|
+
);
|
|
1218
|
+
await verifyProjectedTree(stagingRoot,expectedFiles,{signal});
|
|
1219
|
+
if(existing.destinationIdentity){
|
|
1220
|
+
await assertRealDirectoryLocation(
|
|
1221
|
+
destinationRoot,
|
|
1222
|
+
existing.destinationIdentity,
|
|
1223
|
+
'Existing workspace runtime projection'
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
if(existing.receiptIdentity){
|
|
1227
|
+
await assertOwnedFileLocation(
|
|
1228
|
+
receiptPath,
|
|
1229
|
+
existing.receiptIdentity,
|
|
1230
|
+
'Existing workspace runtime receipt'
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
throwIfAborted(signal);
|
|
1234
|
+
|
|
1235
|
+
try{
|
|
1236
|
+
if(existing.destinationIdentity){
|
|
1237
|
+
await rename(destinationRoot,backupRoot);
|
|
1238
|
+
transaction.oldRootMoved=true;
|
|
1239
|
+
}
|
|
1240
|
+
if(existing.receiptIdentity){
|
|
1241
|
+
await rename(receiptPath,receiptBackupPath);
|
|
1242
|
+
transaction.oldReceiptMoved=true;
|
|
1243
|
+
}
|
|
1244
|
+
await rename(stagingRoot,destinationRoot);
|
|
1245
|
+
transaction.newRootMoved=true;
|
|
1246
|
+
await rename(receiptStagingPath,receiptPath);
|
|
1247
|
+
transaction.newReceiptMoved=true;
|
|
1248
|
+
}catch(commitError){
|
|
1249
|
+
try{await rollbackInstalledReplacement(transaction);}
|
|
1250
|
+
catch(rollbackError){
|
|
1251
|
+
uncertainCommit=true;
|
|
1252
|
+
throw new AggregateError(
|
|
1253
|
+
[commitError,rollbackError],
|
|
1254
|
+
'Installed SDK runtime replacement and rollback both failed.',
|
|
1255
|
+
{cause:commitError}
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
throw commitError;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
let committedPersistent;
|
|
1262
|
+
let verified;
|
|
1263
|
+
try{
|
|
1264
|
+
committedPersistent=(await readPersistentMaterializationReceipt(receiptPath)).document;
|
|
1265
|
+
if(canonicalJson(committedPersistent)!==canonicalJson(desiredReceipt)){
|
|
1266
|
+
fail('Committed installed SDK runtime receipt does not match the staged generation.',
|
|
1267
|
+
'ARCANE_WORKSPACE_RUNTIME_RECEIPT_INVALID');
|
|
1268
|
+
}
|
|
1269
|
+
verified=await verifyWorkspaceRuntime({
|
|
1270
|
+
workspaceRoot,
|
|
1271
|
+
runtimeRoot,
|
|
1272
|
+
runtimeReceipt,
|
|
1273
|
+
browserRuntimeRoot,
|
|
1274
|
+
sdkBrowserRuntimeReceipt
|
|
1275
|
+
});
|
|
1276
|
+
}catch(verificationError){
|
|
1277
|
+
try{await rollbackInstalledReplacement(transaction);}
|
|
1278
|
+
catch(rollbackError){
|
|
1279
|
+
uncertainCommit=true;
|
|
1280
|
+
throw new AggregateError(
|
|
1281
|
+
[verificationError,rollbackError],
|
|
1282
|
+
'Installed SDK runtime verification and rollback both failed.',
|
|
1283
|
+
{cause:verificationError}
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
throw verificationError;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const cleanupWarnings=[];
|
|
1290
|
+
if(existing.destinationIdentity){
|
|
1291
|
+
try{
|
|
1292
|
+
await cleanupOwnedTree(backupRoot,workspace,existing.destinationIdentity,{
|
|
1293
|
+
prefix:BACKUP_PREFIX,
|
|
1294
|
+
label:'backup'
|
|
1295
|
+
});
|
|
1296
|
+
transaction.oldRootMoved=false;
|
|
1297
|
+
}catch(error){cleanupWarnings.push(String(error?.message??error));}
|
|
1298
|
+
}
|
|
1299
|
+
if(existing.receiptIdentity){
|
|
1300
|
+
try{
|
|
1301
|
+
await removeOwnedFile(
|
|
1302
|
+
receiptBackupPath,
|
|
1303
|
+
existing.receiptIdentity,
|
|
1304
|
+
'Workspace runtime receipt backup'
|
|
1305
|
+
);
|
|
1306
|
+
transaction.oldReceiptMoved=false;
|
|
1307
|
+
}catch(error){cleanupWarnings.push(String(error?.message??error));}
|
|
1308
|
+
}
|
|
1309
|
+
transaction.newRootMoved=false;
|
|
1310
|
+
transaction.newReceiptMoved=false;
|
|
1311
|
+
stagingCleaned=true;
|
|
1312
|
+
const status=existing.kind==='absent'?'created':'refreshed';
|
|
1313
|
+
const result=attachMaterialization(verified,{
|
|
1314
|
+
status,
|
|
1315
|
+
persistentReceipt:committedPersistent,
|
|
1316
|
+
receiptPath,
|
|
1317
|
+
cleanupWarnings
|
|
1318
|
+
});
|
|
1319
|
+
await emit(onEvent,{
|
|
1320
|
+
type:'workspace.runtime.materialize.completed',
|
|
1321
|
+
status,
|
|
1322
|
+
generation,
|
|
1323
|
+
receiptPath,
|
|
1324
|
+
contentSha256:result.contentSha256,
|
|
1325
|
+
sourceContentSha256:result.sourceContentSha256,
|
|
1326
|
+
sourceBrowserContentSha256:result.sourceBrowserContentSha256,
|
|
1327
|
+
fileCount:result.fileCount,
|
|
1328
|
+
totalBytes:result.totalBytes,
|
|
1329
|
+
cleanupWarnings:Object.freeze([...cleanupWarnings])
|
|
1330
|
+
});
|
|
1331
|
+
return result;
|
|
1332
|
+
}finally{
|
|
1333
|
+
if(!uncertainCommit&&!stagingCleaned){
|
|
1334
|
+
await cleanupStaging(stagingRoot,workspace,stagingIdentity);
|
|
1335
|
+
if(receiptStagingIdentity){
|
|
1336
|
+
try{
|
|
1337
|
+
await removeOwnedFile(
|
|
1338
|
+
receiptStagingPath,
|
|
1339
|
+
receiptStagingIdentity,
|
|
1340
|
+
'Workspace runtime staged receipt'
|
|
1341
|
+
);
|
|
1342
|
+
}catch(error){
|
|
1343
|
+
if(error?.code!=='ENOENT')throw error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
581
1350
|
export async function materializeWorkspaceRuntime({
|
|
582
1351
|
workspaceRoot,
|
|
583
1352
|
runtimeRoot=path.join(getSdkRoot(),'runtime'),
|
|
584
1353
|
runtimeReceipt,
|
|
585
1354
|
browserRuntimeRoot=getSdkBrowserRuntimeRoot(),
|
|
586
1355
|
sdkBrowserRuntimeReceipt,
|
|
1356
|
+
installedSdkAuthority,
|
|
587
1357
|
signal,
|
|
588
1358
|
onEvent
|
|
589
1359
|
}={}){
|
|
@@ -593,6 +1363,18 @@ export async function materializeWorkspaceRuntime({
|
|
|
593
1363
|
browserRuntimeRoot,
|
|
594
1364
|
signal
|
|
595
1365
|
});
|
|
1366
|
+
if(installedSdkAuthority){
|
|
1367
|
+
return materializeInstalledWorkspaceRuntime({
|
|
1368
|
+
workspaceRoot,
|
|
1369
|
+
runtimeRoot,
|
|
1370
|
+
runtimeReceipt,
|
|
1371
|
+
browserRuntimeRoot,
|
|
1372
|
+
sdkBrowserRuntimeReceipt,
|
|
1373
|
+
installedSdkAuthority,
|
|
1374
|
+
signal,
|
|
1375
|
+
onEvent
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
596
1378
|
const expectedFiles=projectRuntimeFiles(runtimeReceipt,sdkBrowserRuntimeReceipt);
|
|
597
1379
|
const workspace=await workspaceLocation(workspaceRoot);
|
|
598
1380
|
const destinationRoot=path.join(workspace.canonicalRoot,'arcane');
|