@morit/cli 1.2.0 → 1.3.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.
@@ -15,6 +15,23 @@ overflow 메뉴 밑에 떨어뜨리지 않고, 해당 AI 메시지에 속한 하
15
15
  "permissions": [],
16
16
  "config": {
17
17
  "ui_schema": 2,
18
+ "a2ui": {
19
+ "version": "v0.9",
20
+ "description": "수업 정보가 실제 답변에 필요하고 표시할 항목이 있을 때만 사용합니다.",
21
+ "schema": {
22
+ "type": "object",
23
+ "properties": {
24
+ "timetable": {
25
+ "type": "array",
26
+ "items": {"type": "object"},
27
+ "minItems": 1,
28
+ "maxItems": 12
29
+ }
30
+ },
31
+ "required": ["timetable"],
32
+ "additionalProperties": true
33
+ }
34
+ },
18
35
  "theme": {"density": "compact"},
19
36
  "data_sources": [
20
37
  {
@@ -50,21 +67,56 @@ overflow 메뉴 밑에 떨어뜨리지 않고, 해당 AI 메시지에 속한 하
50
67
  ```
51
68
 
52
69
  Response point도 일반 Runtime v2와 같은 node, theme, app bar, navigation, binding, action 계약을
53
- 사용합니다. AI실행한 capability 결과는 `data` namespace에 주입되므로 같은 결과를 얻기 위해
54
- 화면 mount 시 외부 요청을 반복하지 않습니다. 후속 refresh나 사용자가 누른 action에만 추가
55
- capability를 호출합니다.
70
+ 사용합니다. Host선택·검증한 capability 결과는 `data` namespace에 주입되므로 같은 결과를 얻기
71
+ 위해 화면 mount 시 외부 요청을 반복하지 않습니다. 후속 refresh나 사용자가 누른 action에만 추가
72
+ capability를 호출합니다. 기존 `a2ui` 선언이 없는 Response extension은 지원하지 않습니다.
73
+
74
+ ## AI 선택과 Host 검증
75
+
76
+ 서버는 모든 Tool 결과에 UI를 일괄 생성하거나 데이터 key로 컴포넌트를 추측하지 않습니다. 최종
77
+ 답변 모델이 현재 답변에 실제 도움이 되는 후보의 `candidate_id`와 `component`만 선택합니다. 모델은
78
+ 컴포넌트 데이터나 임의 ID를 만들 수 없고, 실제 Tool 결과는 Host가 선택 뒤 주입합니다.
79
+
80
+ 선택 뒤 Host는 다음을 모두 통과한 항목만 A2UI v0.9 DataPart로 저장하고 해당 AI 메시지에 붙입니다.
81
+
82
+ - 실행 상태가 `completed` 또는 `partial`이고 실제 값이 있음
83
+ - 질문·최종 답변과 capability 결과가 관련됨
84
+ - 같은 데이터의 Response UI가 이미 선택되지 않음
85
+ - 기본 컴포넌트별 필수 값과 URL 형식이 유효함
86
+ - Plugin 컴포넌트의 schema, A2UI version, 설치 상태, Instance 권한과 capability 연결이 유효함
87
+
88
+ 빈 데이터, 실패한 Tool, 무관하거나 중복된 결과, 불확실한 선택은 UI를 만들지 않습니다. 선택 모델
89
+ 호출이나 렌더링이 실패해도 일반 텍스트 답변은 그대로 완료됩니다. 진행·실패 상태는 Agent Timeline에
90
+ 표시하며 의미 없는 loading/failed Response 카드를 미리 만들거나 DB에 남기지 않습니다.
91
+
92
+ ## 기본 A2UI Component Catalog
93
+
94
+ | component | 표시 조건 |
95
+ |---|---|
96
+ | `weather` | 현재 기온·상태 또는 예보가 하나 이상 유효함 |
97
+ | `chart` | label과 수치가 있는 행이 2개 이상임; `bar`, `line`, `pie`, `scatter` |
98
+ | `image_gallery` | HTTPS 이미지와 원문 URL·출처명이 함께 있음 |
99
+ | `exchange_rate` | 기준 통화, 상대 통화, 유한한 환율 값이 있음 |
100
+ | `article_list` | 제목·출처·원문이 있는 서로 다른 기사 2개 이상 |
101
+ | `article_card` | 제목·출처·게시 시각·요약·본문 일부·이미지·원문이 모두 있음 |
102
+ | `file_result` | 실제 Morit item ID 또는 검증 가능한 다운로드 URL이 있음 |
103
+ | `image_preview` | 실제 image item 또는 검증 가능한 preview URL이 있음 |
104
+ | `video_preview` | 실제 video item/URL과 thumbnail URL이 함께 있음 |
105
+
106
+ 각 renderer도 같은 필수 값을 다시 확인하고, 값이 바뀌거나 손상되면 해당 컴포넌트만 fallback합니다.
56
107
 
57
- ## 실시간 구조화 응답
108
+ ## Plugin 컴포넌트의 동적 등록
58
109
 
59
- Morit AI 오케스트레이터는 Tool·Skill 실행 전후 같은 `(message, plugin, instance, capability)` 응답을
60
- upsert합니다. `data.__morit_response.state`는 `loading`, `partial`, `completed`, `failed` 하나이며
61
- `retryable`이 true인 실패는 해당 메시지의 재생성으로 복구할 있습니다. 텍스트 delta는 별도로
62
- 계속 스트리밍되므로 UI가 실패해도 텍스트 답변은 유지됩니다.
110
+ 활성 Instance의 `point: "response"` extension은 capability 실행 시
111
+ `plugin.<extension-id>` 이름으로 현재 A2UI catalog에 동적 등록됩니다. AI에게는 Host가 확인한 ID,
112
+ 설명, version, schema만 노출됩니다. `a2ui.schema`는 root `object`인 안전한 JSON Schema 부분집합이며
113
+ `type`, `properties`, `required`, `additionalProperties`, `items`, `enum`, 문자열·숫자·배열 bound를
114
+ 지원합니다. schema는 최대 16 KiB, 깊이 6이며 object/list는 각 64개로 제한됩니다.
63
115
 
64
- Plugin `point: "response"` UI를 선언하면 Runtime v2를 사용합니다. 선언하지 않은 Tool·Skill
65
- 결과도 Host가 같은 검증기를 통해 `metric`, `progress`, `list`, `timeline`, `calendar`, `chart`,
66
- `table`, `empty` 조합으로 안전하게 표시합니다. 차트는 `bar`, `line`, `donut`, `scatter`를 지원합니다.
67
- 없는 shape은 실행하거나 추측하지 않고 summary와 텍스트 fallback을 남깁니다.
116
+ Plugin Response는 Runtime v2의 전체 layout, surface, theme, image, action과 binding을 사용할 수
117
+ 있습니다. 단, extension 권한은 Manifest grant의 부분집합이어야 하고 `data_sources` 중 하나가 실제
118
+ 실행 capability를 가리켜야 합니다. 패키지 설치, CLI validate, Host 로드, Flutter parse와 표시 직전
119
+ 검증이 같은 규칙을 사용합니다.
68
120
 
69
121
  ## 한 container의 정보 순서
70
122
 
@@ -88,8 +140,9 @@ Plugin이 `point: "response"` UI를 선언하면 그 Runtime v2를 사용합니
88
140
  ## Text fallback
89
141
 
90
142
  Response UI를 표시할 수 없더라도 AI의 원래 텍스트 답변은 남아야 합니다. capability는 UI 전용
91
- 원시 JSON만 반환하지 말고 사람이 읽을 수 있는 `summary`를 함께 제공합니다. 없는 node,
92
- 손상된 binding, 이미지 로드 실패는 전체 답변 성공을 숨기지 않습니다.
143
+ 원시 JSON만 반환하지 말고 사람이 읽을 수 있는 `summary`를 함께 제공합니다. 배열만 있거나
144
+ schema가 맞지 않으면 UI를 억지로 채우지 않습니다. 없는 node, 손상된 binding, 이미지 로드
145
+ 실패는 전체 답변 성공을 숨기지 않습니다.
93
146
 
94
147
  ```json
95
148
  {
@@ -189,6 +189,16 @@ route state와 persisted control을 복원합니다. 데이터가 정상적으
189
189
  모든 프리셋은 light/dark, 320px 폭, tablet/desktop 폭, 큰 글자에서 확인합니다. node별 tooltip,
190
190
  semantic label, 최소 탭 영역과 색 이외의 상태 표시는 Host Material 3 규칙을 따릅니다.
191
191
 
192
+ ## A2UI Response catalog와의 관계
193
+
194
+ 위 node는 Plugin Runtime을 구성하는 primitive입니다. AI가 직접 선택하는 Response catalog는
195
+ `weather`, `chart`, `image_gallery`, `exchange_rate`, `article_list`, `article_card`,
196
+ `file_result`, `image_preview`, `video_preview`와 활성 Plugin이 등록한 `plugin.<extension-id>`로
197
+ 구성됩니다. AI는 primitive tree나 실제 결과 값을 생성하지 않고 catalog ID만 선택합니다. Host는
198
+ 기본 컴포넌트 데이터 또는 Plugin의 `a2ui.schema`를 검증한 뒤 Runtime tree에 실제 값을 주입합니다.
199
+ 자세한 등록·fallback 규칙은 [Response UI와 Agent Timeline](ai-response-and-timeline.md)을
200
+ 참고하세요.
201
+
192
202
  ## 공통 크기·정렬·접근성 props
193
203
 
194
204
  - 크기: `width`, `height`, `min_width`, `max_width`, `min_height`, `max_height`
@@ -32,8 +32,8 @@
32
32
 
33
33
  ```text
34
34
  examples/plugins/school-life/dist/school-life-1.5.0.mplg
35
- 크기 263184 bytes
36
- SHA-256 3b6397ca1ef26833fe842087b0ecf5f05621bd45f9d80b8a422b6547c060f603
35
+ 크기 263433 bytes
36
+ SHA-256 0939f8956037638d56f7672ac4f9c4d8f6472d58d23840c1fd5baab198c41e29
37
37
  ```
38
38
 
39
39
  1. Morit 플러그인 관리에서 위 `.mplg`를 선택합니다.
@@ -70,8 +70,8 @@ AI 예:
70
70
  `backend/archive_processing/test_plugin_examples.py`는 서명 package 설치, 권한 전 상태,
71
71
  학교 연결, 단일 날짜와 7일 조회, 알레르기/영양 변환, 검색, rich 알림, 자동 브리핑 중복
72
72
  방지, Storage CRUD/AI 권한, 재시작 복원, actor·Instance 격리, migration, disable/delete purge를
73
- 고정된 공식 response contract로
74
- 검증합니다.
73
+ 검증합니다. day/week Response extension은 A2UI v0.9 schema와 실제 capability data source를
74
+ 검사하고 `plugin.school_life.day.response`, `plugin.school_life.week.response`로 동적 등록됩니다.
75
75
 
76
76
  배포 전에는 [예제 검증 방법](verification.md)에 따라 실제 NEIS 네트워크로 공개 시간표·학사일정·
77
77
  급식 기간 조회도 실행합니다. 이 live 확인은 API와 parser 경계를 검증하지만 release APK의
@@ -142,6 +142,10 @@ timeout은 0.1~30초입니다. capability 권한은 Manifest 권한의 부분집
142
142
  `workspace`, `response` 중 하나입니다. 상세 선택 기준은 [UI extension point](ui-extensions.md),
143
143
  config 계약은 [UI Runtime v2](ui-runtime-v2.md)를 참고하세요.
144
144
 
145
+ `point: "response"`는 `ui_schema: 2`와 A2UI v0.9 metadata/schema가 필수이며 이전 Response UI
146
+ 형식은 허용하지 않습니다. Host가 활성 Instance, 권한, data source와 schema를 확인한 뒤 catalog에
147
+ 동적 등록합니다. AI는 등록된 컴포넌트 ID만 선택할 수 있고 결과 데이터는 Host가 주입합니다.
148
+
145
149
  ## Credential과 Connector
146
150
 
147
151
  Credential은 사용자별 비밀 값의 종류를 선언합니다.
@@ -8,8 +8,9 @@ Morit은 두 MCP 연결을 제공합니다.
8
8
  | Remote `https://morit-api.moring.co/mcp` | Morit Cloud Streamable HTTP | 파일시스템 없는 AI client, 조직 Project·Secret·Deployment |
9
9
 
10
10
  두 연결은 SDK 1.7.8의 `@morit/cli`와 Host Plugin contract를 사용합니다. 계약에는 mode별 Plugin
11
- Theme, Local Storage/AI access, 표준 Storage Tool, UI binding safe preview가 포함됩니다. Local source에 접근할 필요가
12
- 없으면 Remote를, 현재 저장소 파일을 직접 고쳐야 하면 Local을 선택합니다.
11
+ Theme, Local Storage/AI access, 표준 Storage Tool, UI binding, A2UI Response catalog와 safe
12
+ preview가 포함됩니다. Local source에 접근할 필요가 없으면 Remote를, 현재 저장소 파일을 직접
13
+ 고쳐야 하면 Local을 선택합니다.
13
14
 
14
15
  ## 준비
15
16
 
@@ -148,6 +149,11 @@ Storage 기능을 생성할 때는 먼저 `morit_sdk_contract`의 `permissions`,
148
149
  AI가 쓰는 namespace는 `ai_access: read_write`와 `ai_storage` 권한을 모두 선언합니다. MCP는 package
149
150
  runtime 데이터를 읽는 우회 경로가 아니며 개발 source와 artifact만 다룹니다.
150
151
 
152
+ Response extension을 생성할 때는 `ai_response`와 `ui_runtime` 계약을 함께 읽습니다. MCP 에이전트는
153
+ `point: "response"`, Runtime v2, `a2ui.version/description/schema`, 실제 capability data source를 한
154
+ 단위로 작성해야 합니다. AI가 결과 JSON을 만들게 하지 말고 schema에는 표시 가능한 최소 데이터와
155
+ empty 조건을 명시합니다. validate가 schema·권한·version을 거부하면 파일을 우회 패키징하지 않습니다.
156
+
151
157
  ## Remote MCP 작업 순서
152
158
 
153
159
  ```text
@@ -95,6 +95,10 @@ Tool 하나의 실패가 AI 요청 전체 실패를 뜻하지 않는 경우 대
95
95
  계산한 frame 합산 pixel이 16,000,000 이하여야 합니다.
96
96
  - 원격 이미지 실패를 앱 직접 fetch나 임의 외부 URL widget으로 우회하지 않습니다.
97
97
  - Response UI가 원래 AI 메시지 안의 한 container에 붙는지 확인합니다.
98
+ - Response UI가 안 보이면 `config.a2ui.version`이 `v0.9`인지, schema가 실제 완료 데이터와 맞는지,
99
+ response의 data source가 실행 capability를 가리키는지, Instance 권한이 승인됐는지 확인합니다.
100
+ - 빈 데이터·무관한 답변·중복 결과·Tool 실패에서 Response UI가 생기지 않는 것은 정상입니다. AI가
101
+ catalog 항목을 선택하지 않은 경우에도 서버가 임의 fallback UI를 만들지 않고 텍스트만 남깁니다.
98
102
 
99
103
  ## MCP 오류
100
104
 
@@ -32,6 +32,11 @@ UI extension은 Host의 노출 위치와 Runtime config를 연결합니다.
32
32
  일반 Tool/Skill은 자동으로 `workspace`가 되지 않습니다. AI 작업 공간 UI가 필요할 때만 별도
33
33
  `workspace` extension을 선언합니다. Slash Command와 Tool discovery도 별도 계약입니다.
34
34
 
35
+ `response`는 Runtime v2와 `config.a2ui`가 모두 필수입니다. 활성 Instance의 권한과 capability
36
+ data source가 확인되면 Host가 `plugin.<extension-id>` 컴포넌트로 동적 등록하고, AI가 최종 답변에
37
+ 필요하다고 명시적으로 선택한 경우에만 렌더링합니다. 다른 point에 `a2ui`를 선언하거나 기존
38
+ Runtime v1 Response 형식을 사용하는 것은 오류입니다.
39
+
35
40
  ## Home placement
36
41
 
37
42
  Runtime v2의 `card`와 `action`은 고유 section을 만들 수 있습니다.
@@ -61,8 +66,8 @@ enabled, generation, granted permission, timeout을 Host 실행 경계에서 다
61
66
  ## Runtime v1 호환
62
67
 
63
68
  `ui_schema`가 없는 기존 extension은 `sections`, `actions`, `form`, shorthand `capability`/`label`을
64
- 사용하는 Runtime v1으로 읽힙니다. v1은 기존 설치 호환용입니다. 새 서비스형 화면에는 state,
65
- data source, navigation, theme, Response UI를 함께 사용할 수 있는 Runtime v2를 사용합니다.
69
+ 사용하는 Runtime v1으로 읽힙니다. v1은 Response 이외의 기존 설치 호환용입니다. 새 서비스형
70
+ 화면에는 state, data source, navigation, theme를 함께 사용할 수 있는 Runtime v2를 사용합니다.
66
71
 
67
72
  Runtime v1 action style은 `primary` 또는 `secondary`이고, form field는 `text`, `integer`,
68
73
  `select`만 지원합니다. v1 JSON에 v2 필드를 섞으면 오류입니다.
@@ -328,8 +328,11 @@ Host는 source별 실행을 격리하고 중복 실행을 합칩니다. 화면
328
328
  ## Response UI
329
329
 
330
330
  `point: "response"`도 같은 `theme`, `app_bar`, `navigation`, data/state/node/action 계약을
331
- 재사용합니다. 단, 결과는 capability를 실행한 AI 메시지의 response container 안에 렌더링됩니다.
332
- 표현 세부는 [Response UI와 Agent Timeline](ai-response-and-timeline.md)을 참고하세요.
331
+ 재사용합니다. 추가로 `a2ui.version`, 사용 조건을 설명하는 `a2ui.description`, 실제 Tool 결과를
332
+ 검증할 `a2ui.schema`가 필수입니다. 결과는 AI가 catalog 후보를 명시적으로 선택하고 Host의
333
+ 유효성·관련성·중복·권한 검증을 통과했을 때만 해당 AI 메시지의 한 response container 안에
334
+ 렌더링됩니다. 서버는 response를 일괄 생성하거나 shape로 추측하지 않습니다. 상세 계약과 기본
335
+ catalog는 [Response UI와 Agent Timeline](ai-response-and-timeline.md)을 참고하세요.
333
336
 
334
337
  ## Preview와 실제 Host
335
338
 
@@ -10,7 +10,7 @@ SHA-256을 함께 남기면 나중에 같은 결과를 재현할 수 있습니
10
10
 
11
11
  | 패키지 | 크기 | SHA-256 |
12
12
  |---|---:|---|
13
- | `school-life-1.5.0.mplg` | 263184 bytes | `3b6397ca1ef26833fe842087b0ecf5f05621bd45f9d80b8a422b6547c060f603` |
13
+ | `school-life-1.5.0.mplg` | 263433 bytes | `0939f8956037638d56f7672ac4f9c4d8f6472d58d23840c1fd5baab198c41e29` |
14
14
  | `notion-1.1.0.mplg` | 3044 bytes | `eba57c440b305343c5cf917259e30e50b322272cfadb0ed64d39ecfc47539c4e` |
15
15
 
16
16
  ```powershell
@@ -108,6 +108,11 @@ Backend live test는 앱 화면과 Android 알림을 누르지 않습니다. rel
108
108
  6. 강제 종료·재실행 뒤 설치·연결·화면 상태 복원
109
109
  7. 비활성화·삭제 뒤 화면·capability·알림 제거
110
110
 
111
+ Response UI는 정상 데이터에서 필요한 컴포넌트 하나만 선택되는지뿐 아니라 빈 데이터, 관련 없는
112
+ 질문, 같은 결과의 중복, schema 불일치, 권한 미승인, Tool 실패에서 컴포넌트가 생성되지 않고 일반
113
+ 텍스트 답변이 유지되는지도 확인합니다. Plugin Response는 `plugin.<extension-id>` 등록 ID와 실제
114
+ 실행 capability의 data source가 일치해야 합니다.
115
+
111
116
  실기기에서 확인하지 않은 항목은 자동 테스트가 통과했더라도 `미검증`으로 남깁니다.
112
117
 
113
118
  ## 5. 결과 기록 형식
@@ -112,10 +112,22 @@
112
112
  "permission_revalidation": true
113
113
  },
114
114
  "ai_response": {
115
+ "a2ui_version": "v0.9",
116
+ "catalog_id": "https://moring.co/a2ui/catalogs/morit-ai/v1",
117
+ "selection": "ai_explicit_host_validated",
118
+ "selection_limit": 4,
119
+ "model_selection_fields": ["candidate_id", "component", "chart_type"],
120
+ "host_injects_tool_data": true,
121
+ "fallback": "text_only",
122
+ "built_in_components": ["weather", "chart", "image_gallery", "exchange_rate", "article_list", "article_card", "file_result", "image_preview", "video_preview"],
123
+ "validation_gates": ["completed_or_partial", "non_empty", "component_data", "answer_relevance", "not_duplicate", "plugin_schema", "plugin_permission", "plugin_version"],
124
+ "plugin_component_prefix": "plugin.",
125
+ "plugin_metadata_fields": ["version", "description", "schema"],
126
+ "plugin_schema_types": ["object", "array", "string", "number", "integer", "boolean", "null"],
127
+ "plugin_schema_max_bytes": 16384,
128
+ "plugin_schema_max_depth": 6,
115
129
  "state_field": "data.__morit_response.state",
116
- "states": ["loading", "partial", "completed", "failed"],
117
- "retryable_field": "data.__morit_response.retryable",
118
- "fallback_nodes": ["metric", "progress", "list", "timeline", "calendar", "chart", "table", "empty"],
130
+ "emitted_states": ["partial", "completed"],
119
131
  "image_artifact_mime_types": ["image/png", "image/jpeg", "image/gif", "image/webp"],
120
132
  "max_inline_image_bytes": 25165824,
121
133
  "max_inline_image_dimension": 4096,
@@ -192,6 +204,7 @@
192
204
  "theme",
193
205
  "initial_state",
194
206
  "data_sources",
207
+ "a2ui",
195
208
  "view"
196
209
  ],
197
210
  "node_fields": ["id", "type", "props", "children", "action", "visible_when"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morit/cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Official Morit Developer CLI for Cloud Projects, validation, builds, and deployments",
5
5
  "type": "module",
6
6
  "bin": {
package/src/workspace.js CHANGED
@@ -1243,9 +1243,11 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
1243
1243
  const config = extension.config === undefined ? {} : extension.config;
1244
1244
  const executable = new Set(capabilities.filter((value) => ["tool", "skill", "provider", "notification"].includes(value.kind)).map((value) => value.id));
1245
1245
  if (hasStorage) for (const capability of STORAGE_TOOLS) executable.add(capability);
1246
+ if (extension.point === "response" && config.ui_schema !== 2) throw new Error("response UI requires UI Runtime v2");
1246
1247
  if (config.ui_schema === 2) {
1247
1248
  if (config.placement != null && !["card", "action"].includes(extension.point)) throw new Error("UI placement is only valid for home extensions");
1248
1249
  validateUiRuntimeV2(config, executable, routeIds);
1250
+ validateResponseComponent(config, extension.point === "response");
1249
1251
  for (const asset of uiRuntimeAssetPaths(config.view)) {
1250
1252
  if (!(asset in files)) throw new Error(`UI image asset is not packaged: ${asset}`);
1251
1253
  }
@@ -1279,6 +1281,72 @@ function validateUiConfig(extension, capabilities, routeIds, files, hasStorage =
1279
1281
  if (config.form !== undefined) validateLegacyUiForm(config.form, executable, extension.id);
1280
1282
  }
1281
1283
 
1284
+ function validateResponseComponent(config, responsePoint) {
1285
+ if (!responsePoint) {
1286
+ if (Object.hasOwn(config, "a2ui")) throw new Error("A2UI metadata is only valid for response UI");
1287
+ return;
1288
+ }
1289
+ const value = config.a2ui;
1290
+ assertObject(value, "response A2UI metadata");
1291
+ rejectUnknownFields(value, new Set(["version", "description", "schema"]), "response A2UI metadata");
1292
+ if (Object.keys(value).length !== 3 || value.version !== "v0.9") throw new Error("response UI requires complete A2UI v0.9 metadata");
1293
+ requireText(value.description, 500, "response A2UI description");
1294
+ assertObject(value.schema, "response A2UI schema");
1295
+ if (Buffer.byteLength(JSON.stringify(value.schema), "utf8") > 16 * 1024) throw new Error("response A2UI schema is too large");
1296
+ validateResponseSchema(value.schema, 0, true);
1297
+ }
1298
+
1299
+ function validateResponseSchema(schema, depth, root = false) {
1300
+ const fields = new Set([
1301
+ "type", "properties", "required", "additionalProperties", "items", "enum",
1302
+ "minItems", "maxItems", "minLength", "maxLength", "minimum", "maximum",
1303
+ ]);
1304
+ if (depth > 6) throw new Error("response A2UI schema is too deep");
1305
+ rejectUnknownFields(schema, fields, "response A2UI schema");
1306
+ const types = new Set(["object", "array", "string", "number", "integer", "boolean", "null"]);
1307
+ if (!types.has(schema.type) || (root && schema.type !== "object")) throw new Error("response A2UI schema type is invalid");
1308
+ if (schema.enum !== undefined) {
1309
+ if (!Array.isArray(schema.enum) || schema.enum.length < 1 || schema.enum.length > 32 || schema.enum.some((item) => item !== null && typeof item === "object")) throw new Error("response A2UI schema enum is invalid");
1310
+ for (const item of schema.enum) validateUiJson(item, 0);
1311
+ }
1312
+ const byType = {
1313
+ object: new Set(["properties", "required", "additionalProperties"]),
1314
+ array: new Set(["items", "minItems", "maxItems"]),
1315
+ string: new Set(["minLength", "maxLength"]),
1316
+ number: new Set(["minimum", "maximum"]),
1317
+ integer: new Set(["minimum", "maximum"]),
1318
+ boolean: new Set(),
1319
+ null: new Set(),
1320
+ }[schema.type];
1321
+ if (Object.keys(schema).some((key) => !["type", "enum"].includes(key) && !byType.has(key))) throw new Error("response A2UI schema keyword does not match its type");
1322
+ if (schema.type === "object") {
1323
+ const properties = schema.properties === undefined ? {} : schema.properties;
1324
+ const required = schema.required === undefined ? [] : schema.required;
1325
+ const additional = schema.additionalProperties === undefined ? true : schema.additionalProperties;
1326
+ assertObject(properties, "response A2UI properties");
1327
+ if (Object.keys(properties).length > 64 || !Array.isArray(required) || required.length > 64 || new Set(required).size !== required.length || typeof additional !== "boolean") throw new Error("response A2UI object schema is invalid");
1328
+ for (const [key, child] of Object.entries(properties)) {
1329
+ if (!UI_STATE_KEY.test(key)) throw new Error("response A2UI property is invalid");
1330
+ assertObject(child, "response A2UI property schema");
1331
+ validateResponseSchema(child, depth + 1);
1332
+ }
1333
+ if (required.some((key) => typeof key !== "string" || !Object.hasOwn(properties, key))) throw new Error("response A2UI required property is invalid");
1334
+ } else if (schema.type === "array") {
1335
+ assertObject(schema.items, "response A2UI item schema");
1336
+ const minimum = schema.minItems === undefined ? 0 : schema.minItems;
1337
+ const maximum = schema.maxItems === undefined ? 64 : schema.maxItems;
1338
+ if (!Number.isInteger(minimum) || !Number.isInteger(maximum) || minimum < 0 || minimum > maximum || maximum > 64) throw new Error("response A2UI array schema is invalid");
1339
+ validateResponseSchema(schema.items, depth + 1);
1340
+ } else if (schema.type === "string") {
1341
+ const minimum = schema.minLength === undefined ? 0 : schema.minLength;
1342
+ const maximum = schema.maxLength === undefined ? 2000 : schema.maxLength;
1343
+ if (!Number.isInteger(minimum) || !Number.isInteger(maximum) || minimum < 0 || minimum > maximum || maximum > 2000) throw new Error("response A2UI string schema is invalid");
1344
+ } else if (["number", "integer"].includes(schema.type)) {
1345
+ for (const bound of [schema.minimum, schema.maximum]) if (bound !== undefined && (typeof bound !== "number" || !Number.isFinite(bound))) throw new Error("response A2UI number bound is invalid");
1346
+ if (schema.minimum !== undefined && schema.maximum !== undefined && schema.minimum > schema.maximum) throw new Error("response A2UI number range is invalid");
1347
+ }
1348
+ }
1349
+
1282
1350
  function validateLegacyUiForm(form, executable, extensionId) {
1283
1351
  rejectUnknownFields(form, new Set(["submit_capability", "submit_label", "fields"]), `UI ${extensionId} form`);
1284
1352
  if (!executable.has(form.submit_capability)) throw new Error(`UI ${extensionId} form targets an unknown capability`);