@morit/cli 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +3 -5
  2. package/assets/plugin_contract.json +33 -8
  3. package/bin/morit.js +0 -0
  4. package/package.json +1 -1
  5. package/src/cli.js +5 -0
  6. package/src/preview.js +254 -17
  7. package/src/workspace.js +218 -19
  8. package/assets/docs/README.md +0 -107
  9. package/assets/docs/ai-response-and-timeline.md +0 -203
  10. package/assets/docs/ai-skill-and-docx-workflow.md +0 -83
  11. package/assets/docs/app-builder.md +0 -56
  12. package/assets/docs/authentication.md +0 -140
  13. package/assets/docs/components.md +0 -216
  14. package/assets/docs/design-tokens-responsive.md +0 -171
  15. package/assets/docs/docs-index.json +0 -94
  16. package/assets/docs/examples-notion.md +0 -83
  17. package/assets/docs/examples-school-life.md +0 -79
  18. package/assets/docs/getting-started.md +0 -132
  19. package/assets/docs/information-hierarchy.md +0 -81
  20. package/assets/docs/instances-and-connectors.md +0 -93
  21. package/assets/docs/lifecycle-and-api.md +0 -169
  22. package/assets/docs/local-cli.md +0 -125
  23. package/assets/docs/manifest.md +0 -234
  24. package/assets/docs/packaging-and-testing.md +0 -121
  25. package/assets/docs/permissions-and-data.md +0 -149
  26. package/assets/docs/platform-compatibility.md +0 -62
  27. package/assets/docs/plugin-storage.md +0 -175
  28. package/assets/docs/project-structure.md +0 -102
  29. package/assets/docs/remote-mcp.md +0 -158
  30. package/assets/docs/school-life-privacy.md +0 -55
  31. package/assets/docs/screens-layout-navigation.md +0 -95
  32. package/assets/docs/sdk-and-mcp.md +0 -199
  33. package/assets/docs/tool-and-skill.md +0 -172
  34. package/assets/docs/troubleshooting.md +0 -121
  35. package/assets/docs/ui-extensions.md +0 -75
  36. package/assets/docs/ui-runtime-v2.md +0 -343
  37. package/assets/docs/verification.md +0 -133
package/src/workspace.js CHANGED
@@ -40,7 +40,7 @@ const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
40
40
  const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|clock\$|com[1-9]|lpt[1-9])(?:\.|$)/i;
41
41
  const UI_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
42
42
  const UI_STATE_KEY = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
43
- const UI_BINDING = /^(?:state|data|item)(?:\.[A-Za-z0-9_-]+){0,12}$/;
43
+ const UI_BINDING = /^(?:state|data|item|context)(?:\.[A-Za-z0-9_-]+){0,12}$/;
44
44
  const UI_TEMPLATE_BINDING = /\{\{\s*([^{}]+?)\s*\}\}/g;
45
45
  const UI_SINGLE_TEMPLATE_BINDING = /^\{\{\s*([^{}]+?)\s*\}\}$/;
46
46
  const UI_ASSET_PATH = /^assets\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*\.(?:gif|jpe?g|png|webp)$/i;
@@ -152,7 +152,7 @@ export class LocalWorkspace {
152
152
  version: "1.0.0",
153
153
  cloud_project_id: null,
154
154
  required_secrets: [],
155
- min_morit_version: "1.7.8",
155
+ min_morit_version: "1.7.12",
156
156
  max_morit_version: "1.999.999",
157
157
  permissions: [],
158
158
  capabilities: [],
@@ -727,10 +727,10 @@ async function signingKeyForPublisher(keyDirectory, publisher) {
727
727
  }
728
728
  }
729
729
 
