arcane-os 0.1.0-dev.5 → 0.1.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/NOTICE +10 -0
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +162 -0
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +69 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1151 -0
- package/browser-runtime/ai/browser-wasm.mjs +44 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +390 -0
- package/browser-runtime/ai/internal/sha256.mjs +166 -0
- package/browser-runtime/ai/model-controller.mjs +581 -0
- package/browser-runtime/ai/wllama/LICENCE +21 -0
- package/browser-runtime/ai/wllama/index.mjs +3494 -0
- package/browser-runtime/ai/wllama/llama.cpp-LICENSE +21 -0
- package/browser-runtime/ai/wllama/wllama.wasm +0 -0
- package/browser-runtime/dependencies/event-pubsub/index.js +141 -0
- package/browser-runtime/dependencies/event-pubsub/licence +21 -0
- package/browser-runtime/dependencies/event-pubsub/package.json +59 -0
- package/browser-runtime/dependencies/strong-type/index.js +1151 -0
- package/browser-runtime/dependencies/strong-type/licence +21 -0
- package/browser-runtime/dependencies/strong-type/package.json +61 -0
- package/browser-runtime/dom-event-instrumentation.mjs +594 -0
- package/browser-runtime/event-manager.mjs +1342 -0
- package/docs/publishing.md +65 -67
- package/docs/reference/README.md +2 -1
- package/docs/reference/cli.md +86 -3
- package/docs/reference/event-manager.md +20 -11
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/protocols.md +112 -14
- package/docs/reference/sdk-api.md +40 -11
- package/docs/work-amplification.md +4 -3
- package/package.json +15 -8
- package/runtime/ARCANE_RUNTIME_RELEASE.json +1 -1
- package/schemas/arcane-lock.schema.json +97 -1
- package/src/cli/main.mjs +5 -0
- package/src/dev-server.mjs +78 -34
- package/src/doctor.mjs +77 -3
- package/src/import-map.mjs +2352 -0
- package/src/packager/core.mjs +607 -29
- package/src/scaffold.mjs +122 -5
- package/src/sdk-browser-runtime.mjs +702 -0
- package/src/targets/index.mjs +31 -4
- package/src/templates/workspace-template.mjs +141 -23
- package/src/toolchain.mjs +288 -55
- package/src/workspace-operation-lock.mjs +716 -0
- package/src/workspace-runtime.mjs +841 -0
- package/src/workspace.mjs +292 -37
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
+
import {lstat,mkdir,open,realpath,rename,rm} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const LOCK_DIRECTORY='.arcane';
|
|
7
|
+
const LOCK_NAME='workspace-operation.lock.json';
|
|
8
|
+
const LOCK_TTL_MILLISECONDS=6*60*60*1000;
|
|
9
|
+
const MAX_LOCK_BYTES=64*1024;
|
|
10
|
+
const OPEN_EXCLUSIVE=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL|FS_CONSTANTS.O_RDWR
|
|
11
|
+
|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
12
|
+
const OPEN_READ=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
13
|
+
const RELEASE_PROCEDURE=
|
|
14
|
+
'The owning Arcane process removes this identity-checked lock after the operation settles.';
|
|
15
|
+
const STALE_RECOVERY=
|
|
16
|
+
'Only after expiresAt, confirm the recorded owner process is absent, preserve workspace '
|
|
17
|
+
+'changes, and remove this exact identity-checked lock. A live owner is never displaced.';
|
|
18
|
+
const SECURITY_BOUNDARY=
|
|
19
|
+
'This lock coordinates cooperative Arcane SDK mutators. Privileged or non-cooperating '
|
|
20
|
+
+'filesystem mutation is outside the portable JavaScript security boundary.';
|
|
21
|
+
const DOCUMENT_KEYS=Object.freeze([
|
|
22
|
+
'acquiredAt',
|
|
23
|
+
'expiresAt',
|
|
24
|
+
'kind',
|
|
25
|
+
'nonce',
|
|
26
|
+
'operation',
|
|
27
|
+
'owner',
|
|
28
|
+
'releaseProcedure',
|
|
29
|
+
'schemaVersion',
|
|
30
|
+
'scope',
|
|
31
|
+
'securityBoundary',
|
|
32
|
+
'staleRecovery',
|
|
33
|
+
'ttlMilliseconds'
|
|
34
|
+
].sort());
|
|
35
|
+
const OWNER_KEYS=Object.freeze(['pid']);
|
|
36
|
+
const activeRoots=new Map();
|
|
37
|
+
const leaseStates=new WeakMap();
|
|
38
|
+
|
|
39
|
+
function fail(message,code='ARCANE_WORKSPACE_BUSY'){
|
|
40
|
+
const error=new Error(message);
|
|
41
|
+
error.code=code;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function workspaceBusy(message){
|
|
46
|
+
const error=new Error(message);
|
|
47
|
+
error.code='ARCANE_WORKSPACE_BUSY';
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function trackedLeaseState(fields){
|
|
52
|
+
let resolveSettled;
|
|
53
|
+
const settled=new Promise(resolve=>{resolveSettled=resolve;});
|
|
54
|
+
return {
|
|
55
|
+
...fields,
|
|
56
|
+
releasePromise:null,
|
|
57
|
+
settled,
|
|
58
|
+
settledComplete:false,
|
|
59
|
+
resolveSettled
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function settleLeaseState(state){
|
|
64
|
+
if(state.settledComplete)return;
|
|
65
|
+
state.settledComplete=true;
|
|
66
|
+
state.resolveSettled();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function throwIfAborted(signal){
|
|
70
|
+
if(!signal?.aborted)return;
|
|
71
|
+
const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
|
|
72
|
+
error.code=error.code||'ARCANE_CANCELLED';
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sameLocation(left,right){
|
|
77
|
+
return left.dev===right.dev&&left.ino===right.ino;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sameFileState(left,right){
|
|
81
|
+
return sameLocation(left,right)&&left.size===right.size
|
|
82
|
+
&&left.mtimeNs===right.mtimeNs&&left.ctimeNs===right.ctimeNs
|
|
83
|
+
&&left.nlink===right.nlink&&left.mode===right.mode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function samePath(left,right){
|
|
87
|
+
const normalize=value=>{
|
|
88
|
+
const resolved=path.resolve(value);
|
|
89
|
+
return process.platform==='win32'?resolved.toLocaleLowerCase('en-US'):resolved;
|
|
90
|
+
};
|
|
91
|
+
return normalize(left)===normalize(right);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function exactKeys(value,expected){
|
|
95
|
+
return value!==null&&typeof value==='object'&&!Array.isArray(value)
|
|
96
|
+
&&JSON.stringify(Object.keys(value).sort())===JSON.stringify(expected);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function captureRealDirectoryChain(directory,label){
|
|
100
|
+
const resolved=path.resolve(directory);
|
|
101
|
+
let requestedIdentity;
|
|
102
|
+
try{requestedIdentity=await lstat(resolved,{bigint:true});}
|
|
103
|
+
catch(error){
|
|
104
|
+
if(error?.code==='ENOENT'){
|
|
105
|
+
fail(`${label} does not exist.`, 'ARCANE_POLICY_DENIED');
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
if(requestedIdentity.isSymbolicLink()||!requestedIdentity.isDirectory()){
|
|
110
|
+
fail(`${label} must be a physical directory, not a symbolic link or junction.`,
|
|
111
|
+
'ARCANE_POLICY_DENIED');
|
|
112
|
+
}
|
|
113
|
+
const canonical=await realpath(resolved);
|
|
114
|
+
const root=path.parse(canonical).root;
|
|
115
|
+
const relative=path.relative(root,canonical);
|
|
116
|
+
const segments=relative===''?[]:relative.split(path.sep).filter(Boolean);
|
|
117
|
+
const records=[];
|
|
118
|
+
let current=root;
|
|
119
|
+
for(const segment of [null,...segments]){
|
|
120
|
+
if(segment!==null)current=path.join(current,segment);
|
|
121
|
+
let info;
|
|
122
|
+
try{info=await lstat(current,{bigint:true});}
|
|
123
|
+
catch(error){
|
|
124
|
+
if(error?.code==='ENOENT'){
|
|
125
|
+
fail(`${label} does not exist.`, 'ARCANE_POLICY_DENIED');
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if(info.isSymbolicLink()||!info.isDirectory()){
|
|
130
|
+
fail(`${label} must not contain a symbolic link, junction, or non-directory ancestor.`,
|
|
131
|
+
'ARCANE_POLICY_DENIED');
|
|
132
|
+
}
|
|
133
|
+
records.push(Object.freeze({path:current,identity:info}));
|
|
134
|
+
}
|
|
135
|
+
const identity=records.at(-1).identity;
|
|
136
|
+
if(!sameLocation(requestedIdentity,identity)){
|
|
137
|
+
fail(`${label} must resolve to its captured directory identity.`,
|
|
138
|
+
'ARCANE_POLICY_DENIED');
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
requested:resolved,
|
|
142
|
+
requestedIdentity,
|
|
143
|
+
canonical,
|
|
144
|
+
identity,
|
|
145
|
+
records:Object.freeze(records)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function assertDirectoryChain(chain,label){
|
|
150
|
+
for(const record of chain.records){
|
|
151
|
+
let current;
|
|
152
|
+
try{current=await lstat(record.path,{bigint:true});}
|
|
153
|
+
catch(error){
|
|
154
|
+
if(error?.code==='ENOENT'){
|
|
155
|
+
fail(`${label} changed while its operation boundary was active.`,
|
|
156
|
+
'ARCANE_POLICY_DENIED');
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
if(current.isSymbolicLink()||!current.isDirectory()
|
|
161
|
+
||!sameLocation(current,record.identity)){
|
|
162
|
+
fail(`${label} changed while its operation boundary was active.`,
|
|
163
|
+
'ARCANE_POLICY_DENIED');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let canonical;
|
|
167
|
+
let requestedIdentity;
|
|
168
|
+
try{
|
|
169
|
+
canonical=await realpath(chain.requested);
|
|
170
|
+
requestedIdentity=await lstat(chain.requested,{bigint:true});
|
|
171
|
+
}catch(error){
|
|
172
|
+
if(error?.code==='ENOENT'){
|
|
173
|
+
fail(`${label} changed while its operation boundary was active.`,
|
|
174
|
+
'ARCANE_POLICY_DENIED');
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
if(!samePath(canonical,chain.canonical)
|
|
179
|
+
||requestedIdentity.isSymbolicLink()||!requestedIdentity.isDirectory()
|
|
180
|
+
||!sameLocation(requestedIdentity,chain.requestedIdentity)
|
|
181
|
+
||!sameLocation(requestedIdentity,chain.identity)){
|
|
182
|
+
fail(`${label} changed while its operation boundary was active.`,
|
|
183
|
+
'ARCANE_POLICY_DENIED');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function ensureLockDirectory(workspace){
|
|
188
|
+
await assertDirectoryChain(workspace,'Arcane workspace');
|
|
189
|
+
const requested=path.join(workspace.canonical,LOCK_DIRECTORY);
|
|
190
|
+
try{await mkdir(requested,{mode:0o700});}
|
|
191
|
+
catch(error){if(error?.code!=='EEXIST')throw error;}
|
|
192
|
+
const directory=await captureRealDirectoryChain(
|
|
193
|
+
requested,
|
|
194
|
+
'Workspace operation-lock directory'
|
|
195
|
+
);
|
|
196
|
+
if(!samePath(path.dirname(directory.canonical),workspace.canonical)){
|
|
197
|
+
fail('Workspace operation-lock directory must remain directly inside the workspace.',
|
|
198
|
+
'ARCANE_POLICY_DENIED');
|
|
199
|
+
}
|
|
200
|
+
await assertDirectoryChain(workspace,'Arcane workspace');
|
|
201
|
+
return directory;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ownerIsAlive(pid){
|
|
205
|
+
if(!Number.isSafeInteger(pid)||pid<=0)return true;
|
|
206
|
+
try{
|
|
207
|
+
process.kill(pid,0);
|
|
208
|
+
return true;
|
|
209
|
+
}catch(error){
|
|
210
|
+
return error?.code==='EPERM';
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validOperation(operation){
|
|
215
|
+
return typeof operation==='string'&&/^[a-z0-9][a-z0-9.-]*$/u.test(operation);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function recoverableDocument(document,{workspaceRoot,now}){
|
|
219
|
+
if(!exactKeys(document,DOCUMENT_KEYS)||!exactKeys(document.owner,OWNER_KEYS))return false;
|
|
220
|
+
const acquired=Date.parse(document.acquiredAt);
|
|
221
|
+
const expires=Date.parse(document.expiresAt);
|
|
222
|
+
return document.schemaVersion===1
|
|
223
|
+
&&document.kind==='arcane-workspace-operation-lock'
|
|
224
|
+
&&validOperation(document.operation)
|
|
225
|
+
&&Number.isSafeInteger(document.owner.pid)
|
|
226
|
+
&&document.owner.pid>0
|
|
227
|
+
&&typeof document.scope==='string'
|
|
228
|
+
&&samePath(document.scope,workspaceRoot)
|
|
229
|
+
&&typeof document.nonce==='string'
|
|
230
|
+
&&/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
|
|
231
|
+
.test(document.nonce)
|
|
232
|
+
&&Number.isFinite(acquired)
|
|
233
|
+
&&Number.isFinite(expires)
|
|
234
|
+
&&document.ttlMilliseconds===LOCK_TTL_MILLISECONDS
|
|
235
|
+
&&expires===acquired+LOCK_TTL_MILLISECONDS
|
|
236
|
+
&&expires<=now
|
|
237
|
+
&&document.releaseProcedure===RELEASE_PROCEDURE
|
|
238
|
+
&&document.staleRecovery===STALE_RECOVERY
|
|
239
|
+
&&document.securityBoundary===SECURITY_BOUNDARY
|
|
240
|
+
&&!ownerIsAlive(document.owner.pid);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function existingRegularLock(lockPath){
|
|
244
|
+
let current;
|
|
245
|
+
try{current=await lstat(lockPath,{bigint:true});}
|
|
246
|
+
catch(error){
|
|
247
|
+
if(error?.code==='ENOENT')return null;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
if(current.isSymbolicLink()||!current.isFile()){
|
|
251
|
+
fail('Workspace operation lock path is a symbolic link, junction, or non-file.',
|
|
252
|
+
'ARCANE_POLICY_DENIED');
|
|
253
|
+
}
|
|
254
|
+
return current;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function restoreQuarantinedLock({directory,lockPath,quarantinePath,identity}){
|
|
258
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
259
|
+
const quarantined=await existingRegularLock(quarantinePath);
|
|
260
|
+
if(quarantined===null||!sameLocation(quarantined,identity))return false;
|
|
261
|
+
if(await existingRegularLock(lockPath)!==null)return false;
|
|
262
|
+
await rename(quarantinePath,lockPath);
|
|
263
|
+
const restored=await existingRegularLock(lockPath);
|
|
264
|
+
return restored!==null&&sameLocation(restored,identity);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function recoverExpiredLock(lockPath,directory,workspaceRoot,onEvent){
|
|
268
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
269
|
+
const initial=await existingRegularLock(lockPath);
|
|
270
|
+
if(initial===null)return true;
|
|
271
|
+
let handle;
|
|
272
|
+
try{handle=await open(lockPath,OPEN_READ);}
|
|
273
|
+
catch(error){
|
|
274
|
+
if(error?.code==='ENOENT')return true;
|
|
275
|
+
if(error?.code==='ELOOP'){
|
|
276
|
+
fail('Workspace operation lock path is a symbolic link or junction.',
|
|
277
|
+
'ARCANE_POLICY_DENIED');
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
try{
|
|
282
|
+
const before=await handle.stat({bigint:true});
|
|
283
|
+
if(!before.isFile()||before.size>BigInt(MAX_LOCK_BYTES)
|
|
284
|
+
||!sameFileState(before,initial))return false;
|
|
285
|
+
const bytes=await handle.readFile();
|
|
286
|
+
const after=await handle.stat({bigint:true});
|
|
287
|
+
const current=await existingRegularLock(lockPath);
|
|
288
|
+
if(current===null)return true;
|
|
289
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
290
|
+
if(!sameFileState(before,after)||!sameFileState(after,current))return false;
|
|
291
|
+
let document;
|
|
292
|
+
try{document=JSON.parse(bytes.toString('utf8'));}
|
|
293
|
+
catch{return false;}
|
|
294
|
+
if(!recoverableDocument(document,{workspaceRoot,now:Date.now()}))return false;
|
|
295
|
+
const canonical=Buffer.from(`${JSON.stringify(document,null,2)}\n`,'utf8');
|
|
296
|
+
if(!canonical.equals(bytes))return false;
|
|
297
|
+
await handle.close();
|
|
298
|
+
handle=null;
|
|
299
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
300
|
+
const final=await existingRegularLock(lockPath);
|
|
301
|
+
if(final===null)return true;
|
|
302
|
+
if(!sameFileState(final,current))return false;
|
|
303
|
+
|
|
304
|
+
const quarantinePath=path.join(
|
|
305
|
+
directory.canonical,
|
|
306
|
+
`.${LOCK_NAME}.arcane-stale-${String(process.pid)}-${randomUUID()}`
|
|
307
|
+
);
|
|
308
|
+
await rename(lockPath,quarantinePath);
|
|
309
|
+
let quarantinedHandle;
|
|
310
|
+
let quarantineIdentity=await existingRegularLock(quarantinePath);
|
|
311
|
+
if(quarantineIdentity===null||!sameLocation(quarantineIdentity,final)){
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
let valid=false;
|
|
315
|
+
try{
|
|
316
|
+
await onEvent?.(Object.freeze({
|
|
317
|
+
type:'workspace.operation.stale-quarantined',
|
|
318
|
+
workspaceRoot,
|
|
319
|
+
quarantinePath
|
|
320
|
+
}));
|
|
321
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
322
|
+
const quarantined=await existingRegularLock(quarantinePath);
|
|
323
|
+
if(quarantined===null||!sameFileState(quarantined,quarantineIdentity))return false;
|
|
324
|
+
quarantinedHandle=await open(quarantinePath,OPEN_READ);
|
|
325
|
+
const quarantineBefore=await quarantinedHandle.stat({bigint:true});
|
|
326
|
+
if(!sameFileState(quarantineBefore,quarantineIdentity))return false;
|
|
327
|
+
const quarantineBytes=await quarantinedHandle.readFile();
|
|
328
|
+
const quarantineAfter=await quarantinedHandle.stat({bigint:true});
|
|
329
|
+
const quarantineCurrent=await existingRegularLock(quarantinePath);
|
|
330
|
+
if(quarantineCurrent===null
|
|
331
|
+
||!sameFileState(quarantineBefore,quarantineAfter)
|
|
332
|
+
||!sameFileState(quarantineAfter,quarantineCurrent)
|
|
333
|
+
||!quarantineBytes.equals(bytes))return false;
|
|
334
|
+
let quarantineDocument;
|
|
335
|
+
try{quarantineDocument=JSON.parse(quarantineBytes.toString('utf8'));}
|
|
336
|
+
catch{return false;}
|
|
337
|
+
const quarantineCanonical=Buffer.from(
|
|
338
|
+
`${JSON.stringify(quarantineDocument,null,2)}\n`,
|
|
339
|
+
'utf8'
|
|
340
|
+
);
|
|
341
|
+
if(!quarantineCanonical.equals(quarantineBytes)
|
|
342
|
+
||!recoverableDocument(quarantineDocument,{workspaceRoot,now:Date.now()})){
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
valid=true;
|
|
346
|
+
}finally{
|
|
347
|
+
await quarantinedHandle?.close().catch(()=>{});
|
|
348
|
+
if(!valid){
|
|
349
|
+
await restoreQuarantinedLock({
|
|
350
|
+
directory,lockPath,quarantinePath,identity:quarantineIdentity
|
|
351
|
+
}).catch(()=>{});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
355
|
+
const deletionCandidate=await existingRegularLock(quarantinePath);
|
|
356
|
+
if(deletionCandidate===null||!sameFileState(deletionCandidate,quarantineIdentity)){
|
|
357
|
+
await restoreQuarantinedLock({
|
|
358
|
+
directory,lockPath,quarantinePath,identity:quarantineIdentity
|
|
359
|
+
}).catch(()=>{});
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
await rm(quarantinePath);
|
|
363
|
+
return true;
|
|
364
|
+
}finally{
|
|
365
|
+
await handle?.close().catch(()=>{});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function validateOwnedLock({workspace,directory,lockPath,handle,identity,bytes,label}){
|
|
370
|
+
await assertDirectoryChain(workspace,'Arcane workspace');
|
|
371
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
372
|
+
let opened;
|
|
373
|
+
let current;
|
|
374
|
+
try{
|
|
375
|
+
opened=await handle.stat({bigint:true});
|
|
376
|
+
current=await lstat(lockPath,{bigint:true});
|
|
377
|
+
}catch(error){
|
|
378
|
+
if(error?.code==='ENOENT'){
|
|
379
|
+
fail(`Workspace operation lock disappeared ${label}.`, 'ARCANE_POLICY_DENIED');
|
|
380
|
+
}
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if(!opened.isFile()||opened.size!==BigInt(bytes.length)
|
|
384
|
+
||!sameLocation(opened,identity)||current.isSymbolicLink()||!current.isFile()
|
|
385
|
+
||!sameLocation(current,identity)){
|
|
386
|
+
fail(`Workspace operation lock changed ${label}.`, 'ARCANE_POLICY_DENIED');
|
|
387
|
+
}
|
|
388
|
+
const read=Buffer.alloc(bytes.length);
|
|
389
|
+
const result=await handle.read(read,0,read.length,0);
|
|
390
|
+
const after=await handle.stat({bigint:true});
|
|
391
|
+
if(result.bytesRead!==bytes.length||!read.equals(bytes)
|
|
392
|
+
||!sameLocation(after,identity)||after.size!==BigInt(bytes.length)){
|
|
393
|
+
fail(`Workspace operation lock contents changed ${label}.`, 'ARCANE_POLICY_DENIED');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function removeOwnedLock({directory,lockPath,handle,identity}){
|
|
398
|
+
if(!handle||!identity)return {handle:null,removed:false};
|
|
399
|
+
let owned=false;
|
|
400
|
+
try{
|
|
401
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
402
|
+
const opened=await handle.stat({bigint:true});
|
|
403
|
+
const current=await existingRegularLock(lockPath);
|
|
404
|
+
owned=current!==null&&sameLocation(opened,identity)&&sameLocation(current,identity);
|
|
405
|
+
}finally{
|
|
406
|
+
await handle.close().catch(()=>{});
|
|
407
|
+
handle=null;
|
|
408
|
+
}
|
|
409
|
+
if(!owned)return {handle,removed:false};
|
|
410
|
+
await assertDirectoryChain(directory,'Workspace operation-lock directory');
|
|
411
|
+
const final=await existingRegularLock(lockPath);
|
|
412
|
+
if(final===null)return {handle,removed:true};
|
|
413
|
+
if(!sameLocation(final,identity))return {handle,removed:false};
|
|
414
|
+
await rm(lockPath);
|
|
415
|
+
return {handle,removed:true};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function leaseFor(state,operation){
|
|
419
|
+
const lease=Object.freeze({
|
|
420
|
+
workspaceRoot:state.root.workspace.canonical,
|
|
421
|
+
operation,
|
|
422
|
+
nonce:randomUUID(),
|
|
423
|
+
lockPath:state.root.lockPath
|
|
424
|
+
});
|
|
425
|
+
leaseStates.set(lease,state);
|
|
426
|
+
return lease;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function inheritedLease(workspace,operation,supplied){
|
|
430
|
+
const parent=leaseStates.get(supplied);
|
|
431
|
+
const root=parent?.root;
|
|
432
|
+
if(!parent?.active||!root?.active
|
|
433
|
+
||!samePath(parent.root.workspace.canonical,workspace.canonical)
|
|
434
|
+
||activeRoots.get(workspace.canonical)!==root.lease){
|
|
435
|
+
fail('Arcane workspace operation lease is missing, inactive, or belongs to another workspace.',
|
|
436
|
+
'ARCANE_POLICY_DENIED');
|
|
437
|
+
}
|
|
438
|
+
if(parent.child!==null){
|
|
439
|
+
fail('The supplied Arcane workspace operation lease already has active nested work.');
|
|
440
|
+
}
|
|
441
|
+
const state=trackedLeaseState({active:true,child:null,parent,root});
|
|
442
|
+
const lease=leaseFor(state,operation);
|
|
443
|
+
state.lease=lease;
|
|
444
|
+
parent.child=state;
|
|
445
|
+
return {
|
|
446
|
+
inherited:true,
|
|
447
|
+
lease,
|
|
448
|
+
release(){
|
|
449
|
+
if(state.releasePromise)return state.releasePromise;
|
|
450
|
+
state.active=false;
|
|
451
|
+
state.releasePromise=(async()=>{
|
|
452
|
+
const child=state.child;
|
|
453
|
+
const misuseError=child===null?null:workspaceBusy(
|
|
454
|
+
'A nested Arcane workspace operation was still active when its parent settled.'
|
|
455
|
+
);
|
|
456
|
+
try{
|
|
457
|
+
if(child!==null)await child.settled;
|
|
458
|
+
}finally{
|
|
459
|
+
leaseStates.delete(lease);
|
|
460
|
+
if(parent.child===state)parent.child=null;
|
|
461
|
+
settleLeaseState(state);
|
|
462
|
+
}
|
|
463
|
+
if(misuseError)throw misuseError;
|
|
464
|
+
})();
|
|
465
|
+
return state.releasePromise;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function acquire({
|
|
471
|
+
workspaceRoot,
|
|
472
|
+
operation,
|
|
473
|
+
workspaceOperationLease,
|
|
474
|
+
signal,
|
|
475
|
+
onEvent
|
|
476
|
+
}){
|
|
477
|
+
throwIfAborted(signal);
|
|
478
|
+
if(typeof workspaceRoot!=='string'||!workspaceRoot.trim()){
|
|
479
|
+
throw new TypeError('workspaceRoot is required for an Arcane workspace operation lock.');
|
|
480
|
+
}
|
|
481
|
+
if(!validOperation(operation)){
|
|
482
|
+
throw new TypeError('operation must be a stable lowercase Arcane operation name.');
|
|
483
|
+
}
|
|
484
|
+
const workspace=await captureRealDirectoryChain(path.resolve(workspaceRoot),'Arcane workspace');
|
|
485
|
+
if(workspaceOperationLease!==undefined){
|
|
486
|
+
return inheritedLease(workspace,operation,workspaceOperationLease);
|
|
487
|
+
}
|
|
488
|
+
if(activeRoots.has(workspace.canonical)){
|
|
489
|
+
fail(`Another Arcane operation already owns ${workspace.canonical}.`);
|
|
490
|
+
}
|
|
491
|
+
const reservation=Object.freeze({kind:'arcane-workspace-operation-reservation'});
|
|
492
|
+
activeRoots.set(workspace.canonical,reservation);
|
|
493
|
+
let directory;
|
|
494
|
+
let lockPath;
|
|
495
|
+
let handle;
|
|
496
|
+
let identity;
|
|
497
|
+
let rootState;
|
|
498
|
+
try{
|
|
499
|
+
directory=await ensureLockDirectory(workspace);
|
|
500
|
+
lockPath=path.join(directory.canonical,LOCK_NAME);
|
|
501
|
+
let recovered=false;
|
|
502
|
+
for(;;){
|
|
503
|
+
try{
|
|
504
|
+
handle=await open(lockPath,OPEN_EXCLUSIVE,0o600);
|
|
505
|
+
break;
|
|
506
|
+
}catch(error){
|
|
507
|
+
if(error?.code==='EEXIST'){
|
|
508
|
+
if(recovered){
|
|
509
|
+
fail(
|
|
510
|
+
`Workspace is locked by another Arcane operation: ${lockPath}. `
|
|
511
|
+
+'Inspect its owner, expiry, and staleRecovery before retrying.'
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
const didRecover=await recoverExpiredLock(
|
|
515
|
+
lockPath,
|
|
516
|
+
directory,
|
|
517
|
+
workspace.canonical,
|
|
518
|
+
onEvent
|
|
519
|
+
);
|
|
520
|
+
if(!didRecover){
|
|
521
|
+
fail(
|
|
522
|
+
`Workspace is locked by another Arcane operation: ${lockPath}. `
|
|
523
|
+
+'Inspect its owner, expiry, and staleRecovery before retrying.'
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
recovered=true;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if(error?.code==='ELOOP'){
|
|
530
|
+
fail('Workspace operation lock path is a symbolic link or junction.',
|
|
531
|
+
'ARCANE_POLICY_DENIED');
|
|
532
|
+
}
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Claim cleanup ownership immediately after O_EXCL succeeds. This identity remains
|
|
537
|
+
// usable even if a later write, sync, callback, or cancellation boundary fails.
|
|
538
|
+
identity=await handle.stat({bigint:true});
|
|
539
|
+
if(!identity.isFile()){
|
|
540
|
+
fail('Workspace operation lock must be a regular file.','ARCANE_POLICY_DENIED');
|
|
541
|
+
}
|
|
542
|
+
const nonce=randomUUID();
|
|
543
|
+
const acquiredAt=new Date();
|
|
544
|
+
const document=Object.freeze({
|
|
545
|
+
schemaVersion:1,
|
|
546
|
+
kind:'arcane-workspace-operation-lock',
|
|
547
|
+
operation,
|
|
548
|
+
owner:Object.freeze({pid:process.pid}),
|
|
549
|
+
scope:workspace.canonical,
|
|
550
|
+
nonce,
|
|
551
|
+
acquiredAt:acquiredAt.toISOString(),
|
|
552
|
+
expiresAt:new Date(acquiredAt.getTime()+LOCK_TTL_MILLISECONDS).toISOString(),
|
|
553
|
+
ttlMilliseconds:LOCK_TTL_MILLISECONDS,
|
|
554
|
+
releaseProcedure:RELEASE_PROCEDURE,
|
|
555
|
+
staleRecovery:STALE_RECOVERY,
|
|
556
|
+
securityBoundary:SECURITY_BOUNDARY
|
|
557
|
+
});
|
|
558
|
+
const bytes=Buffer.from(`${JSON.stringify(document,null,2)}\n`,'utf8');
|
|
559
|
+
await handle.writeFile(bytes);
|
|
560
|
+
await handle.sync();
|
|
561
|
+
await validateOwnedLock({
|
|
562
|
+
workspace,directory,lockPath,handle,identity,bytes,label:'while it was acquired'
|
|
563
|
+
});
|
|
564
|
+
rootState=trackedLeaseState({
|
|
565
|
+
active:true,
|
|
566
|
+
child:null,
|
|
567
|
+
workspace,
|
|
568
|
+
directory,
|
|
569
|
+
lockPath,
|
|
570
|
+
handle,
|
|
571
|
+
identity,
|
|
572
|
+
bytes
|
|
573
|
+
});
|
|
574
|
+
rootState.root=rootState;
|
|
575
|
+
const lease=Object.freeze({
|
|
576
|
+
workspaceRoot:workspace.canonical,
|
|
577
|
+
operation,
|
|
578
|
+
nonce,
|
|
579
|
+
lockPath
|
|
580
|
+
});
|
|
581
|
+
rootState.lease=lease;
|
|
582
|
+
leaseStates.set(lease,rootState);
|
|
583
|
+
activeRoots.set(workspace.canonical,lease);
|
|
584
|
+
await onEvent?.(Object.freeze({
|
|
585
|
+
type:'workspace.operation.locked',
|
|
586
|
+
workspaceRoot:workspace.canonical,
|
|
587
|
+
operation
|
|
588
|
+
}));
|
|
589
|
+
throwIfAborted(signal);
|
|
590
|
+
await validateOwnedLock({
|
|
591
|
+
workspace,directory,lockPath,handle,identity,bytes,
|
|
592
|
+
label:'after the acquisition callback'
|
|
593
|
+
});
|
|
594
|
+
return {
|
|
595
|
+
inherited:false,
|
|
596
|
+
lease,
|
|
597
|
+
release(){
|
|
598
|
+
if(rootState.releasePromise)return rootState.releasePromise;
|
|
599
|
+
rootState.active=false;
|
|
600
|
+
rootState.releasePromise=(async()=>{
|
|
601
|
+
const child=rootState.child;
|
|
602
|
+
const misuseError=child===null?null:workspaceBusy(
|
|
603
|
+
'A nested Arcane workspace operation was still active when its root settled.'
|
|
604
|
+
);
|
|
605
|
+
let releaseError;
|
|
606
|
+
let observerError;
|
|
607
|
+
if(child!==null)await child.settled;
|
|
608
|
+
try{
|
|
609
|
+
await validateOwnedLock({
|
|
610
|
+
workspace,directory,lockPath,handle,identity,bytes,
|
|
611
|
+
label:'before release'
|
|
612
|
+
});
|
|
613
|
+
const removed=await removeOwnedLock({directory,lockPath,handle,identity});
|
|
614
|
+
handle=removed.handle;
|
|
615
|
+
rootState.handle=handle;
|
|
616
|
+
if(!removed.removed){
|
|
617
|
+
fail('Workspace operation lock path changed before cleanup.',
|
|
618
|
+
'ARCANE_POLICY_DENIED');
|
|
619
|
+
}
|
|
620
|
+
}catch(error){
|
|
621
|
+
releaseError=error;
|
|
622
|
+
if(misuseError)releaseError.nestedLeaseError=misuseError;
|
|
623
|
+
if(handle){
|
|
624
|
+
try{
|
|
625
|
+
const removed=await removeOwnedLock({
|
|
626
|
+
directory,lockPath,handle,identity
|
|
627
|
+
});
|
|
628
|
+
handle=removed.handle;
|
|
629
|
+
rootState.handle=handle;
|
|
630
|
+
}catch(cleanupError){
|
|
631
|
+
releaseError.cleanupError=cleanupError;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}finally{
|
|
635
|
+
if(activeRoots.get(workspace.canonical)===lease){
|
|
636
|
+
activeRoots.delete(workspace.canonical);
|
|
637
|
+
}
|
|
638
|
+
leaseStates.delete(lease);
|
|
639
|
+
await handle?.close().catch(()=>{});
|
|
640
|
+
handle=null;
|
|
641
|
+
rootState.handle=null;
|
|
642
|
+
settleLeaseState(rootState);
|
|
643
|
+
}
|
|
644
|
+
if(releaseError)throw releaseError;
|
|
645
|
+
try{
|
|
646
|
+
await onEvent?.(Object.freeze({
|
|
647
|
+
type:'workspace.operation.released',
|
|
648
|
+
workspaceRoot:workspace.canonical,
|
|
649
|
+
operation
|
|
650
|
+
}));
|
|
651
|
+
}catch(error){observerError=error;}
|
|
652
|
+
if(misuseError){
|
|
653
|
+
if(observerError)misuseError.observerError=observerError;
|
|
654
|
+
throw misuseError;
|
|
655
|
+
}
|
|
656
|
+
return observerError;
|
|
657
|
+
})();
|
|
658
|
+
return rootState.releasePromise;
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}catch(error){
|
|
662
|
+
if(rootState){
|
|
663
|
+
rootState.active=false;
|
|
664
|
+
if(rootState.lease)leaseStates.delete(rootState.lease);
|
|
665
|
+
settleLeaseState(rootState);
|
|
666
|
+
}
|
|
667
|
+
if(activeRoots.get(workspace.canonical)===reservation
|
|
668
|
+
||activeRoots.get(workspace.canonical)===rootState?.lease){
|
|
669
|
+
activeRoots.delete(workspace.canonical);
|
|
670
|
+
}
|
|
671
|
+
if(handle&&directory&&lockPath&&identity){
|
|
672
|
+
try{
|
|
673
|
+
const removed=await removeOwnedLock({directory,lockPath,handle,identity});
|
|
674
|
+
handle=removed.handle;
|
|
675
|
+
}catch(cleanupError){
|
|
676
|
+
error.cleanupError=cleanupError;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await handle?.close().catch(()=>{});
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function attachReleaseError(workError,releaseError){
|
|
685
|
+
try{
|
|
686
|
+
workError.releaseError=releaseError;
|
|
687
|
+
return workError;
|
|
688
|
+
}catch{
|
|
689
|
+
return new AggregateError(
|
|
690
|
+
[workError,releaseError],
|
|
691
|
+
'The Arcane workspace operation and its lock release both failed.',
|
|
692
|
+
{cause:workError}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function withWorkspaceOperationLock(options,work){
|
|
698
|
+
if(typeof work!=='function')throw new TypeError('Workspace operation lock work must be a function.');
|
|
699
|
+
const acquired=await acquire(options);
|
|
700
|
+
let result;
|
|
701
|
+
let workError;
|
|
702
|
+
try{
|
|
703
|
+
result=await work(acquired.lease);
|
|
704
|
+
}catch(error){workError=error;}
|
|
705
|
+
let releaseObserverError;
|
|
706
|
+
try{releaseObserverError=await acquired.release();}
|
|
707
|
+
catch(releaseError){
|
|
708
|
+
if(workError)workError=attachReleaseError(workError,releaseError);
|
|
709
|
+
else throw releaseError;
|
|
710
|
+
}
|
|
711
|
+
if(workError&&releaseObserverError){
|
|
712
|
+
workError=attachReleaseError(workError,releaseObserverError);
|
|
713
|
+
}
|
|
714
|
+
if(workError)throw workError;
|
|
715
|
+
return result;
|
|
716
|
+
}
|