@morit/cli 1.6.0 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/workspace.js CHANGED
@@ -1,4 +1,10 @@
1
+ import { renderV3PreviewHtml } from './preview-v3.js';
2
+ import { compileUiV3, requiresMorit19, uiV3AllowedProps } from './ui-v3.js';
3
+ import { compileScriptProject } from './script-project.js';
4
+ import { ScriptRuntime } from './script-runtime.js';
5
+ import { runtimeSnapshot } from './script-data.js';
1
6
  import { createHash, randomUUID } from "node:crypto";
7
+ import { execFile } from "node:child_process";
2
8
  import {
3
9
  chmod,
4
10
  copyFile,
@@ -11,6 +17,7 @@ import {
11
17
  } from "node:fs/promises";
12
18
  import { readFileSync } from "node:fs";
13
19
  import { homedir } from "node:os";
20
+ import { promisify } from "node:util";
14
21
  import { dirname, extname, join, relative, resolve, sep } from "node:path";
15
22
  import { pathToFileURL } from "node:url";
16
23
  import {
@@ -27,6 +34,7 @@ import { renderPreviewHtml } from "./preview.js";
27
34
  const contract = JSON.parse(
28
35
  readFileSync(new URL("../assets/plugin_contract.json", import.meta.url), "utf8"),
29
36
  );
37
+ const execFileAsync = promisify(execFile);
30
38
 
31
39
  const MAX_FILES = 64;
32
40
  const MAX_FILE_BYTES = 512 * 1024;
@@ -130,7 +138,7 @@ export class LocalWorkspace {
130
138
  await rename(temporary, this.statePath);
131
139
  }
132
140
 
133
- async createProject({ plugin_id, name, publisher, description = "", advanced = true, directory }) {
141
+ async createProject({ plugin_id, name, publisher, description = "", advanced = true, directory, ui_schema = 3 }) {
134
142
  validateMetadata(plugin_id, name, publisher, description);
135
143
  const relativePath = safeProjectPath(directory || join("morit-plugins", plugin_id.split(".").at(-1)));
136
144
  const projectRoot = this.inside(relativePath);
@@ -152,7 +160,7 @@ export class LocalWorkspace {
152
160
  version: "1.0.0",
153
161
  cloud_project_id: null,
154
162
  required_secrets: [],
155
- min_morit_version: "1.7.14",
163
+ min_morit_version: ui_schema === 3 ? "1.9.0" : "1.7.14",
156
164
  max_morit_version: "1.999.999",
157
165
  permissions: [],
158
166
  capabilities: [],
@@ -163,6 +171,13 @@ export class LocalWorkspace {
163
171
  slash_commands: [],
164
172
  data_policy: "purge",
165
173
  };
174
+ if (![2,3].includes(ui_schema)) throw new Error('ui_schema must be 2 or 3');
175
+ if(ui_schema===3){
176
+ manifest.min_morit_version='1.9.0';
177
+ manifest.ui_extensions=[{id:plugin_id+'.screen',point:'screen',title:name,permissions:[],config:{ui_schema:3,initial_state:{},view:{type:'column',children:[{type:'text',props:{text:'{{= runtime.today }}'}}]}}}];
178
+ } else {
179
+ manifest.ui_extensions=[{id:plugin_id+'.screen',point:'screen',title:name,permissions:[],config:{ui_schema:2,initial_state:{},view:{type:'column',children:[{type:'text',props:{text:name}}]}}}];
180
+ }
166
181
  await writeFile(join(projectRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
167
182
  await writeFile(
168
183
  join(projectRoot, "README.md"),
@@ -279,16 +294,39 @@ export class LocalWorkspace {
279
294
  };
280
295
  }
281
296
 
282
- async preview(projectId) {
297
+ async runtimeTest(projectId, options) {
298
+ const project = await this.getProject(projectId, true);
299
+ const result = await runtimeTestProjectFiles(project.files, options);
300
+ const manifest = compileManifest(project.files);
301
+ const fixture = options.fixture ?? 'fixtures/live.json';
302
+ const files = {...project.files, [fixture]: JSON.stringify(result.inspector.raw_response)};
303
+ const html = renderV3PreviewHtml(manifest, files);
304
+ const fileName = `${manifest.id}-runtime-test.html`;
305
+ const target = join(project.workspace_path, "dist", fileName);
306
+ await mkdir(dirname(target), { recursive: true });
307
+ await writeFile(target, html);
308
+ const preview = await this.recordArtifact(projectId, "preview", fileName, "text/html", target, html, null);
309
+ return { project_id: projectId, revision: project.revision, ...result, preview };
310
+ }
311
+
312
+ async preview(projectId, {target: previewTarget = 'semantic'} = {}) {
283
313
  const project = await this.getProject(projectId, true);
284
314
  const manifest = compileManifest(project.files);
285
315
  validateManifest(manifest, project.files);
286
316
  const fileName = `${manifest.id}-preview.html`;
287
317
  const target = join(project.workspace_path, "dist", fileName);
288
318
  await mkdir(dirname(target), { recursive: true });
289
- const html = previewHtml(manifest);
319
+ const html = manifest.ui_extensions.some(e=>e.config?.ui_schema===3) ? renderV3PreviewHtml(manifest,project.files) : previewHtml(manifest);
290
320
  await writeFile(target, html);
291
- return this.recordArtifact(projectId, "preview", fileName, "text/html", target, html, null);
321
+ const artifact=await this.recordArtifact(projectId, "preview", fileName, "text/html", target, html, null);
322
+ if(previewTarget!=='android')return {...artifact,preview_mode:'semantic'};
323
+ try{
324
+ const {stdout}=await execFileAsync(process.env.ADB??'adb',['devices'],{timeout:3000});
325
+ const serial=stdout.split('\n').map(line=>line.trim().split(/\s+/)).find(parts=>parts[1]==='device')?.[0];
326
+ if(!serial)return {...artifact,preview_mode:'semantic',fallback_reason:'No Android emulator or device is available'};
327
+ await execFileAsync(process.env.ADB??'adb',['-s',serial,'shell','am','start','-W','-a','android.intent.action.VIEW','-d',`morit://open/plugin/${encodeURIComponent(manifest.id)}`],{timeout:10000});
328
+ return {...artifact,preview_mode:'android',device_serial:serial};
329
+ }catch(error){return {...artifact,preview_mode:'semantic',fallback_reason:`Android Host preview unavailable: ${error.message}`};}
292
330
  }
293
331
 
294
332
  async sourceDownload(projectId, requestedFileName) {
@@ -415,6 +453,136 @@ export class LocalWorkspace {
415
453
  }
416
454
  }
417
455
 
456
+ export async function runtimeTestProjectDirectory(source, options) {
457
+ const number=value=>value===undefined?undefined:Number(value);
458
+ const screen=options?.screen??{
459
+ width:number(options?.['screen-width']),height:number(options?.['screen-height']),text_scale:number(options?.['screen-text-scale']),
460
+ orientation:options?.['screen-orientation'],size_class:options?.['screen-size-class'],platform:options?.['screen-platform'],
461
+ };
462
+ return runtimeTestProjectFiles(await readStandaloneProjectFiles(resolve(source)), {...options,screen});
463
+ }
464
+
465
+ export async function runtimeTestProjectFiles(files, options = {}) {
466
+ if ((options.mode ?? 'fixture') === 'fixture') return runtimeTestFiles(files, options);
467
+ if (options.mode !== 'live') throw new Error('mode must be fixture or live');
468
+ const origin = new URL(process.env.MORIT_RUNTIME_TEST_BACKEND_URL ?? 'https://invalid.local');
469
+ const token = process.env.MORIT_RUNTIME_TEST_HOST_TOKEN;
470
+ if (origin.protocol !== 'https:' || origin.username || origin.password || !token || !process.env.MORIT_RUNTIME_TEST_BACKEND_URL) throw new Error('Live mode requires configured HTTPS Host URL and Host access token');
471
+ const manifest = JSON.parse(files['manifest.json']);
472
+ if (!manifest.capabilities?.some(c => c.id === options.capability)) throw new Error('Unknown project capability');
473
+ const response = await fetch(new URL(`/v1/plugins/${encodeURIComponent(manifest.id)}/capabilities/${encodeURIComponent(options.capability)}/runtime-test`, origin), {
474
+ method:'POST', redirect:'error', signal:AbortSignal.timeout(20000), headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json','X-Morit-Plugin-Sdk':'1.9.0'},body:JSON.stringify({arguments:options.arguments??{}}),
475
+ });
476
+ if (!response.ok) throw new Error('Live capability request failed; check installation and read-only connector permissions');
477
+ const reader=response.body.getReader();let size=0;const chunks=[];
478
+ try {for (;;) {const {value,done}=await reader.read();if(done)break;size+=value.length;if(size>262144)throw new Error('Live response exceeds 256 KiB');chunks.push(value);}} finally {await reader.cancel();}
479
+ const live=JSON.parse(Buffer.concat(chunks).toString());if(live.state!=='completed')throw new Error('Live capability did not complete');
480
+ const fixture=options.fixture??'fixtures/live.json';
481
+ return {...runtimeTestFiles({...files,[fixture]:JSON.stringify(live.data)},{...options,fixture}),mode:'live'};
482
+ }
483
+
484
+ export function runtimeTestFiles(files, { module, export: exportName, fixture, extension, now, timezone = 'UTC', locale = 'en-US', screen = {} } = {}) {
485
+ const fixturePath=fixture??['fixtures/live.json','fixtures/day.json'].find(path=>Object.hasOwn(files,path));
486
+ if (typeof fixturePath !== 'string' || !/^fixtures\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*\.json$/.test(fixturePath)) throw new Error('fixture must be fixtures/*.json');
487
+ if (!Object.hasOwn(files, fixturePath)) throw new Error('Fixture not found');
488
+ const manifest = files['manifest.json'] ? compileManifest(files) : null;
489
+ const config = extension ? manifest?.ui_extensions?.find(value => value.id === extension)?.config : files['runtime.json'] ? JSON.parse(files['runtime.json']) : manifest?.ui_extensions?.find(value => value.config?.ui_schema === 3)?.config;
490
+ if (!config || config.ui_schema !== 3) throw new Error('Select a Runtime v3 extension or provide runtime.json');
491
+ if(!config.compiled)compileUiV3(config,files,{capabilities:new Set((manifest?.capabilities??[]).map(value=>value.id)),routes:new Set((manifest?.ui_extensions??[]).map(value=>value.id)),nodeTypes:contract.ui_nodes,runtimeContract:contract.ui_runtime_v3,baseRuntimeContract:contract.ui_runtime});
492
+ const compiled = compileScriptProject(config, files);
493
+ const runtime = runtimeSnapshot({now, timezone, locale, screen: {
494
+ width: screen.width ?? 390, height: screen.height ?? 844,
495
+ orientation: screen.orientation ?? ((screen.width ?? 390) >= (screen.height ?? 844) ? 'landscape' : 'portrait'),
496
+ size_class: screen.size_class ?? ((screen.width ?? 390) < 600 ? 'compact' : (screen.width ?? 390) < 840 ? 'medium' : 'expanded'),
497
+ text_scale: screen.text_scale ?? 1, platform: screen.platform ?? 'preview',
498
+ }});
499
+ const input = JSON.parse(files[fixturePath]);
500
+ const redact = value => Array.isArray(value) ? value.map(redact) : value && typeof value === 'object'
501
+ ? Object.fromEntries(Object.entries(value).map(([key,item]) => [key, /authorization|password|secret|token|api[_-]?key/i.test(key) ? '[REDACTED]' : redact(item)])) : value;
502
+ const state={},computed={},data={},raw={},errors={},locals={},transformSteps=[],timeline=[];
503
+ const scope=extra=>({state,computed,data,runtime,context:{platform:'preview'},...extra});
504
+ function evaluate(value, extra={}) {
505
+ if(typeof value==='string'){
506
+ const matches=[...value.matchAll(/\{\{=([\s\S]*?)\}\}/g)];
507
+ const run=source=>{
508
+ const key=createHash('sha256').update(source.trim()).digest('hex');
509
+ const ast=config.compiled.expressions[key];
510
+ if(!ast)throw new Error(`Missing compiled expression ${source.trim()}`);
511
+ return new ScriptRuntime({modules:compiled.modules,profile:'inline'}).evaluate(ast,scope(extra)).value;
512
+ };
513
+ if(matches.length===1&&matches[0][0]===value)return run(matches[0][1]);
514
+ return value.replace(/\{\{=([\s\S]*?)\}\}/g,(_,source)=>run(source)??'');
515
+ }
516
+ if(Array.isArray(value))return value.map(item=>evaluate(item,extra));
517
+ if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,evaluate(item,extra)]));
518
+ return value;
519
+ }
520
+ for(const [key,value] of Object.entries(config.initial_state??{}))state[key]=evaluate(value);
521
+ const invoke=(transform,value,extra={})=>new ScriptRuntime({modules:compiled.modules}).invoke(transform.module,transform.export,value,scope(extra));
522
+ if(module&&exportName){
523
+ const result=new ScriptRuntime({modules:compiled.modules}).invoke(module,exportName,input,{runtime});
524
+ data.result=result.value;transformSteps.push({module,export:exportName,input:redact(input),output:redact(result.value),diagnostics:result.diagnostics});
525
+ }else for(const id of config.compiled.source_order??[]){
526
+ const source=config.data_sources.find(item=>item.id===id);
527
+ if(!source||source.when!==undefined&&!evaluate(source.when))continue;
528
+ const sourceInput=input&&typeof input==='object'&&Object.hasOwn(input,id)?input[id]:input;
529
+ raw[id]=redact(sourceInput);let value=sourceInput;
530
+ try{
531
+ for(const transform of Array.isArray(source.transform)?source.transform:source.transform?[source.transform]:[]){
532
+ const before=redact(value),result=invoke(transform,value);value=result.value;
533
+ transformSteps.push({source:id,module:transform.module,export:transform.export,input:before,output:redact(value),diagnostics:result.diagnostics});
534
+ }
535
+ data[id]=value;timeline.push({type:'source_completed',source:id});
536
+ }catch(error){errors[id]=String(error.message??error);timeline.push({type:'source_failed',source:id,error:errors[id]});}
537
+ }
538
+ for(const key of config.compiled.computed_order??[]){
539
+ const definition=config.computed_state?.[key];
540
+ computed[key]=definition?.module?invoke(definition,evaluate(definition.input)).value:evaluate(definition?.expression??definition);
541
+ }
542
+ let rendered=0;
543
+ const slot=(value,extra)=>[...(Array.isArray(value)?value:value?[value]:[])].map(item=>resolve(item,extra));
544
+ function resolve(node,extra={}){
545
+ if(!node||typeof node!=='object'||++rendered>2000)throw new Error('Resolved UI tree exceeds 2000 nodes');
546
+ if(node.visible_when!==undefined&&!evaluate(node.visible_when,extra))return {type:'hidden'};
547
+ const rawProps=node.props??{},deferred={...rawProps},slots=node.slots??{};
548
+ if(node.type==='repeat')delete deferred.key;if(node.type==='component')delete deferred.events;
549
+ const props=evaluate(deferred,extra);
550
+ for(const [key,value] of Object.entries(props))if(value&&typeof value==='object'&&!Array.isArray(value)&&['compact','medium','expanded'].some(name=>Object.hasOwn(value,name)))props[key]=value[runtime.screen.size_class]??value.compact??Object.values(value)[0];
551
+ if(node.type==='if')return {type:'fragment',children:slot(slots[props.condition?'then':'else'],extra)};
552
+ if(node.type==='repeat')return {type:'repeat',children:(Array.isArray(props.items)?props.items:[]).slice(0,200).map((item,index)=>({key:evaluate(rawProps.key,{...extra,[rawProps.as]:item,[rawProps.index_as??'index']:index}),children:slot(slots.item??node.children,{...extra,[rawProps.as]:item,[rawProps.index_as??'index']:index})}))};
553
+ if(node.type==='component'){
554
+ const definition=config.components[props.component],key=`${props.component}:${props.key??extra._key??node.id??''}`;
555
+ const local=locals[key]??=evaluate(definition.initial_state??{},extra),componentProps={...evaluate(definition.default_props??{},extra),...(props.props??{})},localComputed={};
556
+ const next={...extra,props:componentProps,state:local,root:{state},computed:localComputed,_slots:slots,_parent:extra};
557
+ for(const [name,value] of Object.entries(definition.computed_state??{}))localComputed[name]=evaluate(value,next);
558
+ return {type:'component',component:props.component,key,props:componentProps,child:resolve(definition.view,next)};
559
+ }
560
+ if(node.type==='slot')return {type:'fragment',children:slot(extra._slots?.[props.name],extra._parent??extra)};
561
+ if(node.type==='data_state'){
562
+ const status=errors[props.source]?'error':data[props.source]===undefined?'loading':props.empty_when===true||Array.isArray(data[props.source])&&!data[props.source].length?'empty':'content';
563
+ return {type:'data_state',source:props.source,status,children:slot(slots[status],{...extra,error:errors[props.source]??null})};
564
+ }
565
+ return {type:node.type,...(node.id?{id:node.id}:{}),props,...(node.action?{action:evaluate(node.action,extra)}:{}),children:(node.children??[]).map(child=>resolve(child,extra))};
566
+ }
567
+ const semanticTree=resolve(config.view);
568
+ const output=redact(Object.keys(data).length===1?Object.values(data)[0]:data);
569
+ return {
570
+ mode: 'fixture', valid: true, runtime, output,
571
+ diagnostics: transformSteps.map(step=>step.diagnostics), warnings: compiled.warnings,
572
+ inspector: {
573
+ root_state:redact(state),local_state:redact(locals),computed_state:redact(computed),raw_response:redact(input),raw_data:raw,data:redact(data),errors,
574
+ transform_steps:transformSteps,dependencies:Object.fromEntries((config.data_sources??[]).map(source=>[source.id,source.depends_on??[]])),
575
+ request_timeline:timeline.slice(-200),cache:[],execution:transformSteps.map(step=>step.diagnostics),component_instances:Object.keys(locals),
576
+ repeat_scopes:[],layout:{screen:runtime.screen,overflow:[]},source_map:[],runtime,resolved_ui_tree:redact(semanticTree),
577
+ },
578
+ };
579
+ }
580
+
581
+ export async function compileProjectDirectory(source) {
582
+ const loaded = await loadProjectDirectory(source);
583
+ return {manifest: loaded.manifest};
584
+ }
585
+
418
586
  export async function validateProjectDirectory(source) {
419
587
  const loaded = await loadProjectDirectory(source);
420
588
  return projectDirectoryResult(loaded);
@@ -444,7 +612,7 @@ export async function previewProjectDirectory(source, output) {
444
612
  join(loaded.root, "dist", `${loaded.manifest.id}-preview.html`),
445
613
  ".html",
446
614
  );
447
- const content = previewHtml(loaded.manifest);
615
+ const content = loaded.manifest.ui_extensions.some(e=>e.config?.ui_schema===3) ? renderV3PreviewHtml(loaded.manifest,loaded.files) : previewHtml(loaded.manifest);
448
616
  await mkdir(dirname(target), { recursive: true });
449
617
  await writeFile(target, content);
450
618
  return {
@@ -694,6 +862,7 @@ function installablePackagePath(name) {
694
862
  if (["manifest.json", "signature.json", "README.md"].includes(name)) return true;
695
863
  const parts = name.split("/");
696
864
  if (parts[0] === "assets" && parts.length >= 2) return SAFE_ASSET.has(extname(name).toLowerCase());
865
+ if (parts[0] === "logic" && parts.length >= 2) return /\.(morit|ast\.json)$/.test(name);
697
866
  if (parts[0] === "src" && parts.length >= 2) return name.toLowerCase().endsWith(".py");
698
867
  return parts[0] === "children" && parts.length === 2 && name.toLowerCase().endsWith(".mplg");
699
868
  }
@@ -799,6 +968,8 @@ function validateManifest(manifest, files) {
799
968
  const minimum = manifest.min_morit_version.split(".").map(Number);
800
969
  if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 14)))) && uiExtensions.some(extension => extension.config?.bottom_bar != null || usesUi1714(extension.config?.view))) throw new Error("pagination and bottom_bar require min_morit_version 1.7.14 or newer");
801
970
  if ((minimum[0] < 1 || (minimum[0] === 1 && (minimum[1] < 7 || (minimum[1] === 7 && minimum[2] < 13)))) && uiExtensions.some(extension => usesUi1713(extension.config?.view))) throw new Error("new UI properties require min_morit_version 1.7.13 or newer");
971
+ if (uiExtensions.some(e => e.config?.ui_schema === 3) && (minimum[0] < 1 || (minimum[0] === 1 && minimum[1] < 8))) throw new Error("Runtime v3 requires min_morit_version 1.8.0");
972
+ if (uiExtensions.some(e => requiresMorit19(e.config, contract.ui_runtime_v3)) && (minimum[0] < 1 || (minimum[0] === 1 && minimum[1] < 9))) throw new Error("Runtime v3.1 features require min_morit_version 1.9.0");
802
973
  const credentials = objectCollection(manifest.credentials === undefined ? [] : manifest.credentials, "manifest.credentials", 16);
803
974
  const slashCommands = objectCollection(manifest.slash_commands === undefined ? [] : manifest.slash_commands, "manifest.slash_commands", 32);
804
975
  const connectors = objectCollection(manifest.connectors === undefined ? [] : manifest.connectors, "manifest.connectors", 32);
@@ -858,7 +1029,7 @@ function validateManifest(manifest, files) {
858
1029
  assertObject(extensionConfig, `UI ${extension.id} config`);
859
1030
  // Twelve legal component levels add an object and a children array per
860
1031
  // level before semantic UI validation runs.
861
- assertJsonSize(extensionConfig, 32 * 1024, `UI ${extension.id} config`, 40);
1032
+ assertJsonSize(extensionConfig, extensionConfig.ui_schema === 3 ? 512 * 1024 : 32 * 1024, `UI ${extension.id} config`, 40);
862
1033
  }
863
1034
  const storage = validateStorage(manifest, requestedPermissions);
864
1035
  for (const extension of uiExtensions) validateUiConfig(extension, capabilities, uiIds, files, storage !== null);
@@ -885,13 +1056,22 @@ function validateManifest(manifest, files) {
885
1056
  ].filter(Boolean).join("; ");
886
1057
  throw new Error(`every src/*.py file must be declared by exactly one sandbox_python entrypoint (${details})`);
887
1058
  }
888
- const entries = new Map([["manifest.json", canonicalJson(manifest)]]);
1059
+ const compiledManifest = canonicalJson(manifest);
1060
+ if (compiledManifest.length > 256 * 1024) throw new Error("compiled manifest.json exceeds Morit's 256 KiB install limit");
1061
+ const entries = new Map([["manifest.json", compiledManifest]]);
889
1062
  for (const [name, content] of Object.entries(files)) {
890
1063
  if (name === "README.md") entries.set(name, projectFileBytes(name, content));
891
1064
  else if (name.startsWith("assets/") && SAFE_ASSET.has(extname(name).toLowerCase())) entries.set(name, projectFileBytes(name, content));
1065
+ else if (name.startsWith("logic/") && name.endsWith(".morit")) entries.set(name, projectFileBytes(name, content));
892
1066
  else if (name.startsWith("src/") && name.endsWith(".py")) entries.set(name, projectFileBytes(name, content));
893
1067
  else if (name.startsWith("children/") && name.endsWith(".mplg")) entries.set(name, projectFileBytes(name, content));
894
1068
  }
1069
+ for (const extension of uiExtensions) {
1070
+ for (const declaration of extension.config?.logic_modules ?? []) {
1071
+ const compiled = extension.config.compiled.modules[declaration.id];
1072
+ entries.set(declaration.path.replace(/\.morit$/, '.ast.json'), canonicalJson({version:compiled.version,source_sha256:compiled.source_sha256,ast:compiled.ast}));
1073
+ }
1074
+ }
895
1075
  const declaredChildren = new Set(dependencies.filter((value) => value.package_path).map((value) => value.package_path));
896
1076
  const packagedChildren = new Set([...entries.keys()].filter((name) => name.startsWith("children/")));
897
1077
  if (!equalSets(declaredChildren, packagedChildren)) {
@@ -1231,6 +1411,7 @@ function validateHttpRuntime(runtime, endpoint) {
1231
1411
  if (new URLSearchParams(entries).toString().length > 16 * 1024) throw new Error("http_json request_params is too large");
1232
1412
  }
1233
1413
  const response = runtime.response || {};
1414
+ if(response.result_mode!==undefined&&response.result_mode!=="raw")throw new Error("invalid response.result_mode");
1234
1415
  rejectUnknownFields(response, new Set(contract.object_fields.http_json_response), "http_json response");
1235
1416
  const projection = "summary_path" in response || "data_path" in response;
1236
1417
  if (projection && (Object.keys(response).some(name => !["summary_path", "data_path", "summary_prefix"].includes(name)) ||
@@ -1285,11 +1466,6 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
1285
1466
  if (!permissions.has("storage")) throw new Error("calendar_store requires storage permission");
1286
1467
  if (runtime.operation === "reminder" && !permissions.has("notifications")) throw new Error("calendar reminder requires notifications permission");
1287
1468
  }
1288
- if (adapter === "neis_school") {
1289
- if (!permissions.has("network") || !permissions.has("storage")) throw new Error("neis_school requires network and storage permissions");
1290
- if (!["school_search", "setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
1291
- if (["reminder", "briefing"].includes(runtime.operation) && !permissions.has("notifications")) throw new Error("NEIS reminder requires notifications permission");
1292
- }
1293
1469
  if (capability.kind === "provider" && (runtime.role !== "search" || !adapter)) throw new Error("provider capabilities must declare a search runtime");
1294
1470
  if (capability.kind === "background" && Object.keys(runtime).length) {
1295
1471
  if (!Number.isInteger(runtime.interval_minutes) || runtime.interval_minutes < 15 || runtime.interval_minutes > 10080) {
@@ -1322,6 +1498,11 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
1322
1498
  output_schema: value.output_schema,
1323
1499
  }]));
1324
1500
  if (hasStorage) for (const capability of STORAGE_TOOLS) executable.add(capability);
1501
+ if (config.ui_schema === 3) {
1502
+ compileUiV3(config, files, {capabilities: executable, routes: routeIds, nodeTypes: contract.ui_nodes, runtimeContract: contract.ui_runtime_v3, baseRuntimeContract: contract.ui_runtime});
1503
+ validateUiCapabilityBindings(config, config.data_sources ?? [], capabilitySchemas);
1504
+ return;
1505
+ }
1325
1506
  if (extension.point === "response" && config.ui_schema !== 2) throw new Error("response UI requires UI Runtime v2");
1326
1507
  if (config.ui_schema === 2) {
1327
1508
  if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
@@ -1588,13 +1769,21 @@ function validateThemeContrast(theme) {
1588
1769
  function collectThemeWarnings(manifest) {
1589
1770
  const warnings = [];
1590
1771
  for (const extension of manifest.ui_extensions || []) {
1772
+ const v3 = extension.config?.ui_schema === 3;
1591
1773
  function visit(node, path) {
1592
1774
  if (!node || typeof node !== "object") return;
1593
- const allowed = contract.ui_runtime.component_props[node.type] || [];
1594
- for (const key of Object.keys(node.props || {})) if (!( !contract.ui_runtime.common_props_excluded_components.includes(node.type) && contract.ui_runtime.common_props.includes(key)) && !allowed.includes(key)) warnings.push(`${path}.props.${key}: ignored by ${node.type}; remove or move to a supported component`);
1775
+ const allowed = v3
1776
+ ? uiV3AllowedProps(node.type, contract.ui_runtime_v3, contract.ui_runtime)
1777
+ : new Set([
1778
+ ...(contract.ui_runtime.component_props[node.type] || []),
1779
+ ...(contract.ui_runtime.common_props_excluded_components.includes(node.type) ? [] : contract.ui_runtime.common_props),
1780
+ ]);
1781
+ for (const key of Object.keys(node.props || {})) if (!allowed.has(key)) warnings.push(`${path}.props.${key}: ignored by ${node.type}; remove or move to a supported component`);
1595
1782
  (node.children || []).forEach((child, index) => visit(child, `${path}.children[${index}]`));
1783
+ for (const [name, value] of Object.entries(node.slots || {})) (Array.isArray(value) ? value : [value]).forEach((child, index) => visit(child, `${path}.slots.${name}[${index}]`));
1596
1784
  }
1597
1785
  for (const field of ["view", "bottom_bar"]) visit(extension.config?.[field], `${extension.id}.${field}`);
1786
+ if (v3) for (const [name, component] of Object.entries(extension.config?.components || {})) visit(component.view, `${extension.id}.components.${name}.view`);
1598
1787
  const theme = extension?.config?.theme;
1599
1788
  if (!theme || typeof theme !== "object" || Array.isArray(theme)) continue;
1600
1789
  for (const [mode, variant] of [["base", theme], ["light", theme.light], ["dark", theme.dark]]) {
@@ -1952,7 +2141,7 @@ function validateUiAction(action, executable, routes, sources, stateKeys, requir
1952
2141
  invoke: new Set(["type", "capability", "query", "arguments", "store", "validate"]),
1953
2142
  navigate: new Set(["type", "target", "transition", "replace", "parameters", "result_state", "validate"]),
1954
2143
  set_state: new Set(["type", "values", "persist"]),
1955
- refresh: new Set(["type", "source"]),
2144
+ refresh: new Set(["type", "source", "validate"]),
1956
2145
  back: new Set(["type", "result"]),
1957
2146
  }[action.type];
1958
2147
  rejectUnknownFields(action, allowed, "UI action");