arcane-os 0.1.2 → 0.2.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/NOTICE +5 -3
  3. package/README.md +73 -24
  4. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
  5. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
  6. package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
  7. package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
  8. package/browser-runtime/ai/browser-speech-providers.mjs +475 -0
  9. package/browser-runtime/ai/browser-speech.mjs +9 -0
  10. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
  11. package/browser-runtime/ai/browser-wasm.mjs +46 -1
  12. package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
  13. package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
  14. package/browser-runtime/ai/model-controller.mjs +138 -12
  15. package/browser-runtime/ai/speech-worker-client.mjs +207 -0
  16. package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
  17. package/browser-runtime/ai/wllama/index.mjs +389 -0
  18. package/docs/architecture.md +132 -22
  19. package/docs/reference/README.md +1 -1
  20. package/docs/reference/ai/browser-wasm.md +101 -42
  21. package/docs/reference/availability-and-normalization.md +19 -5
  22. package/docs/reference/behavioral-testing.md +18 -5
  23. package/docs/reference/cli.md +2 -2
  24. package/docs/reference/inventory/package-api.json +14 -14
  25. package/docs/reference/protocols.md +4 -4
  26. package/docs/reference/sdk-api.md +68 -38
  27. package/docs/work-amplification.md +8 -4
  28. package/package.json +7 -3
  29. package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
  30. package/runtime/arcane/components/chat.html +280 -62
  31. package/runtime/arcane/components/speech.html +1113 -265
  32. package/runtime/arcane/entities/Chat.js +246 -43
  33. package/runtime/arcane/modules/AI.js +713 -162
  34. package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
  35. package/runtime/arcane/modules/AIRuntimeState.js +872 -0
  36. package/runtime/arcane/modules/ConfiguredAIChatSession.js +293 -27
  37. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +682 -0
  38. package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
  39. package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
  40. package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
  41. package/schemas/arcane-lock.schema.json +6 -4
  42. package/src/cli/main.mjs +14 -2
  43. package/src/constants.mjs +1 -1
  44. package/src/dev-server.mjs +244 -13
  45. package/src/import-map.mjs +59 -1
  46. package/src/packager/core.mjs +2 -2
  47. package/src/runtime.mjs +14 -4
  48. package/src/sdk-browser-runtime.mjs +28 -75
  49. package/src/templates/workspace-template.mjs +4 -4
  50. package/src/toolchain.mjs +3 -0
  51. package/src/workspace-runtime.mjs +1 -1
  52. package/src/workspace.mjs +1 -1
@@ -17,6 +17,7 @@ import {verifyRuntime} from './runtime.mjs';
17
17
  import {verifySdkBrowserRuntime} from './sdk-browser-runtime.mjs';
18
18
  import {resolveWorkspace} from './workspace.mjs';
19
19
  import {createEventQueue} from './event-queue.mjs';
20
+ import {SDK_NAME,SDK_VERSION} from './constants.mjs';
20
21
 
