arcane-os 0.54.0 → 0.55.0
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 +40 -0
- package/README.md +31 -26
- package/browser-runtime/ai/browser-decisions.mjs +2 -1
- package/browser-runtime/core/client.mjs +491 -0
- package/browser-runtime/core/contracts.mjs +84 -0
- package/docs/architecture.md +31 -23
- package/docs/compatibility.md +10 -1
- package/docs/platform-targets.md +20 -11
- package/docs/reference/README.md +8 -6
- package/docs/reference/ai/browser-decisions.md +1 -1
- package/docs/reference/availability-and-normalization.md +4 -2
- package/docs/reference/behavioral-testing.md +7 -6
- package/docs/reference/cli.md +12 -4
- package/docs/reference/core-client.md +120 -0
- package/docs/reference/core-native-packaging.md +149 -0
- package/docs/reference/core-runtime.md +175 -0
- package/docs/reference/inventory/package-api.json +257 -5
- package/docs/reference/protocols.md +16 -1
- package/docs/reference/sdk-api.md +772 -15
- package/package.json +10 -3
- package/schemas/native-build-plan.schema.json +8 -0
- package/src/cli/main.mjs +8 -6
- package/src/core/host.mjs +23 -0
- package/src/core/runtime.mjs +333 -0
- package/src/core/stdio.mjs +209 -0
- package/src/core-classic-source.mjs +43 -0
- package/src/import-map.mjs +62 -0
- package/src/native/portable-layout.mjs +184 -0
- package/src/native/portable-provider.mjs +211 -0
- package/src/native-plan.mjs +10 -4
- package/src/native-provider-loader.mjs +15 -4
- package/src/packager/core.mjs +47 -18
- package/src/targets/index.mjs +3 -1
- package/src/toolchain.mjs +20 -5
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import {arcaneEvents} from '../event-manager.mjs';
|
|
2
|
+
import {CORE_PROTOCOL,CORE_READY_EVENTS,CoreError,serializeCoreError} from './contracts.mjs';
|
|
3
|
+
|
|
4
|
+
const CORE_CLIENT_KEY=Symbol.for('arcane-os.core.client');
|
|
5
|
+
const CORE_EVENT='core.rpc.event';
|
|
6
|
+
const LONG_OPERATION_TIMEOUT=50*60*1000;
|
|
7
|
+
|
|
8
|
+
/** One RPC client. Native execution and service policy belong to the host. */
|
|
9
|
+
export function createCoreClient({
|
|
10
|
+
global=globalThis,
|
|
11
|
+
transport:providedTransport=null,
|
|
12
|
+
eventOwner=arcaneEvents,
|
|
13
|
+
eventOwnerReady,
|
|
14
|
+
autoConnect=true,
|
|
15
|
+
onError=error=>global.console?.error('Arcane Core client failed.',error)
|
|
16
|
+
}={}){
|
|
17
|
+
const pending=new Map();
|
|
18
|
+
const acknowledgements=new Map();
|
|
19
|
+
const completedEvents=new Map();
|
|
20
|
+
const eventActions=[];
|
|
21
|
+
const cleanups=[];
|
|
22
|
+
const eventIdentity={};
|
|
23
|
+
let source=null;
|
|
24
|
+
let drainingEvents=false;
|
|
25
|
+
let transport=null;
|
|
26
|
+
let closed=false;
|
|
27
|
+
let eventOwnerFailure=null;
|
|
28
|
+
|
|
29
|
+
function report(error){
|
|
30
|
+
try{onError(error instanceof CoreError?error:new CoreError(error));}
|
|
31
|
+
catch(reportError){global.console?.error('Arcane Core error listener failed.',reportError,error);}
|
|
32
|
+
}
|
|
33
|
+
function uuid(){
|
|
34
|
+
return global.crypto?.randomUUID?.()
|
|
35
|
+
??`req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
36
|
+
}
|
|
37
|
+
function attachEventOwner(owner){
|
|
38
|
+
if(closed)return;
|
|
39
|
+
source=owner.createSource(eventIdentity,{source:'core-client',eventTypes:[CORE_EVENT]});
|
|
40
|
+
// This is an ordered handoff queue, not an event dispatcher. The SDK
|
|
41
|
+
// event owner alone manages subscriptions and delivery.
|
|
42
|
+
drainingEvents=true;
|
|
43
|
+
try{
|
|
44
|
+
while(eventActions.length&&!closed)eventActions.shift()();
|
|
45
|
+
}finally{
|
|
46
|
+
drainingEvents=false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function withEvents(action){
|
|
50
|
+
if(closed)return;
|
|
51
|
+
if(source&&!drainingEvents)action();
|
|
52
|
+
else eventActions.push(action);
|
|
53
|
+
}
|
|
54
|
+
if(eventOwner)attachEventOwner(eventOwner);
|
|
55
|
+
const eventsReady=eventOwner?Promise.resolve():Promise.resolve(eventOwnerReady).then(attachEventOwner);
|
|
56
|
+
eventsReady.catch(function failEventOwner(error){
|
|
57
|
+
eventOwnerFailure=error instanceof CoreError?error:new CoreError(error);
|
|
58
|
+
for(const id of pending.keys())settle(id,eventOwnerFailure);
|
|
59
|
+
report(eventOwnerFailure);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function emit(event,data){
|
|
63
|
+
withEvents(()=>source.dispatch(CORE_EVENT,{event,data}));
|
|
64
|
+
}
|
|
65
|
+
function complete(event,data){
|
|
66
|
+
if(completedEvents.has(event))return false;
|
|
67
|
+
completedEvents.set(event,data);
|
|
68
|
+
emit(event,data);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const events={
|
|
72
|
+
on(event,listener){
|
|
73
|
+
if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
|
|
74
|
+
let active=!closed;
|
|
75
|
+
let dispose=null;
|
|
76
|
+
withEvents(()=>{
|
|
77
|
+
if(!active)return;
|
|
78
|
+
dispose=source.on(CORE_EVENT,function receiveCoreEvent(occurrence){
|
|
79
|
+
if(!active)return;
|
|
80
|
+
const message=occurrence.detail;
|
|
81
|
+
if(event==='*')listener(message);
|
|
82
|
+
else if(event===message.event)listener(message.data);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
return function unsubscribe(){
|
|
86
|
+
if(!active)return false;
|
|
87
|
+
active=false;
|
|
88
|
+
dispose?.();
|
|
89
|
+
return true;
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
once(event,listener){
|
|
93
|
+
if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
|
|
94
|
+
const unsubscribe=events.on(event,function receiveOnce(value){
|
|
95
|
+
unsubscribe();
|
|
96
|
+
listener(value);
|
|
97
|
+
});
|
|
98
|
+
return unsubscribe;
|
|
99
|
+
},
|
|
100
|
+
when(event,listener){
|
|
101
|
+
if(!CORE_READY_EVENTS.includes(event))throw new TypeError('Arcane completion event is not designated as durable.');
|
|
102
|
+
if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
|
|
103
|
+
if(!completedEvents.has(event))return events.once(event,listener);
|
|
104
|
+
const value=completedEvents.get(event);
|
|
105
|
+
let active=!closed;
|
|
106
|
+
Promise.resolve().then(function replayReadyEvent(){
|
|
107
|
+
if(!active||closed)return;
|
|
108
|
+
active=false;
|
|
109
|
+
try{listener(value);}catch(error){report(error);}
|
|
110
|
+
});
|
|
111
|
+
return function unsubscribe(){active=false;};
|
|
112
|
+
},
|
|
113
|
+
completed(event){return CORE_READY_EVENTS.includes(event)&&completedEvents.has(event);}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function settle(id,error,result){
|
|
117
|
+
const request=pending.get(id);
|
|
118
|
+
if(!request)return false;
|
|
119
|
+
pending.delete(id);
|
|
120
|
+
clearTimeout(request.timer);
|
|
121
|
+
request.signal?.removeEventListener('abort',request.abort);
|
|
122
|
+
if(error)request.reject(error);
|
|
123
|
+
else request.resolve(result);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function receive(input){
|
|
127
|
+
if(closed)return false;
|
|
128
|
+
let message=input;
|
|
129
|
+
if(typeof message==='string'){
|
|
130
|
+
try{message=JSON.parse(message);}
|
|
131
|
+
catch(error){report(new CoreError({code:'ARCANE_INVALID_NATIVE_JSON',message:error.message,input,cause:error}));return false;}
|
|
132
|
+
}
|
|
133
|
+
if(!message||message.protocol!==CORE_PROTOCOL)return false;
|
|
134
|
+
if(message.type==='event'){
|
|
135
|
+
const data=Object.hasOwn(message,'data')?message.data:{};
|
|
136
|
+
if(CORE_READY_EVENTS.includes(message.event))complete(message.event,data);
|
|
137
|
+
else emit(message.event,data);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if(message.type!=='response'||!pending.has(message.id))return false;
|
|
141
|
+
// Final responses follow earlier event deliveries, including while a
|
|
142
|
+
// classic host is connecting the shared SDK event owner. A stream's
|
|
143
|
+
// completion must not remove its subscription before its chunks arrive.
|
|
144
|
+
withEvents(()=>settle(message.id,message.ok?null:new CoreError(message.error),message.result));
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
function installCallback(name,value){
|
|
148
|
+
const previous=global[name];
|
|
149
|
+
global[name]=value;
|
|
150
|
+
cleanups.push(function restoreCallback(){
|
|
151
|
+
if(global[name]!==value)return;
|
|
152
|
+
if(previous===undefined)delete global[name];
|
|
153
|
+
else global[name]=previous;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function parseAcknowledgement(input,{required=false}={}){
|
|
157
|
+
let acknowledgement=input;
|
|
158
|
+
if(typeof acknowledgement==='string'){
|
|
159
|
+
try{acknowledgement=JSON.parse(acknowledgement);}catch{acknowledgement=null;}
|
|
160
|
+
}
|
|
161
|
+
if(acknowledgement?.accepted===false){
|
|
162
|
+
throw new CoreError(acknowledgement.error??{code:'BRIDGE_REJECTED',message:'The native host rejected the request.'});
|
|
163
|
+
}
|
|
164
|
+
if(required&&acknowledgement?.accepted!==true){
|
|
165
|
+
throw new CoreError({code:'NATIVE_BRIDGE_REPLY_INVALID',message:'The native host returned an invalid acknowledgement.'});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function acknowledge(token,value){
|
|
169
|
+
const record=acknowledgements.get(token);
|
|
170
|
+
if(!record)return false;
|
|
171
|
+
acknowledgements.delete(token);
|
|
172
|
+
clearTimeout(record.timer);
|
|
173
|
+
try{parseAcknowledgement(value,{required:true});record.resolve();}
|
|
174
|
+
catch(error){record.reject(error);}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
function chooseTransport(){
|
|
178
|
+
const webview=global.chrome?.webview;
|
|
179
|
+
if(webview?.hostObjects){
|
|
180
|
+
const bridge=webview.hostObjects.arcaneBridge;
|
|
181
|
+
function nativeMessage(event){receive(event.data);}
|
|
182
|
+
webview.addEventListener('message',nativeMessage);
|
|
183
|
+
cleanups.push(()=>webview.removeEventListener?.('message',nativeMessage));
|
|
184
|
+
return {name:'webview2',async send(frame){
|
|
185
|
+
try{parseAcknowledgement(await bridge.Send(JSON.stringify(frame)));}
|
|
186
|
+
catch(error){
|
|
187
|
+
if(error instanceof CoreError)throw error;
|
|
188
|
+
const details=serializeCoreError(error);
|
|
189
|
+
if(error?.code===undefined)details.code='ARCANE_BRIDGE_CALL_FAILED';
|
|
190
|
+
throw new CoreError({
|
|
191
|
+
method:frame.method,transport:'webview2',...details
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}};
|
|
195
|
+
}
|
|
196
|
+
const webkit=global.webkit?.messageHandlers?.arcane;
|
|
197
|
+
if(typeof global.__arcaneWebKitPostMessage==='function'||webkit){
|
|
198
|
+
installCallback('__arcaneReceive',receive);
|
|
199
|
+
installCallback('__arcaneWebKitAcknowledge',acknowledge);
|
|
200
|
+
return {name:'webkitgtk',async send(frame){
|
|
201
|
+
const serialized=JSON.stringify(frame);
|
|
202
|
+
if(typeof global.__arcaneWebKitPostMessage!=='function'){
|
|
203
|
+
parseAcknowledgement(await webkit.postMessage(serialized),{required:true});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const token=uuid();
|
|
207
|
+
return new Promise(function sendWebKitFrame(resolve,reject){
|
|
208
|
+
const timer=setTimeout(function missingAcknowledgement(){
|
|
209
|
+
acknowledgements.delete(token);
|
|
210
|
+
reject(new CoreError({code:'NATIVE_BRIDGE_ACK_TIMEOUT',message:'The native host did not acknowledge the request.'}));
|
|
211
|
+
},30000);
|
|
212
|
+
acknowledgements.set(token,{resolve,reject,timer});
|
|
213
|
+
try{
|
|
214
|
+
if(global.__arcaneWebKitPostMessage(token,serialized)!==true){
|
|
215
|
+
throw new CoreError({code:'NATIVE_BRIDGE_UNTRUSTED_MAIN_FRAME',message:'The native host did not accept the request.'});
|
|
216
|
+
}
|
|
217
|
+
}catch(error){
|
|
218
|
+
acknowledgements.delete(token);
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
reject(error instanceof CoreError?error:new CoreError(error));
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}};
|
|
224
|
+
}
|
|
225
|
+
const android=global.arcaneAndroid;
|
|
226
|
+
if(typeof android?.postMessage==='function'){
|
|
227
|
+
const previous=android.onmessage;
|
|
228
|
+
function androidMessage(message){
|
|
229
|
+
receive(message&&typeof message==='object'&&'data' in message?message.data:message);
|
|
230
|
+
}
|
|
231
|
+
android.onmessage=androidMessage;
|
|
232
|
+
cleanups.push(()=>{if(android.onmessage===androidMessage)android.onmessage=previous;});
|
|
233
|
+
return {name:'android-webview',async send(frame){
|
|
234
|
+
try{await android.postMessage(JSON.stringify(frame));}
|
|
235
|
+
catch(error){
|
|
236
|
+
const details=serializeCoreError(error);
|
|
237
|
+
if(error?.code===undefined)details.code='ARCANE_ANDROID_BRIDGE_CALL_FAILED';
|
|
238
|
+
throw new CoreError({
|
|
239
|
+
method:frame.method,transport:'android-webview',...details
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}};
|
|
243
|
+
}
|
|
244
|
+
if(global.__ARCANE_DEV_HTTP__){
|
|
245
|
+
if(typeof global.EventSource==='function'){
|
|
246
|
+
const stream=new global.EventSource('/events');
|
|
247
|
+
stream.onmessage=event=>receive(event.data);
|
|
248
|
+
stream.onerror=event=>report(new CoreError({code:'DEV_BRIDGE_EVENT_ERROR',message:'The development event connection reported an error.',event}));
|
|
249
|
+
cleanups.push(()=>stream.close());
|
|
250
|
+
}
|
|
251
|
+
return {name:'development-http',async send(frame){
|
|
252
|
+
const response=await global.fetch('/rpc',{
|
|
253
|
+
method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(frame)
|
|
254
|
+
});
|
|
255
|
+
const payload=await response.json();
|
|
256
|
+
if(!response.ok)throw new CoreError(payload?.error??{code:'DEV_BRIDGE_FAILED',message:'The development bridge failed.'});
|
|
257
|
+
if(payload?.type==='response')receive(payload);
|
|
258
|
+
}};
|
|
259
|
+
}
|
|
260
|
+
throw new CoreError({code:'ARCANE_TRANSPORT_UNAVAILABLE',message:'This interface is not connected to an Arcane native host.'});
|
|
261
|
+
}
|
|
262
|
+
function connect(){
|
|
263
|
+
if(closed)throw new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
|
|
264
|
+
if(transport)return transport;
|
|
265
|
+
const cleanupStart=cleanups.length;
|
|
266
|
+
try{
|
|
267
|
+
const selected=providedTransport??chooseTransport();
|
|
268
|
+
if(typeof selected?.send!=='function'){
|
|
269
|
+
throw new CoreError({code:'ARCANE_TRANSPORT_INVALID',message:'The Core transport must provide send(frame).'});
|
|
270
|
+
}
|
|
271
|
+
transport=selected;
|
|
272
|
+
if(typeof transport.subscribe==='function'){
|
|
273
|
+
const unsubscribe=transport.subscribe(receive);
|
|
274
|
+
if(typeof unsubscribe==='function')cleanups.push(unsubscribe);
|
|
275
|
+
}
|
|
276
|
+
}catch(error){
|
|
277
|
+
transport=null;
|
|
278
|
+
for(const cleanup of cleanups.splice(cleanupStart)){
|
|
279
|
+
try{cleanup();}catch(cleanupError){report(cleanupError);}
|
|
280
|
+
}
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
complete('transport.ready',{protocol:CORE_PROTOCOL,transport:transport.name});
|
|
284
|
+
return transport;
|
|
285
|
+
}
|
|
286
|
+
function runtimeSnapshot(){
|
|
287
|
+
const name=transport?.name??providedTransport?.name
|
|
288
|
+
??(global.chrome?.webview?.hostObjects?'webview2'
|
|
289
|
+
:typeof global.__arcaneWebKitPostMessage==='function'||global.webkit?.messageHandlers?.arcane?'webkitgtk'
|
|
290
|
+
:typeof global.arcaneAndroid?.postMessage==='function'?'android-webview'
|
|
291
|
+
:global.__ARCANE_DEV_HTTP__?'development-http':'standalone');
|
|
292
|
+
return {connected:transport!==null&&!closed,transport:name,
|
|
293
|
+
native:['webview2','webkitgtk','android-webview'].includes(name),
|
|
294
|
+
managedLocalAI:['webview2','webkitgtk'].includes(name)};
|
|
295
|
+
}
|
|
296
|
+
function sendControl(frame){
|
|
297
|
+
if(!transport)return;
|
|
298
|
+
try{Promise.resolve(transport.send(frame)).catch(report);}catch(error){report(error);}
|
|
299
|
+
}
|
|
300
|
+
function aborted(method){
|
|
301
|
+
return new CoreError({name:'AbortError',code:'ARCANE_REQUEST_ABORTED',message:'The Arcane request was cancelled.',method});
|
|
302
|
+
}
|
|
303
|
+
function cancel(id,error){
|
|
304
|
+
if(!settle(id,error))return false;
|
|
305
|
+
sendControl({protocol:CORE_PROTOCOL,type:'control',control:'request.cancel',requestId:id});
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function cancelAll(){
|
|
309
|
+
if(!pending.size)return false;
|
|
310
|
+
for(const [id,request] of pending)settle(id,aborted(request.method));
|
|
311
|
+
sendControl({protocol:CORE_PROTOCOL,type:'control',control:'requests.cancelAll'});
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
function invoke(method,parameters={},options={}){
|
|
315
|
+
if(eventOwnerFailure)return Promise.reject(eventOwnerFailure);
|
|
316
|
+
let selected;
|
|
317
|
+
try{selected=connect();}catch(error){return Promise.reject(error);}
|
|
318
|
+
const {signal,timeoutMs=10*60*1000}=options??{};
|
|
319
|
+
if(signal&&(typeof signal.aborted!=='boolean'
|
|
320
|
+
||typeof signal.addEventListener!=='function'
|
|
321
|
+
||typeof signal.removeEventListener!=='function')){
|
|
322
|
+
return Promise.reject(new TypeError('Arcane request signal must be an AbortSignal.'));
|
|
323
|
+
}
|
|
324
|
+
if(signal?.aborted)return Promise.reject(aborted(method));
|
|
325
|
+
const id=uuid();
|
|
326
|
+
const frame={protocol:CORE_PROTOCOL,type:'request',id,method,parameters,sentAt:new Date().toISOString()};
|
|
327
|
+
const promise=new Promise(function ownRequest(resolve,reject){
|
|
328
|
+
const abort=()=>cancel(id,aborted(method));
|
|
329
|
+
const timer=timeoutMs>0?setTimeout(()=>cancel(id,new CoreError({
|
|
330
|
+
code:'ARCANE_REQUEST_TIMEOUT',message:'Arcane did not finish the operation before the request timed out.',method
|
|
331
|
+
})),timeoutMs):null;
|
|
332
|
+
pending.set(id,{resolve,reject,timer,signal,abort,method});
|
|
333
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
334
|
+
if(signal?.aborted)abort();
|
|
335
|
+
});
|
|
336
|
+
if(!pending.has(id))return promise;
|
|
337
|
+
function sendFailed(error){
|
|
338
|
+
const failure=error instanceof CoreError?error:new CoreError(error);
|
|
339
|
+
if(!settle(id,failure))report(failure);
|
|
340
|
+
}
|
|
341
|
+
try{
|
|
342
|
+
Promise.resolve(selected.send(frame)).catch(sendFailed);
|
|
343
|
+
}catch(error){sendFailed(error);}
|
|
344
|
+
return promise;
|
|
345
|
+
}
|
|
346
|
+
function close(){
|
|
347
|
+
if(closed)return false;
|
|
348
|
+
closed=true;
|
|
349
|
+
cancelAll();
|
|
350
|
+
for(const record of acknowledgements.values()){
|
|
351
|
+
clearTimeout(record.timer);
|
|
352
|
+
record.reject(new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'}));
|
|
353
|
+
}
|
|
354
|
+
acknowledgements.clear();
|
|
355
|
+
for(const cleanup of cleanups.splice(0)){
|
|
356
|
+
try{cleanup();}catch(error){report(error);}
|
|
357
|
+
}
|
|
358
|
+
source?.dispose();
|
|
359
|
+
// Registration closures and already-delivered payloads belong to this
|
|
360
|
+
// client lifetime and are released when the owner explicitly closes it.
|
|
361
|
+
eventActions.length=0;
|
|
362
|
+
completedEvents.clear();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if(typeof global.addEventListener==='function'){
|
|
366
|
+
global.addEventListener('pagehide',cancelAll,{capture:true});
|
|
367
|
+
cleanups.push(()=>global.removeEventListener('pagehide',cancelAll,{capture:true}));
|
|
368
|
+
}
|
|
369
|
+
if(autoConnect){
|
|
370
|
+
try{connect();}catch(error){if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE')report(error);}
|
|
371
|
+
}
|
|
372
|
+
return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Existing namespace call shapes; method implementations remain host-owned. */
|
|
376
|
+
export function createCoreFacade(client){
|
|
377
|
+
const {invoke,events,uuid}=client;
|
|
378
|
+
const long={timeoutMs:LONG_OPERATION_TIMEOUT};
|
|
379
|
+
function ollamaInvoke(operation,request={},options={}){
|
|
380
|
+
const onChunk=typeof options==='function'?options:options?.onChunk;
|
|
381
|
+
const timeoutMs=options?.timeoutMs??(['pull','push','create'].includes(operation)?LONG_OPERATION_TIMEOUT:10*60*1000);
|
|
382
|
+
if(typeof onChunk!=='function')return invoke(`ollama.${operation}`,request,{timeoutMs,signal:options?.signal});
|
|
383
|
+
const streamId=uuid();
|
|
384
|
+
const unsubscribe=events.on('ollama.chunk',function streamChunk(event){
|
|
385
|
+
if(event?.streamId===streamId)onChunk(event.chunk,{operation,streamId});
|
|
386
|
+
});
|
|
387
|
+
return invoke(`ollama.${operation}`,{...request,stream:true,streamId},{timeoutMs,signal:options?.signal}).finally(unsubscribe);
|
|
388
|
+
}
|
|
389
|
+
function environmentNameIsSensitive(name){
|
|
390
|
+
const canonical=String(name??'').toUpperCase();
|
|
391
|
+
return canonical.split(/[_.-]+/u).some(token=>[
|
|
392
|
+
'AUTH','BEARER','CREDENTIAL','CREDENTIALS','KEY','KEYS','PASS','PASSWD','PASSWORD','PASSWORDS','PWD','SECRET','SECRETS','TOKEN','TOKENS'
|
|
393
|
+
].includes(token))||['ACCESSKEY','ACCESSTOKEN','APIKEY','AUTHKEY','AUTHTOKEN','CLIENTSECRET','PRIVATEKEY'].some(suffix=>canonical.endsWith(suffix));
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
protocol:CORE_PROTOCOL,Error:CoreError,runtime:client.runtime,events,
|
|
397
|
+
ai:{
|
|
398
|
+
models:()=>invoke('ai.models'),chat:request=>invoke('ai.chat',request??{},{timeoutMs:130000}),
|
|
399
|
+
profile:()=>invoke('ai.profile.current'),providerSettings:()=>invoke('ai.provider.settings.get'),
|
|
400
|
+
saveProviderSettings:settings=>invoke('ai.provider.settings.set',settings??{},{timeoutMs:130000}),
|
|
401
|
+
providerModels:()=>invoke('ai.provider.models',{},{timeoutMs:130000})
|
|
402
|
+
},
|
|
403
|
+
environment:{list:()=>invoke('environment.list'),get:name=>invoke('environment.get',{name}),
|
|
404
|
+
set:(name,value,options)=>invoke('environment.set',{name,value,protected:options&&Object.hasOwn(options,'protected')?options.protected:environmentNameIsSensitive(name)}),
|
|
405
|
+
remove:name=>invoke('environment.delete',{name})},
|
|
406
|
+
mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
|
|
407
|
+
speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
|
|
408
|
+
synthesize:request=>invoke('speech.synthesize',request??{},{timeoutMs:180000}),
|
|
409
|
+
transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
|
|
410
|
+
localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
|
|
411
|
+
ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
|
|
412
|
+
setParallelRequests:request=>invoke('localai.parallel.requests.set',request??{},long),
|
|
413
|
+
inspectIsolatedModel:request=>invoke('localai.isolated.inspect',request??{},{timeoutMs:45000}),
|
|
414
|
+
runIsolatedQuestion(request={},options={}){
|
|
415
|
+
const operationId=uuid();
|
|
416
|
+
const unsubscribe=typeof options?.onPhase==='function'?events.on('localai.isolated.phase',event=>{
|
|
417
|
+
if(event?.operationId===operationId)options.onPhase(event.phase,event);
|
|
418
|
+
}):()=>{};
|
|
419
|
+
return invoke('localai.isolated.question',{...request,operationId},long).finally(unsubscribe);
|
|
420
|
+
}},
|
|
421
|
+
ollama:{version:()=>invoke('ollama.version'),models:()=>invoke('ollama.models'),list:()=>invoke('ollama.models'),
|
|
422
|
+
running:()=>invoke('ollama.running'),show:(model,options={})=>invoke('ollama.show',{...options,model}),
|
|
423
|
+
generate:(request,options)=>ollamaInvoke('generate',request,options),chat:(request,options)=>ollamaInvoke('chat',request,options),
|
|
424
|
+
embed:request=>invoke('ollama.embed',request??{}),
|
|
425
|
+
pull:(model,options={},streamOptions)=>ollamaInvoke('pull',{...options,model},streamOptions),
|
|
426
|
+
push:(model,options={},streamOptions)=>ollamaInvoke('push',{...options,model},streamOptions),
|
|
427
|
+
create:(request,options)=>ollamaInvoke('create',request,options),
|
|
428
|
+
copy:(source,destination)=>invoke('ollama.copy',{source,destination},{timeoutMs:120000}),
|
|
429
|
+
delete:model=>invoke('ollama.delete',{model},{timeoutMs:120000}),selection:()=>invoke('ollama.selection.get'),
|
|
430
|
+
select:preference=>invoke('ollama.selection.set',{preference:preference??'auto'},long),settings:()=>invoke('ollama.settings.get'),
|
|
431
|
+
saveSettings:settings=>invoke('ollama.settings.set',settings??{},long),createBrain:definition=>invoke('ollama.brain.create',definition??{},long),
|
|
432
|
+
serviceSettings:()=>invoke('ollama.service.settings.get'),saveServiceSettings:settings=>invoke('ollama.service.settings.set',settings??{},long)},
|
|
433
|
+
app:{current:()=>invoke('app.current')},applications:{list:()=>invoke('apps.list'),launch:id=>invoke('apps.launch',{id})},
|
|
434
|
+
external:{open:uri=>invoke('external.open',{uri})},
|
|
435
|
+
terminal:{start:(options={})=>invoke('terminal.start',{shell:options?.shell??'auto',cwd:options?.cwd??'',columns:options?.columns??120,rows:options?.rows??32}),
|
|
436
|
+
list:()=>invoke('terminal.list'),write:(sessionId,data)=>invoke('terminal.write',{sessionId,data}),
|
|
437
|
+
resize:(sessionId,columns,rows)=>invoke('terminal.resize',{sessionId,columns,rows}),
|
|
438
|
+
signal:(sessionId,signal='interrupt')=>invoke('terminal.signal',{sessionId,signal}),close:sessionId=>invoke('terminal.close',{sessionId})},
|
|
439
|
+
capabilities:{list:()=>invoke('capabilities.list')},platform:{status:()=>invoke('platform.status')},permissions:{status:()=>invoke('permissions.status')},
|
|
440
|
+
version:{current:()=>invoke('version.current'),installation:()=>invoke('installation.status')},machine:{status:()=>invoke('machine.status')},user:{current:()=>invoke('user.current')},
|
|
441
|
+
requirements:{list:()=>invoke('requirements.list'),ensure:(requirementIds,options={})=>invoke('requirements.ensure',{
|
|
442
|
+
requirementIds,userProcessInterruption:options?.userProcessInterruption??'deny'
|
|
443
|
+
},long)},
|
|
444
|
+
installation:{status:()=>invoke('installation.status'),ensure:()=>invoke('installation.ensure',{},long),openUninstaller:()=>invoke('installation.uninstaller.open')},
|
|
445
|
+
users:{list:()=>invoke('users.list'),validate:usernames=>invoke('users.validate',{usernames:Array.isArray(usernames)?usernames:[usernames]}),
|
|
446
|
+
add:usernames=>invoke('users.add',{usernames:Array.isArray(usernames)?usernames:[usernames]},long),
|
|
447
|
+
activate:username=>invoke('users.activate',{username},long),resetPassword:username=>invoke('users.resetPassword',{username},long),
|
|
448
|
+
applyPassword:(username,temporaryPassword)=>invoke('users.applyPassword',{username,temporaryPassword},long),
|
|
449
|
+
verifyShell:username=>invoke('users.verifyShell',{username},long),restoreShell:username=>invoke('users.restoreShell',{username},long)},
|
|
450
|
+
system:{lock:()=>invoke('system.lock'),ping:()=>invoke('system.ping',{},{timeoutMs:10000}),metrics:()=>invoke('system.metrics'),
|
|
451
|
+
failurePolicy:()=>invoke('system.failurePolicy.get'),saveFailurePolicy:settings=>invoke('system.failurePolicy.set',settings??{})},
|
|
452
|
+
network:{status:()=>invoke('network.status')},filesystem:{selectDirectory:(options={})=>invoke('filesystem.directory.select',options,long)},
|
|
453
|
+
storage:{list:()=>invoke('storage.list'),get:key=>invoke('storage.get',{key}),set:(key,value)=>invoke('storage.set',{key,value}),delete:key=>invoke('storage.delete',{key})},
|
|
454
|
+
preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
|
|
455
|
+
setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
|
|
456
|
+
appearance:{current:()=>invoke('appearance.current'),apply:appearance=>invoke('appearance.apply',appearance??{})},session:{logout:()=>invoke('session.logout')},
|
|
457
|
+
provisioning:{plan:usernames=>invoke('provisioning.plan',{usernames:Array.isArray(usernames)?usernames:[usernames].filter(Boolean)})},
|
|
458
|
+
diagnostics:{recentErrors:()=>invoke('diagnostics.recent'),get:diagnosticId=>invoke('diagnostics.get',{diagnosticId})},
|
|
459
|
+
development:{inspect:root=>invoke('development.inspect',{root}),context:(root,query)=>invoke('development.context',{root,query},{timeoutMs:130000}),
|
|
460
|
+
setup:(root,taskId)=>invoke('development.setup',{root,taskId},long),installNode:()=>invoke('development.node.install',{},long)}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Installs the same synchronous facade used by classic native-host scripts. */
|
|
465
|
+
export function installCoreClient(global=globalThis,options={}){
|
|
466
|
+
if(global[CORE_CLIENT_KEY])return global[CORE_CLIENT_KEY];
|
|
467
|
+
const client=createCoreClient({...options,global,autoConnect:false});
|
|
468
|
+
const facade=createCoreFacade(client);
|
|
469
|
+
global.Arcane=facade;
|
|
470
|
+
global[CORE_CLIENT_KEY]=client;
|
|
471
|
+
const previous=global.__arcaneReceive;
|
|
472
|
+
global.__arcaneReceive=client.receive;
|
|
473
|
+
const close=client.close;
|
|
474
|
+
client.close=function closeInstalledClient(){
|
|
475
|
+
const changed=close();
|
|
476
|
+
if(global[CORE_CLIENT_KEY]===client)delete global[CORE_CLIENT_KEY];
|
|
477
|
+
if(global.Arcane===facade)delete global.Arcane;
|
|
478
|
+
if(global.__arcaneReceive===client.receive){
|
|
479
|
+
if(previous===undefined)delete global.__arcaneReceive;
|
|
480
|
+
else global.__arcaneReceive=previous;
|
|
481
|
+
}
|
|
482
|
+
return changed;
|
|
483
|
+
};
|
|
484
|
+
try{client.connect();}catch(error){
|
|
485
|
+
if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE'){
|
|
486
|
+
if(options.onError)options.onError(error);
|
|
487
|
+
else global.console?.error('Arcane Core transport initialization failed.',error);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return client;
|
|
491
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const CORE_PROTOCOL='arcane/1';
|
|
2
|
+
export const CORE_READY_EVENTS=['transport.ready','core.ready'];
|
|
3
|
+
|
|
4
|
+
/** Wire descriptions, not method admission or application policy. */
|
|
5
|
+
export const CORE_FRAME_CONTRACTS={
|
|
6
|
+
request:{type:'request',fields:['protocol','type','id','method','parameters','sentAt']},
|
|
7
|
+
response:{type:'response',fields:['protocol','type','id','ok'],success:['result'],failure:['error'],optional:['time']},
|
|
8
|
+
event:{type:'event',fields:['protocol','type','event','data'],optional:['time']},
|
|
9
|
+
cancel:{type:'control',control:'request.cancel',fields:['protocol','type','control','requestId']},
|
|
10
|
+
cancelAll:{type:'control',control:'requests.cancelAll',fields:['protocol','type','control']}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** These names describe existing methods; installed services determine availability. */
|
|
14
|
+
export const CORE_METHOD_CONTRACTS={
|
|
15
|
+
'system.ping':{input:'empty-object-v1',output:'system-ping-result-v1',meaning:'Dispatcher response, not application or model readiness.'},
|
|
16
|
+
'version.current':{input:'empty-object-v1',output:'version-string-v1',meaning:'Active host release version.'},
|
|
17
|
+
'app.current':{input:'empty-object-v1',output:'application-descriptor-v1'},
|
|
18
|
+
'capabilities.list':{input:'empty-object',output:'{app,grants,methods}'},
|
|
19
|
+
'platform.status':{input:'empty-object-v1',output:'platform-status-v1'},
|
|
20
|
+
'network.status':{input:'empty-object-v1',output:'network-status-v1',meaning:'A non-loopback interface is present; this is not proof of Internet reachability.'},
|
|
21
|
+
'apps.list':{input:'empty-object-v1',output:'application-catalog-v1'},
|
|
22
|
+
'apps.launch':{input:'application-launch-v1',output:'application-launch-result-v1',meaning:'Host acceptance, not application readiness.'},
|
|
23
|
+
'external.open':{input:'external-open-v1',output:'external-open-result-v1',meaning:'Host acceptance, not confirmation of a user action.'},
|
|
24
|
+
'filesystem.directory.select':{input:'directory-selection-options',output:'directory-selection-result'},
|
|
25
|
+
'storage.list':{input:'empty-object',output:'{keys}'},
|
|
26
|
+
'storage.get':{input:'{key}',output:'{key,found,value}'},
|
|
27
|
+
'storage.set':{input:'{key,value}',output:'{key,value}'},
|
|
28
|
+
'storage.delete':{input:'{key}',output:'{key,deleted}'},
|
|
29
|
+
'preferences.list':{input:'empty-object',output:'{keys}'},
|
|
30
|
+
'preferences.get':{input:'{key}',output:'{key,found,value}'},
|
|
31
|
+
'preferences.set':{input:'{key,value}',output:'{key,value}'},
|
|
32
|
+
'preferences.setMany':{input:'{entries}',output:'preference-batch-result'},
|
|
33
|
+
'preferences.delete':{input:'{key}',output:'{key,deleted}'},
|
|
34
|
+
'terminal.start':{input:'terminal-start-v1',output:'terminal-session-v1'},
|
|
35
|
+
'terminal.list':{input:'empty-object-v1',output:'terminal-list-v1'},
|
|
36
|
+
'terminal.write':{input:'terminal-write-v1',output:'terminal-write-result-v1'},
|
|
37
|
+
'terminal.resize':{input:'terminal-resize-v1',output:'terminal-resize-result-v1'},
|
|
38
|
+
'terminal.signal':{input:'terminal-signal-v1',output:'terminal-signal-result-v1'},
|
|
39
|
+
'terminal.close':{input:'terminal-session-id-v1',output:'terminal-close-result-v1'},
|
|
40
|
+
'environment.list':{input:'empty-object-v1',output:'environment-list-result-v1'},
|
|
41
|
+
'environment.get':{input:'environment-name-v1',output:'environment-get-result-v1'},
|
|
42
|
+
'environment.set':{input:'environment-set-v1',output:'environment-set-result-v1'},
|
|
43
|
+
'environment.delete':{input:'environment-name-v1',output:'environment-delete-result-v1'},
|
|
44
|
+
'mail.send':{input:'mail-send-v1',output:'mail-send-result-v1'},
|
|
45
|
+
'user.current':{input:'empty-object-v1',output:'user-identity-v1'}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class CoreError extends Error{
|
|
49
|
+
constructor(value){
|
|
50
|
+
const source=value!==null&&typeof value==='object'
|
|
51
|
+
?value
|
|
52
|
+
:{message:value===undefined?'Arcane operation failed.':String(value)};
|
|
53
|
+
super(source.message??source.userMessage??'Arcane operation failed.');
|
|
54
|
+
for(const key of Object.getOwnPropertyNames(source))this[key]=source[key];
|
|
55
|
+
this.name=source.name??'ArcaneError';
|
|
56
|
+
this.code=source.code??'ARCANE_ERROR';
|
|
57
|
+
this.resolution=source.resolution??null;
|
|
58
|
+
this.diagnosticId=source.diagnosticId??null;
|
|
59
|
+
this.technicalMessage=source.technicalMessage??source.message??null;
|
|
60
|
+
const hresult=String(source.message??'').match(/\b0x[0-9a-f]{8}\b/iu);
|
|
61
|
+
this.hresult=source.hresult??hresult?.[0]?.toUpperCase()??null;
|
|
62
|
+
this.causeName=source.causeName??(source.name&&source.name!=='ArcaneError'?source.name:null);
|
|
63
|
+
if(source.stack!==undefined)this.stack=source.stack;
|
|
64
|
+
if(source.cause!==undefined)this.cause=source.cause;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function serializeCoreError(error){
|
|
69
|
+
if(error===null||typeof error!=='object'){
|
|
70
|
+
return {name:'Error',code:'ARCANE_ERROR',message:String(error)};
|
|
71
|
+
}
|
|
72
|
+
const record=Object.fromEntries(Object.getOwnPropertyNames(error).map(key=>[key,error[key]]));
|
|
73
|
+
for(const key of ['name','message','stack','cause']){
|
|
74
|
+
if(error[key]!==undefined)record[key]=key==='cause'&&error[key] instanceof Error
|
|
75
|
+
?serializeCoreError(error[key]):error[key];
|
|
76
|
+
}
|
|
77
|
+
if(Array.isArray(error.errors)){
|
|
78
|
+
record.errors=error.errors.map(value=>value instanceof Error?serializeCoreError(value):value);
|
|
79
|
+
}
|
|
80
|
+
if(record.name===undefined)record.name='Error';
|
|
81
|
+
if(record.code===undefined)record.code='ARCANE_ERROR';
|
|
82
|
+
if(record.message===undefined)record.message='Arcane operation failed.';
|
|
83
|
+
return record;
|
|
84
|
+
}
|