arcane-os 0.54.1 → 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 +31 -0
- package/README.md +31 -26
- 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/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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.55.0
|
|
4
|
+
|
|
5
|
+
- Add public Core client, protocol contracts and classic-script source generation
|
|
6
|
+
through `arcane-os/core/client`, `core/contracts` and `core/classic-source`.
|
|
7
|
+
Native facades install synchronously; the shared event owner retains ordered
|
|
8
|
+
early events and responses. Correlation, complete payloads and diagnostics,
|
|
9
|
+
cancellation and transport-versus-service readiness remain explicit.
|
|
10
|
+
- Add app-neutral native Core runtime and stdio transport through
|
|
11
|
+
`arcane-os/core/runtime` and `core/stdio`. Hosts register services, start them
|
|
12
|
+
independently and distinguish cancellable renderer requests from accepted
|
|
13
|
+
service-lifetime work. Graceful shutdown waits for accepted responses, service
|
|
14
|
+
drain/disposal and queued transport output without terminating the host.
|
|
15
|
+
- Add SDK-owned portable native payload assembly and explicit application-service
|
|
16
|
+
composition through `arcane-os/native/portable-provider` and `core/host`.
|
|
17
|
+
Portable provider loading no longer requires an Arcane OS checkout; explicit
|
|
18
|
+
checkout providers remain supported. Preserve complete selected releases,
|
|
19
|
+
dependency records and the application's actual SDK version separately from
|
|
20
|
+
the builder, including partial direct browser package projections. Portable
|
|
21
|
+
output declares the host's serving, client injection and stdio responsibilities;
|
|
22
|
+
it is not an executable platform host.
|
|
23
|
+
- Preserve Core client FIFO order during deferred event-owner handoff, report
|
|
24
|
+
asynchronous send failures after request settlement, retain original native
|
|
25
|
+
diagnostic fields and require callable transport sending before readiness.
|
|
26
|
+
- Correct disabled-PWA packaging so selected output documents lose active SDK
|
|
27
|
+
manifest and bootstrap references before native-module processing. Preserve
|
|
28
|
+
source PWA settings, unrelated authored links, inactive HTML and unselected
|
|
29
|
+
documents; enabled PWA generation keeps its existing behavior.
|
|
30
|
+
- Executable platform hosts and application host cutover remain separate work.
|
|
31
|
+
Applications retain service composition, model selection and saved settings.
|
|
32
|
+
No new dependency, automatic model loading or data migration is added.
|
|
33
|
+
|
|
3
34
|
## 0.54.1
|
|
4
35
|
|
|
5
36
|
- Select the self-contained Transformers.js 4.3.0 browser CDN bundle for
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.55.0` SDK contract. Applications pin one exact npm
|
|
24
24
|
version and lockfile; registry state is deliberately not baked into application
|
|
25
25
|
artifacts.
|
|
26
26
|
|
|
@@ -594,10 +594,11 @@ Arcane root, shared, and native tests require the explicit shared focused-test
|
|
|
594
594
|
form above. External app repositories retain their root `test/` plus selected
|
|
595
595
|
app-test behavior.
|
|
596
596
|
|
|
597
|
-
Integrated native app builds
|
|
598
|
-
`--
|
|
599
|
-
|
|
600
|
-
|
|
597
|
+
Integrated native app builds select one `--app` and one declared native target,
|
|
598
|
+
and `--output-root` must resolve outside that checkout. An explicit
|
|
599
|
+
`--arcane-root` must resolve to the same Arcane checkout as `--workspace`;
|
|
600
|
+
the default portable provider instead comes from the installed SDK package.
|
|
601
|
+
The SDK then uses the same package, plan, provider, and
|
|
601
602
|
same-process run lifecycle as an external app repository. Verification remains
|
|
602
603
|
an explicit operation for a selected release output.
|
|
603
604
|
|
|
@@ -617,8 +618,8 @@ arcane package [--app <id>] [--dry-run]
|
|
|
617
618
|
arcane verify [--app <id>]
|
|
618
619
|
arcane bundle [--app <id>] [--artifact <file>.arcane-app.tar.gz] [--overwrite]
|
|
619
620
|
arcane verify-bundle <file.arcane-app.tar.gz>
|
|
620
|
-
arcane native-doctor --target <native-target> --arcane-root <directory>
|
|
621
|
-
arcane native-prepare --target <native-target> --arcane-root <directory>
|
|
621
|
+
arcane native-doctor --target <native-target> [--arcane-root <directory>]
|
|
622
|
+
arcane native-prepare --target <native-target> [--arcane-root <directory>]
|
|
622
623
|
arcane build --target <target> [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
|
|
623
624
|
arcane run [--target <target>] [--app <id>] [--arcane-root <directory>] [--output-root <directory>] [--format <format>] [--signing <mode>]
|
|
624
625
|
arcane targets
|
|
@@ -689,24 +690,25 @@ package installation, or assertions.
|
|
|
689
690
|
|
|
690
691
|
## Current target support
|
|
691
692
|
|
|
692
|
-
The SDK exposes one browser target and
|
|
693
|
-
|
|
694
|
-
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
693
|
+
The SDK exposes one browser target and an SDK-owned, non-runnable portable
|
|
694
|
+
payload directory. Four executable target entries require an explicit Arcane OS
|
|
695
|
+
provider: a Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
695
696
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
696
697
|
`android-arm64` APK is architecture-neutral because it contains no native ABI;
|
|
697
698
|
the target name identifies its supported physical/native ARM64 run profile.
|
|
698
699
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
700
|
+
Portable uses the installed SDK provider without an Arcane OS checkout.
|
|
701
|
+
An explicit `--arcane-root` retains the checkout provider selection; executable
|
|
702
|
+
targets still require that root and their platform toolchains. Every native
|
|
703
|
+
build needs a canonical app descriptor declaring the exact selected target.
|
|
704
|
+
The SDK never searches for a mutable toolchain root. Scaffold a separate
|
|
703
705
|
repository with the matching `--target`, or add and validate that target in the
|
|
704
706
|
canonical descriptor before invoking its native command. From an external
|
|
705
707
|
application repository, run one selected target:
|
|
706
708
|
|
|
707
709
|
```bash
|
|
708
|
-
npm exec -- arcane native-doctor --target portable
|
|
709
|
-
npm exec -- arcane build --target portable
|
|
710
|
+
npm exec -- arcane native-doctor --target portable
|
|
711
|
+
npm exec -- arcane build --target portable
|
|
710
712
|
|
|
711
713
|
# Windows x64: build or build, verify, and launch in one process
|
|
712
714
|
npm exec -- arcane build --target windows-x64 --arcane-root "../Arcane OS"
|
|
@@ -737,15 +739,18 @@ browser package.
|
|
|
737
739
|
`native-prepare` is available as a standalone toolchain diagnostic. Do not run
|
|
738
740
|
it automatically: the build command prepares the selected toolchain itself.
|
|
739
741
|
|
|
740
|
-
The build
|
|
741
|
-
Core to
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
742
|
+
The build passes the complete selected app release, descriptor, dependencies and
|
|
743
|
+
minimum Core requirement to the provider. The SDK portable provider copies those
|
|
744
|
+
payloads and generates a Core entry and synchronous classic client. Explicit
|
|
745
|
+
application services use `createPortableNativeProvider({services:[{module,options}]})`
|
|
746
|
+
from `arcane-os/native/portable-provider`; the default imports no OS product or
|
|
747
|
+
application services. `arcane-os/core/host` owns reusable native service
|
|
748
|
+
composition and shutdown. See [Core native packaging](docs/reference/core-native-packaging.md).
|
|
749
|
+
Payload assembly is separate from composed-host compatibility and execution.
|
|
750
|
+
The portable payload under `build/portable/` is not executable and
|
|
751
|
+
`arcane run --target portable` is intentionally unavailable. No executable host,
|
|
752
|
+
installer, model assets or platform build toolchain is added by this SDK
|
|
753
|
+
provider. Through explicit Arcane OS providers, Windows emits
|
|
749
754
|
an app-scoped executable bundle under `build/windows-x64/`. Linux emits an
|
|
750
755
|
app-scoped amd64 DEB under `build/linux-x64/` or ARM64 DEB under
|
|
751
756
|
`build/linux-arm64/`. Android emits one development-signed, architecture-neutral
|
|
@@ -778,7 +783,7 @@ signing, store-publishing, release, or promotion evidence.
|
|
|
778
783
|
| Integrated Arcane checkout workflow | Yes |
|
|
779
784
|
| ArcaneOllama managed-service check | Yes on supported Windows hosts |
|
|
780
785
|
| SDK native plan/provider boundary | Yes |
|
|
781
|
-
| Portable app-scoped Core
|
|
786
|
+
| Portable app-scoped payload and reusable Core entry | SDK provider by default; explicit checkout override retained; not directly runnable |
|
|
782
787
|
| Windows x64 EXE bundle | Yes, unsigned local development with explicit `--arcane-root` |
|
|
783
788
|
| Linux x64 DEB | Yes, unsigned local development with explicit `--arcane-root` |
|
|
784
789
|
| Linux ARM64 DEB | Yes, unsigned local development on a compatible native ARM64 toolchain with explicit `--arcane-root`; native build, WebKit readiness, and cancellation evidence is recorded |
|
|
@@ -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
|
+
}
|