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,38 +1,18 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
2
|
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
-
import {lstat,mkdir,open,realpath,
|
|
3
|
+
import {lstat,mkdir,open,readFile,realpath,rm} from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
6
|
const LOCK_DIRECTORY='.arcane';
|
|
7
7
|
const LOCK_NAME='workspace-operation.lock.json';
|
|
8
8
|
const LOCK_TTL_MILLISECONDS=6*60*60*1000;
|
|
9
|
-
const MAX_LOCK_BYTES=64*1024;
|
|
10
9
|
const OPEN_EXCLUSIVE=FS_CONSTANTS.O_CREAT|FS_CONSTANTS.O_EXCL|FS_CONSTANTS.O_RDWR
|
|
11
10
|
|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
12
|
-
const OPEN_READ=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
13
11
|
const RELEASE_PROCEDURE=
|
|
14
|
-
'The owning Arcane process removes this
|
|
12
|
+
'The owning Arcane process removes this cooperative lock after the operation settles.';
|
|
15
13
|
const STALE_RECOVERY=
|
|
16
|
-
'
|
|
17
|
-
+'
|
|
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']);
|
|
14
|
+
'After expiresAt, confirm the recorded owner process is absent, preserve workspace changes, '
|
|
15
|
+
+'and remove only this exact cooperative lock.';
|
|
36
16
|
const activeRoots=new Map();
|
|
37
17
|
const leaseStates=new WeakMap();
|
|
38
18
|
|
|
@@ -48,24 +28,6 @@ function workspaceBusy(message){
|
|
|
48
28
|
return error;
|
|
49
29
|
}
|
|
50
30
|
|
|
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
31
|
function throwIfAborted(signal){
|
|
70
32
|
if(!signal?.aborted)return;
|
|
71
33
|
const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
|
|
@@ -73,16 +35,6 @@ function throwIfAborted(signal){
|
|
|
73
35
|
throw error;
|
|
74
36
|
}
|
|
75
37
|
|
|
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
38
|
function samePath(left,right){
|
|
87
39
|
const normalize=value=>{
|
|
88
40
|
const resolved=path.resolve(value);
|
|
@@ -91,118 +43,60 @@ function samePath(left,right){
|
|
|
91
43
|
return normalize(left)===normalize(right);
|
|
92
44
|
}
|
|
93
45
|
|
|
94
|
-
function
|
|
95
|
-
return
|
|
96
|
-
&&JSON.stringify(Object.keys(value).sort())===JSON.stringify(expected);
|
|
46
|
+
function validOperation(value){
|
|
47
|
+
return typeof value==='string'&&/^[a-z][a-z0-9._-]*$/u.test(value);
|
|
97
48
|
}
|
|
98
49
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
});
|
|
50
|
+
function trackedLeaseState(fields){
|
|
51
|
+
let resolveSettled;
|
|
52
|
+
const settled=new Promise(resolve=>{resolveSettled=resolve;});
|
|
53
|
+
return {
|
|
54
|
+
...fields,
|
|
55
|
+
releasePromise:null,
|
|
56
|
+
settled,
|
|
57
|
+
settledComplete:false,
|
|
58
|
+
resolveSettled
|
|
59
|
+
};
|
|
147
60
|
}
|
|
148
61
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
}
|
|
62
|
+
function settleLeaseState(state){
|
|
63
|
+
if(state.settledComplete)return;
|
|
64
|
+
state.settledComplete=true;
|
|
65
|
+
state.resolveSettled();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function physicalDirectory(directory,label,{create=false}={}){
|
|
69
|
+
const requested=path.resolve(directory);
|
|
70
|
+
if(create){
|
|
71
|
+
await mkdir(requested,{recursive:true});
|
|
165
72
|
}
|
|
166
|
-
let
|
|
167
|
-
let requestedIdentity;
|
|
73
|
+
let information;
|
|
168
74
|
try{
|
|
169
|
-
|
|
170
|
-
requestedIdentity=await lstat(chain.requested,{bigint:true});
|
|
75
|
+
information=await lstat(requested);
|
|
171
76
|
}catch(error){
|
|
172
77
|
if(error?.code==='ENOENT'){
|
|
173
|
-
fail(`${label}
|
|
174
|
-
'ARCANE_POLICY_DENIED');
|
|
78
|
+
fail(`${label} does not exist.`,'ARCANE_POLICY_DENIED');
|
|
175
79
|
}
|
|
176
80
|
throw error;
|
|
177
81
|
}
|
|
178
|
-
if(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
82
|
+
if(information.isSymbolicLink()||!information.isDirectory()){
|
|
83
|
+
fail(`${label} must be a physical directory.`,'ARCANE_POLICY_DENIED');
|
|
84
|
+
}
|
|
85
|
+
const canonical=await realpath(requested);
|
|
86
|
+
const canonicalInformation=await lstat(canonical);
|
|
87
|
+
if(canonicalInformation.isSymbolicLink()||!canonicalInformation.isDirectory()){
|
|
88
|
+
fail(`${label} must resolve to a physical directory.`,'ARCANE_POLICY_DENIED');
|
|
184
89
|
}
|
|
90
|
+
return {requested,canonical};
|
|
185
91
|
}
|
|
186
92
|
|
|
187
93
|
async function ensureLockDirectory(workspace){
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
94
|
+
const directory=path.join(workspace.canonical,LOCK_DIRECTORY);
|
|
95
|
+
return physicalDirectory(directory,'Workspace operation-lock directory',{create:true});
|
|
202
96
|
}
|
|
203
97
|
|
|
204
98
|
function ownerIsAlive(pid){
|
|
205
|
-
if(!Number.isSafeInteger(pid)||pid
|
|
99
|
+
if(!Number.isSafeInteger(pid)||pid<1)return false;
|
|
206
100
|
try{
|
|
207
101
|
process.kill(pid,0);
|
|
208
102
|
return true;
|
|
@@ -211,217 +105,73 @@ function ownerIsAlive(pid){
|
|
|
211
105
|
}
|
|
212
106
|
}
|
|
213
107
|
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
108
|
+
function lockDocument(value,{workspaceRoot,now=Date.now()}={}){
|
|
109
|
+
if(!value||typeof value!=='object'||Array.isArray(value)
|
|
110
|
+
||value.schemaVersion!==1
|
|
111
|
+
||value.kind!=='arcane-workspace-operation-lock'
|
|
112
|
+
||!validOperation(value.operation)
|
|
113
|
+
||typeof value.nonce!=='string'||!value.nonce
|
|
114
|
+
||!value.owner||!Number.isSafeInteger(value.owner.pid)
|
|
115
|
+
||typeof value.scope!=='string'||!samePath(value.scope,workspaceRoot)
|
|
116
|
+
||typeof value.acquiredAt!=='string'
|
|
117
|
+
||typeof value.expiresAt!=='string'
|
|
118
|
+
||value.releaseProcedure!==RELEASE_PROCEDURE
|
|
119
|
+
||value.staleRecovery!==STALE_RECOVERY){
|
|
120
|
+
return null;
|
|
249
121
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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);
|
|
122
|
+
const expiresAt=Date.parse(value.expiresAt);
|
|
123
|
+
if(!Number.isFinite(expiresAt))return null;
|
|
124
|
+
return {...value,expired:expiresAt<=now};
|
|
265
125
|
}
|
|
266
126
|
|
|
267
|
-
async function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
}
|
|
127
|
+
async function readLock(lockPath,workspaceRoot){
|
|
128
|
+
let information;
|
|
129
|
+
try{
|
|
130
|
+
information=await lstat(lockPath);
|
|
131
|
+
}catch(error){
|
|
132
|
+
if(error?.code==='ENOENT')return null;
|
|
279
133
|
throw error;
|
|
280
134
|
}
|
|
135
|
+
if(information.isSymbolicLink()||!information.isFile())return null;
|
|
136
|
+
let value;
|
|
281
137
|
try{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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(()=>{});
|
|
138
|
+
value=JSON.parse(await readFile(lockPath,'utf8'));
|
|
139
|
+
}catch{
|
|
140
|
+
return null;
|
|
366
141
|
}
|
|
142
|
+
return lockDocument(value,{workspaceRoot});
|
|
367
143
|
}
|
|
368
144
|
|
|
369
|
-
async function
|
|
370
|
-
await
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
}
|
|
145
|
+
async function recoverExpiredLock(lockPath,workspaceRoot,onEvent){
|
|
146
|
+
const document=await readLock(lockPath,workspaceRoot);
|
|
147
|
+
if(!document?.expired||ownerIsAlive(document.owner.pid))return false;
|
|
148
|
+
await onEvent?.({
|
|
149
|
+
type:'workspace.operation.stale-recovered',
|
|
150
|
+
workspaceRoot,
|
|
151
|
+
lockPath,
|
|
152
|
+
previousOwner:document.owner,
|
|
153
|
+
previousOperation:document.operation
|
|
154
|
+
});
|
|
155
|
+
await rm(lockPath);
|
|
156
|
+
return true;
|
|
395
157
|
}
|
|
396
158
|
|
|
397
|
-
async function removeOwnedLock({
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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};
|
|
159
|
+
async function removeOwnedLock({lockPath,workspaceRoot,nonce,handle}){
|
|
160
|
+
await handle?.close().catch(()=>{});
|
|
161
|
+
const document=await readLock(lockPath,workspaceRoot);
|
|
162
|
+
if(document===null)return true;
|
|
163
|
+
if(document.nonce!==nonce||document.owner.pid!==process.pid)return false;
|
|
414
164
|
await rm(lockPath);
|
|
415
|
-
return
|
|
165
|
+
return true;
|
|
416
166
|
}
|
|
417
167
|
|
|
418
168
|
function leaseFor(state,operation){
|
|
419
|
-
const lease=
|
|
169
|
+
const lease={
|
|
420
170
|
workspaceRoot:state.root.workspace.canonical,
|
|
421
171
|
operation,
|
|
422
172
|
nonce:randomUUID(),
|
|
423
173
|
lockPath:state.root.lockPath
|
|
424
|
-
}
|
|
174
|
+
};
|
|
425
175
|
leaseStates.set(lease,state);
|
|
426
176
|
return lease;
|
|
427
177
|
}
|
|
@@ -430,10 +180,12 @@ function inheritedLease(workspace,operation,supplied){
|
|
|
430
180
|
const parent=leaseStates.get(supplied);
|
|
431
181
|
const root=parent?.root;
|
|
432
182
|
if(!parent?.active||!root?.active
|
|
433
|
-
||!samePath(
|
|
183
|
+
||!samePath(root.workspace.canonical,workspace.canonical)
|
|
434
184
|
||activeRoots.get(workspace.canonical)!==root.lease){
|
|
435
|
-
fail(
|
|
436
|
-
'
|
|
185
|
+
fail(
|
|
186
|
+
'Arcane workspace operation lease is missing, inactive, or belongs to another workspace.',
|
|
187
|
+
'ARCANE_POLICY_DENIED'
|
|
188
|
+
);
|
|
437
189
|
}
|
|
438
190
|
if(parent.child!==null){
|
|
439
191
|
fail('The supplied Arcane workspace operation lease already has active nested work.');
|
|
@@ -481,22 +233,21 @@ async function acquire({
|
|
|
481
233
|
if(!validOperation(operation)){
|
|
482
234
|
throw new TypeError('operation must be a stable lowercase Arcane operation name.');
|
|
483
235
|
}
|
|
484
|
-
const workspace=await
|
|
236
|
+
const workspace=await physicalDirectory(workspaceRoot,'Arcane workspace');
|
|
485
237
|
if(workspaceOperationLease!==undefined){
|
|
486
238
|
return inheritedLease(workspace,operation,workspaceOperationLease);
|
|
487
239
|
}
|
|
488
240
|
if(activeRoots.has(workspace.canonical)){
|
|
489
241
|
fail(`Another Arcane operation already owns ${workspace.canonical}.`);
|
|
490
242
|
}
|
|
491
|
-
const reservation=
|
|
243
|
+
const reservation={kind:'arcane-workspace-operation-reservation'};
|
|
492
244
|
activeRoots.set(workspace.canonical,reservation);
|
|
493
|
-
let directory;
|
|
494
|
-
let lockPath;
|
|
495
245
|
let handle;
|
|
496
|
-
let
|
|
246
|
+
let lockPath;
|
|
497
247
|
let rootState;
|
|
248
|
+
let nonce;
|
|
498
249
|
try{
|
|
499
|
-
directory=await ensureLockDirectory(workspace);
|
|
250
|
+
const directory=await ensureLockDirectory(workspace);
|
|
500
251
|
lockPath=path.join(directory.canonical,LOCK_NAME);
|
|
501
252
|
let recovered=false;
|
|
502
253
|
for(;;){
|
|
@@ -504,93 +255,56 @@ async function acquire({
|
|
|
504
255
|
handle=await open(lockPath,OPEN_EXCLUSIVE,0o600);
|
|
505
256
|
break;
|
|
506
257
|
}catch(error){
|
|
507
|
-
if(error?.code
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
directory,
|
|
517
|
-
workspace.canonical,
|
|
518
|
-
onEvent
|
|
258
|
+
if(error?.code!=='EEXIST')throw error;
|
|
259
|
+
if(recovered||!(await recoverExpiredLock(
|
|
260
|
+
lockPath,
|
|
261
|
+
workspace.canonical,
|
|
262
|
+
onEvent
|
|
263
|
+
))){
|
|
264
|
+
fail(
|
|
265
|
+
`Workspace is locked by another Arcane operation: ${lockPath}. `
|
|
266
|
+
+'Inspect its owner, expiry, and staleRecovery before retrying.'
|
|
519
267
|
);
|
|
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
268
|
}
|
|
533
|
-
|
|
269
|
+
recovered=true;
|
|
534
270
|
}
|
|
535
271
|
}
|
|
536
|
-
|
|
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();
|
|
272
|
+
nonce=randomUUID();
|
|
543
273
|
const acquiredAt=new Date();
|
|
544
|
-
const document=
|
|
274
|
+
const document={
|
|
545
275
|
schemaVersion:1,
|
|
546
276
|
kind:'arcane-workspace-operation-lock',
|
|
547
277
|
operation,
|
|
548
|
-
owner:
|
|
278
|
+
owner:{pid:process.pid},
|
|
549
279
|
scope:workspace.canonical,
|
|
550
280
|
nonce,
|
|
551
281
|
acquiredAt:acquiredAt.toISOString(),
|
|
552
282
|
expiresAt:new Date(acquiredAt.getTime()+LOCK_TTL_MILLISECONDS).toISOString(),
|
|
553
283
|
ttlMilliseconds:LOCK_TTL_MILLISECONDS,
|
|
554
284
|
releaseProcedure:RELEASE_PROCEDURE,
|
|
555
|
-
staleRecovery:STALE_RECOVERY
|
|
556
|
-
|
|
557
|
-
});
|
|
558
|
-
const bytes=Buffer.from(`${JSON.stringify(document,null,2)}\n`,'utf8');
|
|
559
|
-
await handle.writeFile(bytes);
|
|
285
|
+
staleRecovery:STALE_RECOVERY
|
|
286
|
+
};
|
|
287
|
+
await handle.writeFile(`${JSON.stringify(document,null,2)}\n`,'utf8');
|
|
560
288
|
await handle.sync();
|
|
561
|
-
await validateOwnedLock({
|
|
562
|
-
workspace,directory,lockPath,handle,identity,bytes,label:'while it was acquired'
|
|
563
|
-
});
|
|
564
289
|
rootState=trackedLeaseState({
|
|
565
290
|
active:true,
|
|
566
291
|
child:null,
|
|
567
292
|
workspace,
|
|
568
|
-
directory,
|
|
569
293
|
lockPath,
|
|
570
294
|
handle,
|
|
571
|
-
|
|
572
|
-
bytes
|
|
295
|
+
nonce
|
|
573
296
|
});
|
|
574
297
|
rootState.root=rootState;
|
|
575
|
-
const lease=
|
|
576
|
-
workspaceRoot:workspace.canonical,
|
|
577
|
-
operation,
|
|
578
|
-
nonce,
|
|
579
|
-
lockPath
|
|
580
|
-
});
|
|
298
|
+
const lease={workspaceRoot:workspace.canonical,operation,nonce,lockPath};
|
|
581
299
|
rootState.lease=lease;
|
|
582
300
|
leaseStates.set(lease,rootState);
|
|
583
301
|
activeRoots.set(workspace.canonical,lease);
|
|
584
|
-
await onEvent?.(
|
|
302
|
+
await onEvent?.({
|
|
585
303
|
type:'workspace.operation.locked',
|
|
586
304
|
workspaceRoot:workspace.canonical,
|
|
587
305
|
operation
|
|
588
|
-
}));
|
|
589
|
-
throwIfAborted(signal);
|
|
590
|
-
await validateOwnedLock({
|
|
591
|
-
workspace,directory,lockPath,handle,identity,bytes,
|
|
592
|
-
label:'after the acquisition callback'
|
|
593
306
|
});
|
|
307
|
+
throwIfAborted(signal);
|
|
594
308
|
return {
|
|
595
309
|
inherited:false,
|
|
596
310
|
lease,
|
|
@@ -606,31 +320,20 @@ async function acquire({
|
|
|
606
320
|
let observerError;
|
|
607
321
|
if(child!==null)await child.settled;
|
|
608
322
|
try{
|
|
609
|
-
await
|
|
610
|
-
|
|
611
|
-
|
|
323
|
+
const removed=await removeOwnedLock({
|
|
324
|
+
lockPath,
|
|
325
|
+
workspaceRoot:workspace.canonical,
|
|
326
|
+
nonce,
|
|
327
|
+
handle
|
|
612
328
|
});
|
|
613
|
-
|
|
614
|
-
handle=
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
fail('Workspace operation lock path changed before cleanup.',
|
|
618
|
-
'ARCANE_POLICY_DENIED');
|
|
329
|
+
handle=null;
|
|
330
|
+
rootState.handle=null;
|
|
331
|
+
if(!removed){
|
|
332
|
+
fail('Workspace operation lock belongs to another owner at cleanup.');
|
|
619
333
|
}
|
|
620
334
|
}catch(error){
|
|
621
335
|
releaseError=error;
|
|
622
336
|
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
337
|
}finally{
|
|
635
338
|
if(activeRoots.get(workspace.canonical)===lease){
|
|
636
339
|
activeRoots.delete(workspace.canonical);
|
|
@@ -643,11 +346,11 @@ async function acquire({
|
|
|
643
346
|
}
|
|
644
347
|
if(releaseError)throw releaseError;
|
|
645
348
|
try{
|
|
646
|
-
await onEvent?.(
|
|
349
|
+
await onEvent?.({
|
|
647
350
|
type:'workspace.operation.released',
|
|
648
351
|
workspaceRoot:workspace.canonical,
|
|
649
352
|
operation
|
|
650
|
-
})
|
|
353
|
+
});
|
|
651
354
|
}catch(error){observerError=error;}
|
|
652
355
|
if(misuseError){
|
|
653
356
|
if(observerError)misuseError.observerError=observerError;
|
|
@@ -668,15 +371,21 @@ async function acquire({
|
|
|
668
371
|
||activeRoots.get(workspace.canonical)===rootState?.lease){
|
|
669
372
|
activeRoots.delete(workspace.canonical);
|
|
670
373
|
}
|
|
671
|
-
if(handle&&
|
|
374
|
+
if(handle&&lockPath&&nonce){
|
|
672
375
|
try{
|
|
673
|
-
|
|
674
|
-
|
|
376
|
+
await removeOwnedLock({
|
|
377
|
+
lockPath,
|
|
378
|
+
workspaceRoot:workspace.canonical,
|
|
379
|
+
nonce,
|
|
380
|
+
handle
|
|
381
|
+
});
|
|
382
|
+
handle=null;
|
|
675
383
|
}catch(cleanupError){
|
|
676
384
|
error.cleanupError=cleanupError;
|
|
677
385
|
}
|
|
386
|
+
}else{
|
|
387
|
+
await handle?.close().catch(()=>{});
|
|
678
388
|
}
|
|
679
|
-
await handle?.close().catch(()=>{});
|
|
680
389
|
throw error;
|
|
681
390
|
}
|
|
682
391
|
}
|