@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.
- package/assets/docs/ai-response-and-timeline.md +67 -14
- package/assets/docs/components.md +10 -0
- package/assets/docs/examples-school-life.md +4 -4
- package/assets/docs/manifest.md +4 -0
- package/assets/docs/sdk-and-mcp.md +8 -2
- package/assets/docs/troubleshooting.md +4 -0
- package/assets/docs/ui-extensions.md +7 -2
- package/assets/docs/ui-runtime-v2.md +5 -2
- package/assets/docs/verification.md +6 -1
- package/assets/plugin_contract.json +16 -3
- package/package.json +1 -1
- package/src/workspace.js +68 -0
|
@@ -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
|
-
사용합니다.
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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`를 함께 제공합니다.
|
|
92
|
-
|
|
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
|
-
크기
|
|
36
|
-
SHA-256
|
|
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
|
-
|
|
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의
|
package/assets/docs/manifest.md
CHANGED
|
@@ -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
|
|
12
|
-
없으면 Remote를, 현재 저장소 파일을 직접
|
|
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은 기존 설치 호환용입니다. 새 서비스형
|
|
65
|
-
data source, navigation, theme
|
|
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
|
-
재사용합니다.
|
|
332
|
-
|
|
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` |
|
|
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
|
-
"
|
|
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
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`);
|