730
- function previewHtml(manifest) {
731
- return renderPreviewHtml(manifest);
732
- }
733
-
730
+ function previewHtml(manifest) {
731
+ return renderPreviewHtml(manifest);
732
+ }
733
+
734
734
  function validateMetadata(pluginId, name, publisher, description) {
735
735
  if (!PLUGIN_ID.test(pluginId) || pluginId.length > 120) throw new Error("plugin_id must be a reverse-domain lowercase identifier");
736
736
  if (typeof name !== "string" || name.trim().length < 1 || name.trim().length > 80) throw new Error("name must contain 1 to 80 characters");
@@ -813,6 +813,12 @@ function validateManifest(manifest, files) {
813
813
  throw new Error(`unsupported runtime adapter: ${adapter || "missing"}`);
814
814
  }
815
815
  validateRuntime(capability, runtime, permissions, credentials, connectors, dependencies);
816
+ for (const [field, schema] of [["input_schema", capability.input_schema], ["output_schema", capability.output_schema]]) {
817
+ if (schema === undefined) continue;
818
+ assertObject(schema, `capability ${capability.id} ${field}`);
819
+ if (Buffer.byteLength(JSON.stringify(schema), "utf8") > 16 * 1024) throw new Error(`capability ${capability.id} ${field} is too large`);
820
+ validateResponseSchema(schema, 0, true);
821
+ }
816
822
  if (adapter === "sandbox_python") {
817
823
  const entrypoint = normalizeEntrypointPath(runtime.entrypoint);
818
824
  if (!entrypoint.startsWith("src/") || !entrypoint.endsWith(".py")) throw new Error("sandbox_python entrypoint must be src/*.py");
@@ -1182,6 +1188,54 @@ function validateSlashCommands(commands, capabilities) {
1182
1188
  }
1183
1189
  }
1184
1190
 
1191
+ function validateHttpRuntime(runtime, endpoint) {
1192
+ let url;
1193
+ try { url = new URL(endpoint); } catch { throw new Error("http_json requires a valid HTTPS endpoint"); }
1194
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443") || endpoint.length > 2048) {
1195
+ throw new Error("http_json requires a valid HTTPS endpoint");
1196
+ }
1197
+ if (!["GET", "POST"].includes(String(runtime.method || "POST").toUpperCase())) {
1198
+ throw new Error("http_json supports GET and POST");
1199
+ }
1200
+ if (runtime.request_body != null) assertObject(runtime.request_body, "http_json request_body");
1201
+ if (runtime.request_params != null) {
1202
+ const params = runtime.request_params;
1203
+ assertObject(params, "http_json request_params");
1204
+ if (Object.keys(params).length > 32) throw new Error("http_json request_params accepts at most 32 parameters");
1205
+ const entries = [];
1206
+ for (const [name, value] of Object.entries(params)) {
1207
+ if (!name.length || name.length > 80 || /[\x00-\x1f]/.test(name)) throw new Error("invalid http_json parameter name");
1208
+ const values = Array.isArray(value) ? value : [value];
1209
+ if (values.length > 64 || values.some(item => item != null && (
1210
+ !["string", "number", "boolean"].includes(typeof item) || typeof item === "number" && !Number.isFinite(item)
1211
+ ))) throw new Error("http_json parameters must be scalars or scalar lists");
1212
+ for (const item of values) if (item != null) entries.push([name, String(item)]);
1213
+ }
1214
+ if (new URLSearchParams(entries).toString().length > 16 * 1024) throw new Error("http_json request_params is too large");
1215
+ }
1216
+ const response = runtime.response || {};
1217
+ rejectUnknownFields(response, new Set(contract.object_fields.http_json_response), "http_json response");
1218
+ const projection = "summary_path" in response || "data_path" in response;
1219
+ if (projection && (Object.keys(response).some(name => !["summary_path", "data_path", "summary_prefix"].includes(name)) ||
1220
+ !("summary_path" in response) && !("summary_prefix" in response))) {
1221
+ throw new Error("http_json cannot mix object and collection response settings");
1222
+ }
1223
+ const validPath = value => typeof value === "string" && value.length > 0 && value.length <= 240 && value.split(".").every(part => /^(?:\*|[A-Za-z0-9_-]{1,80})$/.test(part));
1224
+ for (const name of ["summary_path", "data_path", "collection_path", "id_path", "url_path"]) {
1225
+ if (name in response && (!validPath(response[name]) || ["summary_path", "data_path"].includes(name) && response[name].split(".").includes("*"))) {
1226
+ throw new Error(`invalid http_json response.${name}`);
1227
+ }
1228
+ }
1229
+ for (const name of ["title_paths", "content_paths"]) {
1230
+ if (name in response && (!Array.isArray(response[name]) || response[name].length > 24 || response[name].some(path => !validPath(path)))) {
1231
+ throw new Error(`invalid http_json response.${name}`);
1232
+ }
1233
+ }
1234
+ if ("max_items" in response && (!Number.isInteger(response.max_items) || response.max_items < 1 || response.max_items > 20)) {
1235
+ throw new Error("invalid http_json response.max_items");
1236
+ }
1237
+ }
1238
+
1185
1239
  function validateRuntime(capability, runtime, permissions, credentials, connectors, dependencies) {
1186
1240
  const adapter = runtime.adapter;
1187
1241
  const credentialIds = new Set(credentials.map((value) => value.id));
@@ -1192,9 +1246,13 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
1192
1246
  if (adapter === "text_template" && (typeof runtime.template !== "string" || !runtime.template.trim() || runtime.template.length > 2000)) {
1193
1247
  throw new Error("text_template requires a bounded template");
1194
1248
  }
1195
- if (adapter === "http_json" && effectiveCredential && (
1196
- !credentialIds.has(effectiveCredential) || !permissions.has("network") || !permissions.has("credentials")
1197
- )) throw new Error("http_json requires a declared credential and network/credentials permissions");
1249
+ if (adapter === "http_json") {
1250
+ if (!permissions.has("network")) throw new Error("http_json requires network permission");
1251
+ if (effectiveCredential && (!credentialIds.has(effectiveCredential) || !permissions.has("credentials"))) {
1252
+ throw new Error("http_json requires a declared credential");
1253
+ }
1254
+ validateHttpRuntime(runtime, effectiveEndpoint);
1255
+ }
1198
1256
  if (adapter === "mcp_http") {
1199
1257
  if (!permissions.has("network")) throw new Error("mcp_http requires network permission");
1200
1258
  if (effectiveCredential && (!credentialIds.has(effectiveCredential) || !permissions.has("credentials"))) throw new Error("mcp_http requires a declared credential");
@@ -1212,7 +1270,7 @@ function validateRuntime(capability, runtime, permissions, credentials, connecto
1212
1270
  }
1213
1271
  if (adapter === "neis_school") {
1214
1272
  if (!permissions.has("network") || !permissions.has("storage")) throw new Error("neis_school requires network and storage permissions");
1215
- if (!["setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
1273
+ if (!["school_search", "setup", "lookup", "overview", "search", "reminder", "briefing"].includes(runtime.operation)) throw new Error("invalid neis_school operation");
1216
1274
  if (["reminder", "briefing"].includes(runtime.operation) && !permissions.has("notifications")) throw new Error("NEIS reminder requires notifications permission");
1217
1275
  }
1218
1276
  if (capability.kind === "provider" && (runtime.role !== "search" || !adapter)) throw new Error("provider capabilities must declare a search runtime");
@@ -1242,11 +1300,15 @@ function validateRuntimeReferences(capabilities, connectors, connectorIds, depen
1242
1300
  function validateUiConfig(extension, capabilities, routeIds, files, hasStorage = false) {
1243
1301
  const config = extension.config === undefined ? {} : extension.config;
1244
1302
  const executable = new Set(capabilities.filter((value) => ["tool", "skill", "provider", "notification"].includes(value.kind)).map((value) => value.id));
1303
+ const capabilitySchemas = new Map(capabilities.map((value) => [value.id, {
1304
+ input_schema: value.input_schema,
1305
+ output_schema: value.output_schema,
1306
+ }]));
1245
1307
  if (hasStorage) for (const capability of STORAGE_TOOLS) executable.add(capability);
1246
1308
  if (extension.point === "response" && config.ui_schema !== 2) throw new Error("response UI requires UI Runtime v2");
1247
1309
  if (config.ui_schema === 2) {
1248
1310
  if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
1249
- validateUiRuntimeV2(config, executable, routeIds);
1311
+ validateUiRuntimeV2(config, executable, routeIds, capabilitySchemas);
1250
1312
  validateResponseComponent(config, extension.point === "response");
1251
1313
  for (const asset of uiRuntimeAssetPaths(config.view)) {
1252
1314
  if (!(asset in files)) throw new Error(`UI image asset is not packaged: ${asset}`);
@@ -1376,7 +1438,7 @@ function validateLegacyUiForm(form, executable, extensionId) {
1376
1438
  }
1377
1439
  }
1378
1440
 
1379
- function validateUiRuntimeV2(config, executable, routes) {
1441
+ function validateUiRuntimeV2(config, executable, routes, capabilitySchemas = new Map()) {
1380
1442
  rejectUnknownFields(config, new Set(contract.ui_runtime.config_fields), "UI Runtime v2");
1381
1443
  if (config.ui_schema !== 2) throw new Error("UI Runtime version must be 2");
1382
1444
  validateUiIcon(config.icon);
@@ -1429,6 +1491,7 @@ function validateUiRuntimeV2(config, executable, routes) {
1429
1491
  validateUiNavigation(config.navigation, executable, routes, sourceIds, stateKeys);
1430
1492
  assertObject(config.view, "UI Runtime v2 view");
1431
1493
  validateUiNode(config.view, 0, { value: 0 }, executable, routes, sourceIds, stateKeys, null);
1494
+ validateUiCapabilityBindings(config, sources, capabilitySchemas);
1432
1495
  }
1433
1496
 
1434
1497
  function validateUiTheme(theme, variant = false) {
@@ -1600,7 +1663,7 @@ function validateUiNode(node, depth, counter, executable, routes, sources, state
1600
1663
  const props = node.props === undefined ? {} : node.props;
1601
1664
  validateUiProps(node.type, props, sources, stateKeys);
1602
1665
  validateUiCondition(node.visible_when, stateKeys, sources);
1603
- if (node.action != null && !["surface", "card", "button", "chip"].includes(node.type)) {
1666
+ if (node.action != null && !["surface", "card", "button", "chip", "form", "field", "select", "switch"].includes(node.type)) {
1604
1667
  throw new Error("UI action requires an interactive component");
1605
1668
  }
1606
1669
  validateUiAction(node.action, executable, routes, sources, stateKeys);
@@ -1633,7 +1696,7 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1633
1696
  "border_radius", "elevation", "opacity", "clip", "enabled", "tooltip", "semantic_label", "exclude_semantics",
1634
1697
  ]);
1635
1698
  if (nodeType === "surface") rejectUnknownFields(props, surfaceProps, "surface UI component props");
1636
- for (const key of ["text", "title", "subtitle", "label", "supporting", "placeholder", "empty_text", "value", "tooltip", "semantic_label"]) {
1699
+ for (const key of ["text", "title", "subtitle", "label", "supporting", "placeholder", "empty_text", "value", "tooltip", "semantic_label", "error_text", "submit_label"]) {
1637
1700
  if (props[key] !== undefined) {
1638
1701
  optionalUiTemplate(props[key], 1200, true);
1639
1702
  validateTemplateReferences(props[key], stateKeys, sources);
@@ -1644,6 +1707,11 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1644
1707
  }
1645
1708
  if (props.icon !== undefined) validateUiIcon(props.icon, true);
1646
1709
  if (props.max_lines !== undefined && (!Number.isInteger(props.max_lines) || props.max_lines < 1 || props.max_lines > 100)) throw new Error("UI numeric property max_lines is invalid");
1710
+ if (props.input_type !== undefined && !["text", "number", "email", "url"].includes(props.input_type)) throw new Error("UI input type is invalid");
1711
+ for (const key of ["min_length", "max_length"]) if (props[key] !== undefined && (!Number.isInteger(props[key]) || props[key] < 0 || props[key] > 2000)) throw new Error(`UI ${key} is invalid`);
1712
+ if ((props.min_length ?? 0) > (props.max_length ?? 2000)) throw new Error("UI text length constraints are invalid");
1713
+ for (const key of ["minimum", "maximum"]) if (props[key] !== undefined) validateUiNumber(props[key], `UI ${key}`, -1000000000, 1000000000);
1714
+ if ((props.minimum ?? -1000000000) > (props.maximum ?? 1000000000)) throw new Error("UI numeric constraints are invalid");
1647
1715
  for (const key of ["columns", "size", "limit"]) {
1648
1716
  if (props[key] !== undefined && (!Number.isInteger(props[key]) || props[key] < 0 || props[key] > 100)) throw new Error(`UI numeric property ${key} is invalid`);
1649
1717
  }
@@ -1668,7 +1736,7 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1668
1736
  for (const key of ["left", "top", "right", "bottom"]) if (props[key] !== undefined) validateUiNumber(props[key], `UI position ${key}`, -4096, 4096);
1669
1737
  if (props.stack_at !== undefined && (!Number.isInteger(props.stack_at) || props.stack_at < 0 || props.stack_at > 1200)) throw new Error("UI stack_at is invalid");
1670
1738
  if (props.min_item_width !== undefined && (!Number.isInteger(props.min_item_width) || props.min_item_width < 96 || props.min_item_width > 600)) throw new Error("UI min_item_width is invalid");
1671
- for (const key of ["persist", "selected", "dense", "show_legend", "full_width", "exclude_semantics", "clip", "shrink_wrap", "enabled"]) if (props[key] !== undefined && typeof props[key] !== "boolean") throw new Error(`UI boolean property ${key} is invalid`);
1739
+ for (const key of ["persist", "selected", "dense", "show_legend", "full_width", "exclude_semantics", "clip", "shrink_wrap", "enabled", "required", "searchable"]) if (props[key] !== undefined && typeof props[key] !== "boolean") throw new Error(`UI boolean property ${key} is invalid`);
1672
1740
  if (props.fit !== undefined && !["contain", "cover", "fill", "fit_width", "fit_height", "none", "scale_down"].includes(props.fit)) throw new Error("UI image fit is invalid");
1673
1741
  if (props.scroll_direction !== undefined && !["vertical", "horizontal"].includes(props.scroll_direction)) throw new Error("UI scroll direction is invalid");
1674
1742
  if (["image", "avatar"].includes(nodeType)) validateUiImageSource(props, sources);
@@ -1688,6 +1756,11 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1688
1756
  if (nodeType === "chart" && !["bar", "line", "donut", "scatter"].includes(chartType)) throw new Error("UI chart type is invalid");
1689
1757
  if (nodeType === "calendar" && props.state_key != null && !stateKeys.has(props.state_key)) throw new Error("UI calendar state is unknown");
1690
1758
  if (["field", "select", "switch"].includes(nodeType) && !stateKeys.has(props.state_key)) throw new Error("UI state field is unknown");
1759
+ if (props.suggestions !== undefined || props.suggestions_source !== undefined) {
1760
+ if (nodeType !== "field" || (props.suggestions !== undefined && props.suggestions_source !== undefined)) throw new Error("UI autocomplete options are invalid");
1761
+ if (props.suggestions !== undefined && (!Array.isArray(props.suggestions) || props.suggestions.length < 1 || props.suggestions.length > 32 || props.suggestions.some((value) => typeof value !== "string" || !value.trim() || value.length > 120))) throw new Error("UI autocomplete options are invalid");
1762
+ if (props.suggestions_source !== undefined) validateUiDataBinding(props.suggestions_source, sources, "autocomplete");
1763
+ }
1691
1764
  if (props.options != null) {
1692
1765
  if (nodeType !== "select" || !Array.isArray(props.options) || props.options.length < 1 || props.options.length > 32) throw new Error("UI select options are invalid");
1693
1766
  for (const option of props.options) {
@@ -1696,6 +1769,18 @@ function validateUiProps(nodeType, props, sources, stateKeys) {
1696
1769
  optionalUiText(option.label, 80, { required: true });
1697
1770
  }
1698
1771
  }
1772
+ if (props.options !== undefined && props.options_source !== undefined) throw new Error("UI select cannot declare two options sources");
1773
+ if (props.options_source !== undefined) {
1774
+ if (nodeType !== "select") throw new Error("UI options source requires a select");
1775
+ validateUiDataBinding(props.options_source, sources, "select options");
1776
+ }
1777
+ if (props.searchable !== undefined && nodeType !== "select") throw new Error("UI searchable requires a select");
1778
+ for (const key of ["required", "min_length", "max_length", "minimum", "maximum", "error_text"]) if (props[key] !== undefined && !["field", "select", "switch"].includes(nodeType)) throw new Error("UI validation properties require an input");
1779
+ if (props.submit_label !== undefined && nodeType !== "form") throw new Error("UI submit label requires a form");
1780
+ }
1781
+
1782
+ function validateUiDataBinding(value, sources, label) {
1783
+ if (typeof value !== "string" || !UI_BINDING.test(value) || !value.startsWith("data.") || !sources.has(value.split(".")[1])) throw new Error(`UI ${label} source is invalid`);
1699
1784
  }
1700
1785
 
1701
1786
  function validateUiImageSource(props, sources) {
@@ -1737,8 +1822,21 @@ function validateUiInsets(value, label) {
1737
1822
  for (const item of Object.values(value)) validateUiNumber(item, label, 0, 128);
1738
1823
  }
1739
1824
 
1740
- function validateUiCondition(condition, stateKeys, sources) {
1825
+ function validateUiCondition(condition, stateKeys, sources, depth = 0, counter = { value: 0 }) {
1741
1826
  if (condition == null) return;
1827
+ assertObject(condition, "UI visibility condition");
1828
+ counter.value += 1;
1829
+ if (depth > 4 || counter.value > 32) throw new Error("UI visibility condition is too complex");
1830
+ const composites = ["all", "any", "not"].filter((key) => Object.hasOwn(condition, key));
1831
+ if (composites.length) {
1832
+ if (composites.length !== 1 || Object.keys(condition).length !== 1) throw new Error("UI visibility condition is invalid");
1833
+ const key = composites[0];
1834
+ if (key === "not") return validateUiCondition(condition.not, stateKeys, sources, depth + 1, counter);
1835
+ const children = condition[key];
1836
+ if (!Array.isArray(children) || children.length < 1 || children.length > 8) throw new Error("UI visibility condition group is invalid");
1837
+ for (const child of children) validateUiCondition(child, stateKeys, sources, depth + 1, counter);
1838
+ return;
1839
+ }
1742
1840
  rejectUnknownFields(condition, new Set(["path", "equals", "not_equals", "exists"]), "UI visibility condition");
1743
1841
  if (typeof condition.path !== "string" || !UI_BINDING.test(condition.path)) throw new Error("UI visibility path is invalid");
1744
1842
  validateKnownBinding(condition.path, stateKeys, sources);
@@ -1747,13 +1845,23 @@ function validateUiCondition(condition, stateKeys, sources) {
1747
1845
  if (checks[0] !== "exists") validateUiJson(condition[checks[0]], 0);
1748
1846
  }
1749
1847
 
1750
- function validateUiAction(action, executable, routes, sources, stateKeys, required = false) {
1848
+ function validateUiAction(action, executable, routes, sources, stateKeys, required = false, depth = 0, counter = { value: 0 }) {
1751
1849
  if (action == null) {
1752
1850
  if (required) throw new Error("UI action is required");
1753
1851
  return;
1754
1852
  }
1853
+ assertObject(action, "UI action");
1854
+ counter.value += 1;
1855
+ if (depth > 4 || counter.value > 16) throw new Error("UI action flow is too complex");
1755
1856
  rejectUnknownFields(action, new Set(contract.ui_runtime.action_fields), "UI action");
1756
- if (!["invoke", "navigate", "set_state", "refresh", "back"].includes(action.type)) throw new Error("UI action type is invalid");
1857
+ if (!["invoke", "navigate", "set_state", "refresh", "back", "flow"].includes(action.type)) throw new Error("UI action type is invalid");
1858
+ if (action.validate !== undefined && typeof action.validate !== "boolean") throw new Error("UI action validation flag is invalid");
1859
+ if (action.type === "flow") {
1860
+ rejectUnknownFields(action, new Set(["type", "mode", "actions", "continue_on_error", "validate"]), "UI action flow");
1861
+ if (!["sequential", "parallel"].includes(action.mode) || !Array.isArray(action.actions) || action.actions.length < 1 || action.actions.length > 8 || (action.continue_on_error !== undefined && typeof action.continue_on_error !== "boolean")) throw new Error("UI action flow is invalid");
1862
+ for (const child of action.actions) validateUiAction(child, executable, routes, sources, stateKeys, true, depth + 1, counter);
1863
+ return;
1864
+ }
1757
1865
  if (action.type === "invoke") {
1758
1866
  if (!executable.has(action.capability)) throw new Error("UI action targets a non-executable capability");
1759
1867
  optionalUiTemplate(action.query, 2000);
@@ -1768,6 +1876,11 @@ function validateUiAction(action, executable, routes, sources, stateKeys, requir
1768
1876
  if (!["platform", "fade", "slide", "none"].includes(transition)) throw new Error("UI navigation transition is invalid");
1769
1877
  const replace = action.replace === undefined ? false : action.replace;
1770
1878
  if (typeof replace !== "boolean") throw new Error("UI navigation replacement is invalid");
1879
+ const parameters = action.parameters === undefined ? {} : action.parameters;
1880
+ assertObject(parameters, "UI navigation parameters");
1881
+ validateUiBindingValue(parameters, 0, stateKeys, sources);
1882
+ if (action.result_state !== undefined && !stateKeys.has(action.result_state)) throw new Error("UI navigation result state is unknown");
1883
+ if (action.result_state !== undefined && replace) throw new Error("replacement navigation cannot return a result");
1771
1884
  } else if (action.type === "set_state") {
1772
1885
  assertObject(action.values, "UI state action values");
1773
1886
  if (!Object.keys(action.values).length || Object.keys(action.values).some((key) => !stateKeys.has(key))) throw new Error("UI state action targets an unknown state key");
@@ -1776,8 +1889,21 @@ function validateUiAction(action, executable, routes, sources, stateKeys, requir
1776
1889
  if (typeof persist !== "boolean") throw new Error("UI state persistence flag is invalid");
1777
1890
  } else if (action.type === "refresh") {
1778
1891
  if (!sources.has(action.source)) throw new Error("UI refresh source is unknown");
1779
- } else if (Object.keys(action).length !== 1) throw new Error("back UI action cannot contain arguments");
1892
+ } else {
1893
+ rejectUnknownFields(action, new Set(["type", "result"]), "back UI action");
1894
+ const result = action.result === undefined ? {} : action.result;
1895
+ assertObject(result, "UI route result");
1896
+ validateUiBindingValue(result, 0, stateKeys, sources);
1897
+ }
1780
1898
  if (action.type !== "navigate" && (action.transition !== undefined || action.replace !== undefined)) throw new Error("navigation options require a navigate UI action");
1899
+ const allowed = {
1900
+ invoke: new Set(["type", "capability", "query", "arguments", "store", "validate"]),
1901
+ navigate: new Set(["type", "target", "transition", "replace", "parameters", "result_state", "validate"]),
1902
+ set_state: new Set(["type", "values", "persist"]),
1903
+ refresh: new Set(["type", "source"]),
1904
+ back: new Set(["type", "result"]),
1905
+ }[action.type];
1906
+ rejectUnknownFields(action, allowed, "UI action");
1781
1907
  }
1782
1908
 
1783
1909
  function validateUiBindingValue(value, depth, stateKeys, sources) {
@@ -1818,6 +1944,73 @@ function validateUiJson(value, depth) {
1818
1944
  for (const item of Object.values(value)) validateUiJson(item, depth + 1);
1819
1945
  }
1820
1946
 
1947
+ function validateUiCapabilityBindings(config, sources, capabilitySchemas) {
1948
+ const sourceCapabilities = new Map(sources.map((source) => [source.id, source.capability]));
1949
+ for (const source of sources) {
1950
+ const schema = capabilitySchemas.get(source.capability)?.input_schema;
1951
+ if (schema !== undefined && !uiSchemaAccepts(schema, source.arguments === undefined ? {} : source.arguments)) {
1952
+ throw new Error(`UI data source ${source.id} does not match capability input_schema`);
1953
+ }
1954
+ }
1955
+ function visit(value) {
1956
+ if (typeof value === "string") {
1957
+ for (const match of value.matchAll(UI_TEMPLATE_BINDING)) {
1958
+ const parts = match[1].trim().split(".");
1959
+ if (parts.length < 4 || parts[0] !== "data" || parts[2] !== "data") continue;
1960
+ const schema = capabilitySchemas.get(sourceCapabilities.get(parts[1]))?.output_schema;
1961
+ if (schema !== undefined && !uiSchemaHasPath(schema, parts.slice(3))) throw new Error("UI binding references an unknown capability output");
1962
+ }
1963
+ return;
1964
+ }
1965
+ if (Array.isArray(value)) return value.forEach(visit);
1966
+ if (!value || typeof value !== "object") return;
1967
+ if (value.type === "invoke") {
1968
+ const schema = capabilitySchemas.get(value.capability)?.input_schema;
1969
+ if (schema !== undefined && !uiSchemaAccepts(schema, value.arguments === undefined ? {} : value.arguments)) throw new Error("UI action does not match capability input_schema");
1970
+ }
1971
+ Object.values(value).forEach(visit);
1972
+ }
1973
+ visit(config);
1974
+ }
1975
+
1976
+ function uiSchemaAccepts(schema, value) {
1977
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) return false;
1978
+ if (typeof value === "string") {
1979
+ const exact = UI_SINGLE_TEMPLATE_BINDING.exec(value);
1980
+ if (exact) return true;
1981
+ if ([...value.matchAll(UI_TEMPLATE_BINDING)].length) return schema.type === "string";
1982
+ }
1983
+ if (schema.enum !== undefined && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) return false;
1984
+ if (schema.type === "null") return value === null;
1985
+ if (schema.type === "boolean") return typeof value === "boolean";
1986
+ if (schema.type === "integer") return Number.isInteger(value) && (schema.minimum === undefined || value >= schema.minimum) && (schema.maximum === undefined || value <= schema.maximum);
1987
+ if (schema.type === "number") return typeof value === "number" && Number.isFinite(value) && (schema.minimum === undefined || value >= schema.minimum) && (schema.maximum === undefined || value <= schema.maximum);
1988
+ if (schema.type === "string") return typeof value === "string" && value.length >= (schema.minLength ?? 0) && value.length <= (schema.maxLength ?? 2000);
1989
+ if (schema.type === "array") return Array.isArray(value) && value.length >= (schema.minItems ?? 0) && value.length <= (schema.maxItems ?? 64) && value.every((item) => uiSchemaAccepts(schema.items, item));
1990
+ if (schema.type !== "object" || !value || typeof value !== "object" || Array.isArray(value)) return false;
1991
+ const properties = schema.properties || {};
1992
+ if ((schema.required || []).some((key) => !Object.hasOwn(value, key))) return false;
1993
+ if (schema.additionalProperties === false && Object.keys(value).some((key) => !Object.hasOwn(properties, key))) return false;
1994
+ return Object.entries(value).every(([key, item]) => !Object.hasOwn(properties, key) || uiSchemaAccepts(properties[key], item));
1995
+ }
1996
+
1997
+ function uiSchemaHasPath(schema, parts) {
1998
+ let current = schema;
1999
+ for (const part of parts) {
2000
+ if (!current || typeof current !== "object" || Array.isArray(current)) return false;
2001
+ if (current.type === "array") {
2002
+ current = current.items;
2003
+ if (part === "length") return true;
2004
+ if (/^\d+$/.test(part)) continue;
2005
+ }
2006
+ if (!current || current.type !== "object") return false;
2007
+ const properties = current.properties || {};
2008
+ if (!Object.hasOwn(properties, part)) return current.additionalProperties !== false;
2009
+ current = properties[part];
2010
+ }
2011
+ return true;
2012
+ }
2013
+
1821
2014
  function optionalUiTemplate(value, maximum, required = false) {
1822
2015
  if (value === undefined || value === null) {
1823
2016
  if (required) throw new Error("UI text template is required");
@@ -1836,6 +2029,12 @@ function validateKnownBinding(path, stateKeys, sources) {
1836
2029
  const parts = path.split(".");
1837
2030
  if (parts[0] === "state" && (parts.length < 2 || !stateKeys.has(parts[1]))) throw new Error("UI state binding is unknown");
1838
2031
  if (parts[0] === "data" && (parts.length < 2 || !sources.has(parts[1]))) throw new Error("UI data binding is unknown");
2032
+ if (parts[0] === "context") {
2033
+ const roots = new Set(["platform", "screen", "permissions", "connections", "connected", "loading", "errors", "route"]);
2034
+ if (parts.length < 2 || !roots.has(parts[1])) throw new Error("UI runtime context binding is unknown");
2035
+ if (parts[1] === "screen" && (parts.length !== 3 || !["width", "height", "size_class"].includes(parts[2]))) throw new Error("UI screen context binding is unknown");
2036
+ if (["loading", "errors"].includes(parts[1]) && (parts.length !== 3 || !sources.has(parts[2]))) throw new Error("UI data-source context binding is unknown");
2037
+ }
1839
2038
  }
1840
2039
 
1841
2040
  function optionalUiText(value, maximum, { required = false, identifier = false } = {}) {
@@ -1,107 +0,0 @@
1
- # Morit Plugin 개발 문서
2
-
3
- 이 문서는 아이디어를 실제 설치 가능한 `.mplg`로 만드는 순서대로 구성되어 있습니다. 플러그인은
4
- 실행 코드를 앱에 직접 주입하지 않습니다. Manifest와 JSON fragment로 기능·화면·권한을 선언하고,
5
- Morit Host가 서명과 계약을 확인한 뒤 공용 런타임으로 실행합니다.
6
-
7
- ## 가장 짧은 개발 경로
8
-
9
- ```text
10
- 요구사항 정리
11
- → 프로젝트 생성
12
- → manifest와 기능 fragment 작성
13
- → 화면·상태·내비게이션 작성
14
- → 권한·설정·알림 연결
15
- → validate
16
- → preview
17
- → build
18
- → verify
19
- → 실제 앱 설치·실사용 테스트
20
- → deploy
21
- ```
22
-
23
- 공식 CLI를 사용하는 기본 명령은 다음과 같습니다.
24
-
25
- ```bash
26
- npx -y @morit/cli plugin setup . \
27
- --id com.example.study \
28
- --name "Study" \
29
- --publisher example
30
- npx -y @morit/cli plugin validate .
31
- npx -y @morit/cli plugin preview . --output ./dist/preview.html
32
- npx -y @morit/cli plugin build .
33
- npx -y @morit/cli login
34
- npx -y @morit/cli plugin add .
35
- npx -y @morit/cli plugin deploy . --visibility private
36
- ```
37
-
38
- 같은 safe renderer는 공식 CLI, Local/Remote Plugin MCP의 `morit_project_preview`, 저장소 도구
39
- `python tools/morit_plugin.py preview <project>`에서 사용합니다. preview의 라이트·다크 전환으로
40
- 계약과 배치를 확인하고, 실제 Flutter focus·navigation·Tool 흐름은 앱에서도 확인합니다.
41
-
42
- ## 개발 순서별 인덱스
43
-
44
- ### 1. 시작과 개발 흐름
45
-
46
- 1. [시작하기와 개발 흐름](getting-started.md) — 개발 방식 선택, 첫 프로젝트, 완료 조건
47
- 2. [앱에서 플러그인 만들기](app-builder.md) — 단순 Tool을 앱 Builder로 만드는 범위
48
-
49
- ### 2. 프로젝트와 계약
50
-
51
- 3. [프로젝트 구조와 fragment](project-structure.md) — 디렉터리, 병합 규칙, 패키지 포함 파일
52
- 4. [Manifest 레퍼런스](manifest.md) — ID, 버전, capability, connector, dependency
53
- 5. [Instance, Connector, 복합 패키지](instances-and-connectors.md) — 사용자별 실행 단위와 계정 연결
54
-
55
- ### 3. 화면과 사용자 경험
56
-
57
- 6. [화면, 레이아웃, 내비게이션](screens-layout-navigation.md) — 화면 역할과 이동 구조
58
- 7. [기본·커스텀 컴포넌트](components.md) — 각 node의 목적, 속성, 제약
59
- 8. [토큰, 크기, 색, 여백, 반응형](design-tokens-responsive.md) — Material 3 기반 시각 규칙
60
- 9. [화면 분리와 정보 계층](information-hierarchy.md) — 한 화면에 정보를 몰지 않는 설계
61
- 10. [UI extension point](ui-extensions.md) — Host의 어느 위치에 UI를 노출할지 선택
62
- 11. [UI Runtime v2 레퍼런스](ui-runtime-v2.md) — data, state, binding, event 계약
63
- 12. [Response UI와 Agent Timeline](ai-response-and-timeline.md) — AI 메시지 안의 결과 UI
64
-
65
- ### 4. 기능, 데이터, 사용자 제어
66
-
67
- 13. [Tool, Skill, Search, Slash Command](tool-and-skill.md) — capability와 runtime adapter
68
- 14. [권한, 설정, 저장소, 알림](permissions-and-data.md) — 최소 권한과 Host action
69
- 15. [Plugin Local Storage와 AI 접근](plugin-storage.md) — namespace, CRUD, migration, 격리, AI Tool
70
- 16. [외부 서비스 인증과 Cloud Secrets](authentication.md) — OAuth, API key, 비밀 값 경계
71
- 17. [AI Skill과 파일 산출물](ai-skill-and-docx-workflow.md) — 실제 파일을 반환하는 완료 흐름
72
-
73
- ### 5. 플랫폼
74
-
75
- 18. [Android, iOS, Desktop 호환](platform-compatibility.md) — 현재 지원 범위와 이식 원칙
76
-
77
- ### 6. 검증과 배포
78
-
79
- 19. [공식 CLI 개발 흐름](local-cli.md) — setup, sync, preview, build, deploy
80
- 20. [패키징과 테스트](packaging-and-testing.md) — 서명, 무결성, 테스트 층
81
- 21. [오류 해결](troubleshooting.md) — validate, preview, build, 설치, 실행 오류 구분
82
- 22. [예제 검증 방법과 확인 경계](verification.md) — 자동·live·실기기 검증의 차이
83
-
84
- ### 7. SDK, MCP, API
85
-
86
- 23. [CLI와 AI 에이전트 MCP](sdk-and-mcp.md) — Codex·Claude 등 로컬/원격 연결
87
- 24. [원격 Plugin MCP](remote-mcp.md) — Cloud Project를 다루는 Tool 계약
88
- 25. [수명주기와 HTTP API](lifecycle-and-api.md) — Host API와 설치·실행 상태
89
-
90
- ### 8. 실제 예제
91
-
92
- 26. [학교 생활 플러그인](examples-school-life.md) — 공개 NEIS와 AI 할 일을 사용하는 학생용 경험
93
- 27. [학교 생활 개인정보 처리](school-life-privacy.md) — 저장·전송·삭제 범위
94
- 28. [Notion 플러그인](examples-notion.md) — OAuth Connection과 실제 문서 검색
95
-
96
- ## 문서가 배포되는 위치
97
-
98
- `docs/plugin_docs`가 문서 콘텐츠의 공통 원본입니다. 같은 파일이 다음 위치에 복사되거나 빌드 시
99
- 포함됩니다.
100
-
101
- - `@morit/cli`: npm 패키지의 `assets/docs`
102
- - Local MCP `@morit/plugin-mcp`: npm 패키지의 `assets/docs`
103
- - Remote MCP: 서비스 이미지의 `/app/docs/plugin_docs`
104
- - `developers.moring.co`: `docs-index.json`의 순서와 경로로 생성한 MDX
105
-
106
- 네 배포 위치는 같은 계약과 예제를 제공하며, 문서의 JSON은 공식 CLI와 Host 계약으로 지속
107
- 검증합니다. 각 경로에서 별도 규격을 정의하지 않습니다.