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.
- package/CHANGELOG.md +27 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +887 -909
- package/browser-runtime/ai/browser-speech-providers.mjs +96 -152
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -146
- package/browser-runtime/ai/speech-worker-runtime.mjs +643 -363
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1050 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +658 -363
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +40 -62
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +112 -779
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -187
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1252
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -677
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2960
- package/docs/reference/sdk-api.md +0 -6694
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
|
25
|
-
const
|
|
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='
|
|
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(
|
|
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
|
|
55
|
-
|
|
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(
|
|
36
|
+
try{info=await lstat(requested);}
|
|
171
37
|
catch(error){
|
|
172
|
-
if(error?.code==='ENOENT')fail(
|
|
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
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
|
372
|
-
|
|
373
|
-
if(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
481
|
-
|
|
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
|
|
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
|
-
|
|
592
|
-
await
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
639
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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.
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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(
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
165
|
+
throw error;
|
|
838
166
|
}finally{
|
|
839
|
-
await
|
|
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
|
+
}
|