21
22
  const MIME_TYPES=new Map([
22
23
  ['.css','text/css; charset=utf-8'],
@@ -44,6 +45,17 @@ const SESSION_COOKIE='Arcane-Dev-Session';
44
45
  const PRIVATE_SOURCE_SEGMENTS=new Set([
45
46
  'arcane-app.json','arcane-package.json','test','tests','scripts','node_modules','dist','local'
46
47
  ]);
48
+ const SDK_RUNTIME_SOURCE_PROTOCOL='arcane-sdk-runtime-source/1';
49
+ const SDK_RUNTIME_SOURCE_ARCANE_ROOTS=new Set([
50
+ 'components','css','entities','img','modules','security'
51
+ ]);
52
+ const SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS=new Set([
53
+ 'node_modules','.git','.hg','.svn','cvs'
54
+ ]);
55
+ const SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS=new Set([
56
+ 'arcane-app.json','arcane-package.json','arcane.lock.json',
57
+ 'arcane_app_release.json','arcane_runtime_release.json','arcane_sdk_browser_release.json'
58
+ ]);
47
59
 
48
60
  // This server is a loopback-only development host, not a production policy
49
61
  // boundary. The bundled runtime currently needs inline component execution,
@@ -139,6 +151,173 @@ function inventoryKey(value){
139
151
  return process.platform==='win32'?value.toLowerCase():value;
140
152
  }
141
153
 
154
+ function canonicalLocationKey(value){
155
+ return inventoryKey(path.resolve(value));
156
+ }
157
+
158
+ function pathIsWithin(root,candidate){
159
+ const relative=path.relative(root,candidate);
160
+ return relative===''||(!path.isAbsolute(relative)&&relative!=='..'
161
+ &&!relative.startsWith(`..${path.sep}`));
162
+ }
163
+
164
+ function pathsOverlap(left,right){
165
+ return pathIsWithin(left,right)||pathIsWithin(right,left);
166
+ }
167
+
168
+ async function canonicalRealDirectory(requested,label){
169
+ let info;
170
+ try{
171
+ info=await lstat(requested);
172
+ }catch{
173
+ fail(`${label} must be an existing real directory.`,'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
174
+ }
175
+ if(info.isSymbolicLink()||!info.isDirectory()){
176
+ fail(`${label} must be an existing real directory.`,'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
177
+ }
178
+ let canonical;
179
+ try{
180
+ canonical=await realpath(requested);
181
+ }catch{
182
+ fail(`${label} could not be resolved as a real directory.`,'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
183
+ }
184
+ if(canonicalLocationKey(canonical)!==canonicalLocationKey(requested)){
185
+ fail(`${label} must not contain a symlink or reparse-point escape.`,'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
186
+ }
187
+ return canonical;
188
+ }
189
+
190
+ function sdkRuntimeSourcePathAllowed(relative,{arcaneRoot=false}={}){
191
+ if(relative.length===0)return false;
192
+ const normalized=relative.map(function normalizeRuntimeSourceSegment(segment){
193
+ return segment.normalize('NFC').toLowerCase();
194
+ });
195
+ if(normalized.some(function runtimeSourceSegmentIsPrivate(segment){
196
+ return segment.startsWith('.')||SDK_RUNTIME_SOURCE_PRIVATE_SEGMENTS.has(segment)
197
+ ||SDK_RUNTIME_SOURCE_PRIVATE_MANIFESTS.has(segment);
198
+ }))return false;
199
+ return !arcaneRoot||SDK_RUNTIME_SOURCE_ARCANE_ROOTS.has(normalized[0]);
200
+ }
201
+
202
+ function sdkArcaneSourcePathAllowed(relative){
203
+ return sdkRuntimeSourcePathAllowed(relative,{arcaneRoot:true});
204
+ }
205
+
206
+ function sdkDependencySourcePathAllowed(relative){
207
+ return sdkRuntimeSourcePathAllowed(relative);
208
+ }
209
+
210
+ function sdkBrowserSourcePathAllowed(relative){
211
+ return sdkRuntimeSourcePathAllowed(relative);
212
+ }
213
+
214
+ async function emitRuntimeSourceEvent(onEvent,event){
215
+ if(typeof onEvent==='function')await onEvent(event);
216
+ }
217
+
218
+ async function verifySdkRuntimeSourceRoot(sourceRoot,workspaceRoot,appId,{signal,onEvent}={}){
219
+ throwIfAborted(signal);
220
+ if(typeof sourceRoot!=='string'||!sourceRoot.trim()){
221
+ fail('sdkRuntimeSourceRoot must name an Arcane SDK directory.',
222
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
223
+ }
224
+ const requestedRoot=path.resolve(sourceRoot);
225
+ await emitRuntimeSourceEvent(onEvent,{
226
+ type:'runtime.source.mount.started',
227
+ appId,
228
+ requestedRoot,
229
+ target:'browser'
230
+ });
231
+ const canonicalRoot=await canonicalRealDirectory(requestedRoot,'SDK runtime source root');
232
+ const canonicalWorkspaceRoot=await realpath(workspaceRoot);
233
+ if(pathsOverlap(canonicalRoot,canonicalWorkspaceRoot)){
234
+ fail('SDK runtime source root must not overlap the application workspace.',
235
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
236
+ }
237
+
238
+ let packageFile;
239
+ try{
240
+ packageFile=await openSafeFile(canonicalRoot,['package.json']);
241
+ }catch{
242
+ fail('SDK runtime source package.json must be a readable bounded real file.',
243
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
244
+ }
245
+ if(!packageFile){
246
+ fail('SDK runtime source root must contain a real package.json.',
247
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
248
+ }
249
+ let packageDocument;
250
+ try{
251
+ packageDocument=JSON.parse(packageFile.bytes.toString('utf8'));
252
+ }catch{
253
+ fail('SDK runtime source package.json must be valid JSON.',
254
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
255
+ }
256
+ if(!packageDocument||Array.isArray(packageDocument)||packageDocument.name!==SDK_NAME){
257
+ fail(`SDK runtime source package name must be exactly ${SDK_NAME}.`,
258
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
259
+ }
260
+ if(packageDocument.version!==SDK_VERSION){
261
+ fail(
262
+ `SDK runtime source version must exactly match the executing SDK (${SDK_VERSION}).`,
263
+ 'ARCANE_DEV_RUNTIME_VERSION_MISMATCH'
264
+ );
265
+ }
266
+
267
+ const roots=[
268
+ {
269
+ path:'runtime/arcane',
270
+ prefix:['arcane'],
271
+ allow:sdkArcaneSourcePathAllowed
272
+ },
273
+ {
274
+ path:'runtime/strong-type',
275
+ prefix:['arcane','dependencies','strong-type'],
276
+ allow:sdkDependencySourcePathAllowed
277
+ },
278
+ {
279
+ path:'browser-runtime',
280
+ prefix:['arcane','sdk'],
281
+ allow:sdkBrowserSourcePathAllowed
282
+ }
283
+ ];
284
+ const mappings=[];
285
+ for(let index=0;index<roots.length;index+=1){
286
+ throwIfAborted(signal);
287
+ const root=roots[index];
288
+ const requested=path.join(canonicalRoot,...root.path.split('/'));
289
+ const canonical=await canonicalRealDirectory(requested,`SDK runtime source ${root.path}`);
290
+ if(!pathIsWithin(canonicalRoot,canonical)){
291
+ fail(`SDK runtime source ${root.path} must remain inside the SDK root.`,
292
+ 'ARCANE_DEV_RUNTIME_SOURCE_INVALID');
293
+ }
294
+ mappings.push({prefix:root.prefix,root:canonical,allow:root.allow});
295
+ await emitRuntimeSourceEvent(onEvent,{
296
+ type:'runtime.source.mount.progress',
297
+ current:index+1,
298
+ total:roots.length,
299
+ path:root.path
300
+ });
301
+ }
302
+ const runtime=Object.freeze({
303
+ mode:'sdk-source',
304
+ protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
305
+ sdkVersion:SDK_VERSION,
306
+ mutable:true,
307
+ distributionAuthority:false,
308
+ sourceRoot:canonicalRoot
309
+ });
310
+ await emitRuntimeSourceEvent(onEvent,{
311
+ type:'runtime.source.mount.ready',
312
+ appId,
313
+ canonicalRoot,
314
+ sdkVersion:SDK_VERSION,
315
+ protocol:SDK_RUNTIME_SOURCE_PROTOCOL,
316
+ routeCount:mappings.length
317
+ });
318
+ return {mappings,runtime};
319
+ }
320
+
142
321
  function identityMap(identities,prefix=''){
143
322
  const normalizedPrefix=prefix?`${prefix}/`:'';
144
323
  const result=new Map();
@@ -346,8 +525,34 @@ function sharedPathAllowed(relative,route){
346
525
  });
347
526
  }
348
527
 
349
- async function sourceRoutes(workspaceRoot,appId,{workspaceRuntimeReceipt,signal}){
528
+ async function sourceRoutes(workspaceRoot,appId,{
529
+ workspaceRuntimeReceipt,
530
+ sdkRuntimeSourceRoot,
531
+ signal,
532
+ onEvent
533
+ }){
350
534
  const resolved=await resolveWorkspace({workspaceRoot,appId});
535
+ const appMapping={
536
+ prefix:['apps',resolved.appId],
537
+ root:resolved.appRoot,
538
+ allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
539
+ };
540
+ if(sdkRuntimeSourceRoot!==undefined){
541
+ const sdkSource=await verifySdkRuntimeSourceRoot(
542
+ sdkRuntimeSourceRoot,
543
+ resolved.workspaceRoot,
544
+ resolved.appId,
545
+ {signal,onEvent}
546
+ );
547
+ return {
548
+ workspaceRoot:resolved.workspaceRoot,
549
+ workspaceMode:resolved.config.workspaceMode,
550
+ appId:resolved.appId,
551
+ startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
552
+ runtime:sdkSource.runtime,
553
+ mappings:[appMapping,...sdkSource.mappings]
554
+ };
555
+ }
351
556
  if(resolved.config.workspaceMode==='integrated'){
352
557
  return {
353
558
  workspaceRoot:resolved.workspaceRoot,
@@ -355,11 +560,7 @@ async function sourceRoutes(workspaceRoot,appId,{workspaceRuntimeReceipt,signal}
355
560
  appId:resolved.appId,
356
561
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
357
562
  mappings:[
358
- {
359
- prefix:['apps',resolved.appId],
360
- root:resolved.appRoot,
361
- allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
362
- },
563
+ appMapping,
363
564
  ...resolved.config.sharedPayloads['browser-runtime'].map(route=>({
364
565
  prefix:route.destination.split('/'),
365
566
  root:path.join(resolved.workspaceRoot,...route.source.split('/')),
@@ -411,11 +612,7 @@ async function sourceRoutes(workspaceRoot,appId,{workspaceRuntimeReceipt,signal}
411
612
  appId:resolved.appId,
412
613
  startPath:`/apps/${resolved.appId}/${resolved.app.manifest.entry}`,
413
614
  mappings:[
414
- {
415
- prefix:['apps',resolved.appId],
416
- root:resolved.appRoot,
417
- allow:relative=>sourcePathAllowed(relative,resolved.app.manifest)
418
- },
615
+ appMapping,
419
616
  {
420
617
  prefix:['arcane'],
421
618
  root:runtimeRoot,
@@ -506,18 +703,33 @@ async function startOwnedDevServer({
506
703
  port=0,
507
704
  signal,
508
705
  workspaceRuntimeReceipt,
706
+ sdkRuntimeSourceRoot,
509
707
  releaseReceipt
510
708
  }={},events,releaseSignal){
511
709
  throwIfAborted(signal);
512
710
  if(mode!=='source'&&mode!=='packaged')fail(`Unsupported server mode: ${String(mode)}.`,'ARCANE_USAGE');
711
+ if(sdkRuntimeSourceRoot!==undefined&&mode!=='source'){
712
+ fail('sdkRuntimeSourceRoot is supported only in source development mode.','ARCANE_USAGE');
713
+ }
513
714
  if(host!=='127.0.0.1'&&host!=='::1'){
514
715
  fail('Development server host must be a numeric loopback address (127.0.0.1 or ::1).','ARCANE_POLICY_DENIED');
515
716
  }
516
717
  if(!Number.isInteger(port)||port<0||port>65535)fail('port must be an integer from 0 through 65535.','ARCANE_USAGE');
517
- await events.send({type:'server.starting',mode,host,port,appId});
718
+ const requestedRuntimeMode=mode==='source'&&sdkRuntimeSourceRoot!==undefined
719
+ ?'sdk-source'
720
+ :null;
721
+ await events.send({
722
+ type:'server.starting',
723
+ mode,
724
+ host,
725
+ port,
726
+ appId,
727
+ ...(requestedRuntimeMode?{runtimeMode:requestedRuntimeMode}:{})
728
+ });
518
729
  const routeSet=mode==='source'
519
730
  ?await sourceRoutes(workspaceRoot,appId,{
520
731
  workspaceRuntimeReceipt,
732
+ sdkRuntimeSourceRoot,
521
733
  signal,
522
734
  onEvent:event=>events.send(event)
523
735
  })
@@ -647,6 +859,17 @@ async function startOwnedDevServer({
647
859
  while(requestTasks.size>0){
648
860
  await Promise.allSettled([...requestTasks]);
649
861
  }
862
+ if(routeSet.runtime?.mode==='sdk-source'){
863
+ await events.send({
864
+ type:'runtime.source.mount.stopped',
865
+ appId:routeSet.appId,
866
+ reason:events.error||operationalError
867
+ ?'failed'
868
+ :signal?.aborted
869
+ ?'cancelled'
870
+ :'closed'
871
+ });
872
+ }
650
873
  await events.send({
651
874
  type:'server.stopped',
652
875
  host:address.address,
@@ -695,6 +918,10 @@ async function startOwnedDevServer({
695
918
  mode,
696
919
  workspaceRoot:routeSet.workspaceRoot,
697
920
  appId:routeSet.appId,
921
+ ...(routeSet.runtime?{
922
+ runtimeMode:routeSet.runtime.mode,
923
+ runtime:routeSet.runtime
924
+ }:{}),
698
925
  host:address.address,
699
926
  port:address.port,
700
927
  origin,
@@ -711,7 +938,11 @@ async function startOwnedDevServer({
711
938
  host:result.host,
712
939
  port:result.port,
713
940
  url,
714
- appId:result.appId
941
+ appId:result.appId,
942
+ ...(routeSet.runtime?{
943
+ runtimeMode:routeSet.runtime.mode,
944
+ runtime:routeSet.runtime
945
+ }:{})
715
946
  });
716
947
  throwIfAborted(signal);
717
948
  }catch(error){
@@ -14,15 +14,26 @@ export const MANAGED_IMPORT_MAP_ATTRIBUTE='data-arcane-import-map';
14
14
  const JAVASCRIPT_EXTENSION=/\.(?:js|mjs)$/u;
15
15
  const NODE_ONLY_MODULE='modules/CaseEvidenceIndexer.js';
16
16
  const RUNTIME_STRONG_TYPE_IMPORT='../../node_modules/strong-type/index.js';
17
+ const PERSISTENT_CHAT_IMPORT='#arcane/persistent-ai-chat-session';
18
+ const PERSISTENT_CHAT_MODULE='modules/PersistentAIChatSession.js';
17
19
  const SDK_BROWSER_ENTRY='sdk/event-manager.mjs';
18
20
  const SDK_BROWSER_AI_ENTRY='sdk/ai/browser-wasm.mjs';
21
+ const SDK_BROWSER_SPEECH_ENTRY='sdk/ai/browser-speech.mjs';
22
+ const SDK_BROWSER_SPEECH_WORKER_RUNTIME='sdk/ai/speech-worker-runtime.mjs';
19
23
  const SDK_BROWSER_FILES=Object.freeze([
20
24
  'sdk/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json',
25
+ 'sdk/ai/browser-kokoro-worker.mjs',
26
+ 'sdk/ai/browser-speech-artifacts.mjs',
27
+ 'sdk/ai/browser-speech-providers.mjs',
28
+ SDK_BROWSER_SPEECH_ENTRY,
21
29
  'sdk/ai/browser-wasm-llm-provider.mjs',
22
30
  SDK_BROWSER_AI_ENTRY,
31
+ 'sdk/ai/browser-whisper-worker.mjs',
23
32
  'sdk/ai/browser-wllama-runtime.mjs',
24
33
  'sdk/ai/internal/sha256.mjs',
25
34
  'sdk/ai/model-controller.mjs',
35
+ 'sdk/ai/speech-worker-client.mjs',
36
+ SDK_BROWSER_SPEECH_WORKER_RUNTIME,
26
37
  'sdk/ai/wllama/LICENCE',
27
38
  'sdk/ai/wllama/index.mjs',
28
39
  'sdk/ai/wllama/llama.cpp-LICENSE',
@@ -719,6 +730,14 @@ function nonliteralDynamic(importer,offset){
719
730
  );
720
731
  }
721
732
 
733
+ function isAuthorizedSpeechRuntimeImport(tokens,index,close,importer){
734
+ if(importer!==SDK_BROWSER_SPEECH_WORKER_RUNTIME||close!==index+5)return false;
735
+ const [entry,dot,moduleUrl]=tokens.slice(index+2,close);
736
+ return entry?.type==='identifier'&&entry.value==='entry'
737
+ &&dot?.value==='.'
738
+ &&moduleUrl?.type==='identifier'&&moduleUrl.value==='moduleUrl';
739
+ }
740
+
722
741
  export function scanModuleImports(source,{importer='<module>'}={}){
723
742
  if(typeof source!=='string')throw new TypeError('scanModuleImports source must be a string.');
724
743
  const tokens=tokenize(source);
@@ -745,6 +764,11 @@ export function scanModuleImports(source,{importer='<module>'}={}){
745
764
  ||commas.length>2
746
765
  ||(commas.length===2
747
766
  &&(commas[1]!==close-1||commas[1]===commas[0]+1))){
767
+ if(isAuthorizedSpeechRuntimeImport(tokens,index,close,importer)){
768
+ hasModuleSyntax=true;
769
+ index=close;
770
+ continue;
771
+ }
748
772
  nonliteralDynamic(importer,current.start);
749
773
  }
750
774
  hasModuleSyntax=true;
@@ -811,6 +835,20 @@ function unresolved(importer,specifier,normalizedTarget,reason='is not in the sh
811
835
  }
812
836
 
813
837
  function resolveImport(importer,specifier,files){
838
+ if(specifier===PERSISTENT_CHAT_IMPORT){
839
+ if(!files.has(PERSISTENT_CHAT_MODULE)){
840
+ unresolved(importer,specifier,PERSISTENT_CHAT_MODULE);
841
+ }
842
+ return {target:PERSISTENT_CHAT_MODULE,persistentChat:true};
843
+ }
844
+ if(specifier.startsWith(PERSISTENT_CHAT_IMPORT)){
845
+ unresolved(
846
+ importer,
847
+ specifier,
848
+ PERSISTENT_CHAT_MODULE,
849
+ 'does not match the exact browser import-map key'
850
+ );
851
+ }
814
852
  const reachableSpecifier=stripQueryAndHash(specifier);
815
853
  if(!reachableSpecifier)unresolved(importer,specifier,'<empty>');
816
854
  if(reachableSpecifier.includes('%')){
@@ -966,9 +1004,13 @@ export async function buildImportMap({files,readFile,signal}={}){
966
1004
  }
967
1005
  const hasSdkBrowserGraph=inventory.has(SDK_BROWSER_ENTRY);
968
1006
  const hasSdkAiGraph=inventory.has(SDK_BROWSER_AI_ENTRY);
1007
+ const hasSdkSpeechGraph=inventory.has(SDK_BROWSER_SPEECH_ENTRY);
969
1008
  if(hasSdkAiGraph&&!hasSdkBrowserGraph){
970
1009
  unresolved(SDK_BROWSER_AI_ENTRY,'<authenticated SDK browser closure>',SDK_BROWSER_ENTRY);
971
1010
  }
1011
+ if(hasSdkSpeechGraph&&!hasSdkAiGraph){
1012
+ unresolved(SDK_BROWSER_SPEECH_ENTRY,'<authenticated SDK browser closure>',SDK_BROWSER_AI_ENTRY);
1013
+ }
972
1014
  if(hasSdkBrowserGraph){
973
1015
  for(const required of SDK_BROWSER_FILES){
974
1016
  if(!inventory.has(required)){
@@ -1016,11 +1058,20 @@ export async function buildImportMap({files,readFile,signal}={}){
1016
1058
  'arcane-os/ai/browser-wasm',
1017
1059
  './arcane/sdk/ai/browser-wasm.mjs'
1018
1060
  );
1061
+ registerSpecifier(
1062
+ namedRegistry,
1063
+ 'arcane-os/ai/browser-speech',
1064
+ './arcane/sdk/ai/browser-speech.mjs'
1065
+ );
1019
1066
  }
1020
1067
  }
1021
1068
 
1022
1069
  const sdkRoots=hasSdkBrowserGraph
1023
- ?[SDK_BROWSER_ENTRY,...(hasSdkAiGraph?[SDK_BROWSER_AI_ENTRY]:[])]:[];
1070
+ ?[
1071
+ SDK_BROWSER_ENTRY,
1072
+ ...(hasSdkAiGraph?[SDK_BROWSER_AI_ENTRY]:[]),
1073
+ ...(hasSdkSpeechGraph?[SDK_BROWSER_SPEECH_ENTRY]:[]),
1074
+ ]:[];
1024
1075
  const queue=[...roots,...sdkRoots];
1025
1076
  const reached=new Set();
1026
1077
  const entities=new Set();
@@ -1036,6 +1087,13 @@ export async function buildImportMap({files,readFile,signal}={}){
1036
1087
  const resolution=resolveImport(importer,imported.specifier,inventory);
1037
1088
  if(resolution.runtimeStrongType)usesRuntimeStrongType=true;
1038
1089
  if(resolution.eventPubSub)usesEventPubSub=true;
1090
+ if(resolution.persistentChat){
1091
+ registerSpecifier(
1092
+ namedRegistry,
1093
+ PERSISTENT_CHAT_IMPORT,
1094
+ './arcane/modules/PersistentAIChatSession.js'
1095
+ );
1096
+ }
1039
1097
  if(resolution.target.startsWith('entities/'))entities.add(resolution.target);
1040
1098
  if(!reached.has(resolution.target))queue.push(resolution.target);
1041
1099
  }
@@ -1036,7 +1036,7 @@ function packagedRuntimeAuthorities(receipt){
1036
1036
  ||!Number.isSafeInteger(receipt.fileCount)||receipt.fileCount<1
1037
1037
  ||!Number.isSafeInteger(receipt.totalBytes)||receipt.totalBytes<1
1038
1038
  ||!SHA256_PATTERN.test(receipt.contentSha256??'')
1039
- ||!isPlainObject(arcane)||arcane.authority!=='arcane-os-upstream'
1039
+ ||!isPlainObject(arcane)||arcane.authority!=='arcane-os-sdk'
1040
1040
  ||!SHA256_PATTERN.test(arcane.manifestSha256??'')
1041
1041
  ||!SHA256_PATTERN.test(arcane.contentSha256??'')
1042
1042
  ||!isPlainObject(arcane.source)
@@ -1158,7 +1158,7 @@ async function authenticatePackageRuntimeVerificationState(context,state,{signal
1158
1158
  });
1159
1159
  const workspaceReceipt=snapshot.workspaceRuntimeReceipt;
1160
1160
  const expectedArcaneSource={
1161
- authority:'arcane-os-upstream',
1161
+ authority:'arcane-os-sdk',
1162
1162
  location:snapshot.runtimeReceipt.canonicalLocation,
1163
1163
  manifestSha256:snapshot.runtimeReceipt.manifestSha256,
1164
1164
  contentSha256:snapshot.runtimeReceipt.contentSha256,
package/src/runtime.mjs CHANGED
@@ -7,6 +7,8 @@ import {SDK_VERSION} from './constants.mjs';
7
7
 
8
8
  export const RUNTIME_MANIFEST_NAME='ARCANE_RUNTIME_RELEASE.json';
9
9
  const SHA256_PATTERN=/^[a-f0-9]{64}$/;
10
+ const SDK_RUNTIME_REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
11
+ const LEGACY_ARCANE_REPOSITORY='https://github.com/TheWizardNexus/ARCANE-OS.git';
10
12
  const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
11
13
  const MAX_VERIFIED_RUNTIME_FILE_BYTES=64*1024*1024;
12
14
  const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
@@ -59,10 +61,18 @@ function validateRelease(value){
59
61
  }
60
62
  if(value.sdkVersion!==SDK_VERSION)fail('Runtime manifest sdkVersion is incompatible with this SDK.');
61
63
  if(!value.source||typeof value.source!=='object'
62
- ||Object.keys(value.source).sort(compareText).join('\0')!=='bundleVersion\0commit\0protocol\0repository'
63
- ||value.source.repository!=='https://github.com/TheWizardNexus/ARCANE-OS.git'
64
- ||typeof value.source.commit!=='string'||!/^[a-f0-9]{40}$/.test(value.source.commit)
65
- ||value.source.bundleVersion!=='0.8.12'||value.source.protocol!=='arcane/1'){
64
+ ||Object.keys(value.source).sort(compareText).join('\0')!=='authority\0legacyProjection\0path\0protocol\0repository'
65
+ ||value.source.authority!=='sdk-canonical'
66
+ ||value.source.repository!==SDK_RUNTIME_REPOSITORY
67
+ ||value.source.path!=='runtime/arcane'
68
+ ||value.source.protocol!=='arcane/1'
69
+ ||!value.source.legacyProjection
70
+ ||typeof value.source.legacyProjection!=='object'
71
+ ||Object.keys(value.source.legacyProjection).sort(compareText).join('\0')!=='bundleVersion\0commit\0repository'
72
+ ||value.source.legacyProjection.repository!==LEGACY_ARCANE_REPOSITORY
73
+ ||typeof value.source.legacyProjection.commit!=='string'
74
+ ||!/^[a-f0-9]{40}$/.test(value.source.legacyProjection.commit)
75
+ ||value.source.legacyProjection.bundleVersion!=='0.8.12'){
66
76
  fail('Runtime manifest source identity is invalid.');
67
77
  }
68
78
  if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)
@@ -1,6 +1,6 @@
1
1
  import {createHash} from 'node:crypto';
2
2
  import {constants as FS_CONSTANTS} from 'node:fs';
3
- import {lstat,open,readdir,realpath} from 'node:fs/promises';
3
+ import {lstat,open,readFile,readdir,realpath} from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import {fileURLToPath} from 'node:url';
6
6
  import {SDK_VERSION} from './constants.mjs';
@@ -8,96 +8,49 @@ import {SDK_VERSION} from './constants.mjs';
8
8
  const MANIFEST_NAME='ARCANE_SDK_BROWSER_RELEASE.json';
9
9
  const BUILDER='arcane-sdk-browser-runtime-v1';
10
10
  const PROTOCOL='arcane-sdk-browser-runtime/1';
11
- export const SDK_BROWSER_RUNTIME_MANIFEST_SHA256=
12
- '88395493b411fd5461fbb2bb065ae2b745f6d1672f796583fd248ec97f71f4f7';
13
- export const SDK_BROWSER_RUNTIME_CONTENT_SHA256=
14
- '5e03f45a732db51cb5a2b2193cc79ecda34501d07a9b2e82e794e5fa37d55d00';
15
11
  const REPOSITORY='https://github.com/TheWizardNexus/arcane-os-sdk.git';
16
12
  const SHA256_PATTERN=/^[a-f0-9]{64}$/u;
17
13
  const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
18
14
  const MAX_VERIFIED_FILE_BYTES=64*1024*1024;
19
15
  const packageRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
20
16
  const defaultRoot=path.join(packageRoot,'browser-runtime');
17
+ const canonicalManifestBytes=await readFile(path.join(defaultRoot,MANIFEST_NAME));
18
+ const canonicalRelease=JSON.parse(canonicalManifestBytes.toString('utf8'));
19
+ if(
20
+ canonicalRelease?.schemaVersion!==1
21
+ ||canonicalRelease.builder!==BUILDER
22
+ ||canonicalRelease.sdkVersion!==SDK_VERSION
23
+ ||!SHA256_PATTERN.test(canonicalRelease.contentSha256)
24
+ ){
25
+ throw new Error('The canonical SDK browser runtime receipt is invalid.');
26
+ }
27
+ export const SDK_BROWSER_RUNTIME_MANIFEST_SHA256=
28
+ createHash('sha256').update(canonicalManifestBytes).digest('hex');
29
+ export const SDK_BROWSER_RUNTIME_CONTENT_SHA256=canonicalRelease.contentSha256;
21
30
  const issuedReceipts=new WeakSet();
22
- const expectedAiComponents=Object.freeze({
23
- schemaVersion:1,
24
- kind:'arcane-ai-browser-wasm-components',
25
- protocol:'arcane-ai-browser-wasm/1',
26
- packageExport:'arcane-os/ai/browser-wasm',
27
- browserEntry:'ai/browser-wasm.mjs',
28
- runtimePolicy:{
29
- modelAuthorities:'caller-supplied-exact-bytes-and-sha256',
30
- modelWeightsPacked:false,
31
- remoteModelHelpers:false,
32
- compatibilityRuntime:false,
33
- toolCalls:'structural-only-never-executed'
34
- },
35
- components:[
36
- {
37
- name:'@wllama/wllama',
38
- version:'3.6.0',
39
- licenseSpdx:'MIT',
40
- sourceRepository:'https://github.com/ngxson/wllama.git',
41
- sourceRevision:'f16050d8d51a00602c6a2a6b8ac9c09f490eea7f',
42
- npm:{
43
- resolved:'https://registry.npmjs.org/@wllama/wllama/-/wllama-3.6.0.tgz',
44
- integrity:'sha512-NN3ZBXqaaUwGXTQubkNvsCaLPjN2XVa0bVS40OYCE8zquYmRc2W3oHYEgwvuSWWDB8aUqTLyMioySCXNkcnD1w==',
45
- tarballBytes:5671369,
46
- tarballSha256:'137c35ceccb4911a9b0ce9b427889f75991654ec6a6d1dd8fabd879b14b07a1b'
47
- },
48
- files:[
49
- {
50
- role:'runtime-module',
51
- path:'ai/wllama/index.mjs',
52
- sourcePath:'node_modules/@wllama/wllama/esm/index.js',
53
- bytes:373519,
54
- sha256:'4637e42d636010493a9b274fbbe70bfd8120365da726b1d9e589d85ca84a00d6'
55
- },
56
- {
57
- role:'runtime-wasm',
58
- path:'ai/wllama/wllama.wasm',
59
- sourcePath:'node_modules/@wllama/wllama/esm/wasm/wllama.wasm',
60
- bytes:8524865,
61
- sha256:'95c6ff9ef2a03ff2c63bc91db132f0126a0bd0456b272cd8ae2e0f592fb059f6'
62
- },
63
- {
64
- role:'license',
65
- path:'ai/wllama/LICENCE',
66
- sourcePath:'node_modules/@wllama/wllama/LICENCE',
67
- bytes:1071,
68
- sha256:'5866e3bd7e3cbd3f7c8bea6efd8a1e7fa7cc8de68c30f428aff7c6584a0fb720'
69
- }
70
- ]
71
- },
72
- {
73
- name:'llama.cpp',
74
- version:'b10454',
75
- licenseSpdx:'MIT',
76
- sourceRepository:'https://github.com/ggerganov/llama.cpp.git',
77
- sourceRevision:'4df29be4f4c3673f428170fda944a5b19f743bb8',
78
- embeddedBy:'@wllama/wllama@3.6.0',
79
- files:[
80
- {
81
- role:'license',
82
- path:'ai/wllama/llama.cpp-LICENSE',
83
- sourceUrl:'https://raw.githubusercontent.com/ggerganov/llama.cpp/4df29be4f4c3673f428170fda944a5b19f743bb8/LICENSE',
84
- bytes:1078,
85
- sha256:'94f29bbed6a22c35b992c5c6ebf0e7c92f13b836b90f36f461c9cf2f0f1d010d'
86
- }
87
- ]
88
- }
89
- ]
90
- });
31
+ const canonicalAiComponentsBytes=await readFile(
32
+ path.join(defaultRoot,'ai','ARCANE_AI_BROWSER_WASM_COMPONENTS.json')
33
+ );
34
+ const expectedAiComponents=Object.freeze(
35
+ JSON.parse(canonicalAiComponentsBytes.toString('utf8'))
36
+ );
91
37
 
92
38
  const expectedFiles=Object.freeze([
93
39
  ['ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json','browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json','sdk-source-identity'],
40
+ ['ai/browser-kokoro-worker.mjs','browser-runtime/ai/browser-kokoro-worker.mjs','sdk-source-identity'],
41
+ ['ai/browser-speech-artifacts.mjs','browser-runtime/ai/browser-speech-artifacts.mjs','sdk-source-identity'],
42
+ ['ai/browser-speech-providers.mjs','browser-runtime/ai/browser-speech-providers.mjs','sdk-source-identity'],
43
+ ['ai/browser-speech.mjs','browser-runtime/ai/browser-speech.mjs','sdk-source-identity'],
94
44
  ['ai/browser-wasm-llm-provider.mjs','browser-runtime/ai/browser-wasm-llm-provider.mjs','sdk-source-identity'],
95
45
  ['ai/browser-wasm.mjs','browser-runtime/ai/browser-wasm.mjs','sdk-source-identity'],
46
+ ['ai/browser-whisper-worker.mjs','browser-runtime/ai/browser-whisper-worker.mjs','sdk-source-identity'],
96
47
  ['ai/browser-wllama-runtime.mjs','browser-runtime/ai/browser-wllama-runtime.mjs','sdk-source-identity'],
97
48
  ['ai/internal/sha256.mjs','browser-runtime/ai/internal/sha256.mjs','sdk-source-identity'],
98
49
  ['ai/model-controller.mjs','browser-runtime/ai/model-controller.mjs','sdk-source-identity'],
50
+ ['ai/speech-worker-client.mjs','browser-runtime/ai/speech-worker-client.mjs','sdk-source-identity'],
51
+ ['ai/speech-worker-runtime.mjs','browser-runtime/ai/speech-worker-runtime.mjs','sdk-source-identity'],
99
52
  ['ai/wllama/LICENCE','node_modules/@wllama/wllama/LICENCE','vendor-package-identity'],
100
- ['ai/wllama/index.mjs','node_modules/@wllama/wllama/esm/index.js','vendor-package-identity'],
53
+ ['ai/wllama/index.mjs','browser-runtime/ai/wllama/index.mjs','deterministic-derived-vendor'],
101
54
  ['ai/wllama/llama.cpp-LICENSE','browser-runtime/ai/wllama/llama.cpp-LICENSE','vendor-source-identity'],
102
55
  ['ai/wllama/wllama.wasm','node_modules/@wllama/wllama/esm/wasm/wllama.wasm','vendor-package-identity'],
103
56
  ['dependencies/event-pubsub/index.js','node_modules/event-pubsub/index.js','vendor-package-identity'],
@@ -47,7 +47,7 @@ export function workspaceTemplate({
47
47
  const name=displayName||`Arcane ${titleCase(appId)}`;
48
48
  const packageName=`arcane-${appId}`;
49
49
  const runtimeContentSha256=runtimeRelease?.contentSha256;
50
- const upstreamCommit=runtimeRelease?.source?.commit;
50
+ const upstreamCommit=runtimeRelease?.source?.legacyProjection?.commit;
51
51
  const browserManifestSha256=sdkBrowserRuntimeRelease?.manifestSha256;
52
52
  const browserContentSha256=sdkBrowserRuntimeRelease?.contentSha256;
53
53
  const browserSource=sdkBrowserRuntimeRelease?.source;
@@ -153,15 +153,15 @@ appropriate.
153
153
  files.set('AGENTS.md',`# ${name} development instructions
154
154
 
155
155
  - Use plain JavaScript, HTML, and CSS; do not introduce TypeScript or TSX.
156
- - Keep reusable mechanisms in Arcane OS and app-specific behavior under \`apps/${appId}/\`.
156
+ - Keep reusable portable mechanisms in the Arcane SDK and app-specific behavior under \`apps/${appId}/\`.
157
157
  - Keep \`arcane/css/theme.css\` before app styles and import \`arcane/ThemeBootstrap\` before app code runs.
158
158
  - Use \`rgb(...)\` or \`rgba(...)\` for new CSS colors.
159
159
  - Build one named app and one explicit target at a time. Native targets may be unavailable until their adapters are installed.
160
- - Run \`npm run check\` before committing.
160
+ - Run tests and checks only when the user explicitly selects verification or a distribution artifact requires it.
161
161
  `);
162
162
  files.set('README.md',`# ${name}
163
163
 
164
- This repository contains the Arcane OS application \`${appId}\`. Its source stays outside the Arcane OS repository and uses the \`${SDK_NAME}\` toolchain.
164
+ This repository contains the portable Arcane application \`${appId}\`. It embeds immutable SDK runtime bytes for distribution and has no runtime dependency on an Arcane OS source checkout.
165
165
 
166
166
  ## Start
167
167