@morit/cli 1.0.0 → 1.1.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/README.md +53 -18
- package/assets/docs/README.md +105 -0
- package/assets/docs/ai-response-and-timeline.md +125 -0
- package/assets/docs/ai-skill-and-docx-workflow.md +83 -0
- package/assets/docs/app-builder.md +56 -0
- package/assets/docs/authentication.md +140 -0
- package/assets/docs/components.md +159 -0
- package/assets/docs/design-tokens-responsive.md +148 -0
- package/assets/docs/docs-index.json +93 -0
- package/assets/docs/examples-notion.md +83 -0
- package/assets/docs/examples-school-life.md +74 -0
- package/assets/docs/getting-started.md +132 -0
- package/assets/docs/information-hierarchy.md +81 -0
- package/assets/docs/instances-and-connectors.md +93 -0
- package/assets/docs/lifecycle-and-api.md +158 -0
- package/assets/docs/local-cli.md +124 -0
- package/assets/docs/manifest.md +208 -0
- package/assets/docs/packaging-and-testing.md +115 -0
- package/assets/docs/permissions-and-data.md +131 -0
- package/assets/docs/platform-compatibility.md +62 -0
- package/assets/docs/project-structure.md +102 -0
- package/assets/docs/remote-mcp.md +152 -0
- package/assets/docs/school-life-privacy.md +49 -0
- package/assets/docs/screens-layout-navigation.md +95 -0
- package/assets/docs/sdk-and-mcp.md +182 -0
- package/assets/docs/tool-and-skill.md +163 -0
- package/assets/docs/troubleshooting.md +117 -0
- package/assets/docs/ui-extensions.md +70 -0
- package/assets/docs/ui-runtime-v2.md +273 -0
- package/assets/docs/verification.md +128 -0
- package/assets/plugin_contract.json +222 -1
- package/package.json +1 -1
- package/src/cli.js +27 -3
- package/src/secure-store.js +113 -43
- package/src/workspace.js +297 -86
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Manifest 레퍼런스
|
|
2
|
+
|
|
3
|
+
`manifest.json`은 UTF-8 JSON 객체입니다. Host는 schema 1과 2를 읽으며 새 프로젝트는 schema 2를
|
|
4
|
+
사용합니다. 알 수 없는 필드는 무시하지 않고 오류로 처리합니다.
|
|
5
|
+
|
|
6
|
+
## 전체 형태
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"schema_version": 2,
|
|
11
|
+
"id": "com.example.notes",
|
|
12
|
+
"name": "Notes",
|
|
13
|
+
"icon": "assets/icon.png",
|
|
14
|
+
"short_description": "연결된 노트를 검색합니다.",
|
|
15
|
+
"description": "내 노트 계정을 연결해 검색하고 요약합니다.",
|
|
16
|
+
"category": "productivity",
|
|
17
|
+
"keywords": ["notes", "search"],
|
|
18
|
+
"developer": {"name": "Example", "url": "https://example.com"},
|
|
19
|
+
"homepage_url": "https://example.com/notes",
|
|
20
|
+
"privacy_policy_url": "https://example.com/privacy",
|
|
21
|
+
"publisher": "example",
|
|
22
|
+
"version": "1.0.0",
|
|
23
|
+
"min_morit_version": "1.7.6",
|
|
24
|
+
"max_morit_version": "1.999.999",
|
|
25
|
+
"permissions": ["network", "credentials"],
|
|
26
|
+
"capabilities": [
|
|
27
|
+
{
|
|
28
|
+
"id": "com.example.notes.echo",
|
|
29
|
+
"kind": "tool",
|
|
30
|
+
"title": "입력 확인",
|
|
31
|
+
"description": "입력한 검색어를 확인합니다.",
|
|
32
|
+
"permissions": [],
|
|
33
|
+
"runtime": {"adapter": "text_template", "template": "검색어: {query}"}
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"ui_extensions": [],
|
|
37
|
+
"credentials": [],
|
|
38
|
+
"slash_commands": [],
|
|
39
|
+
"connectors": [],
|
|
40
|
+
"dependencies": [],
|
|
41
|
+
"required_secrets": [],
|
|
42
|
+
"data_policy": "purge"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`icon`을 선언했다면 `assets/icon.png` 파일도 실제 프로젝트에 있어야 합니다. 준비 전에는 필드를
|
|
47
|
+
생략하고 존재하지 않는 asset 경로를 넣지 않습니다.
|
|
48
|
+
|
|
49
|
+
## 신원과 표시 정보
|
|
50
|
+
|
|
51
|
+
| 필드 | 규칙 |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `id` | 소문자 reverse-domain ID, 예: `com.example.notes` |
|
|
54
|
+
| `name` | 1~80자 |
|
|
55
|
+
| `publisher` | 2~120자의 영문·숫자·점·밑줄·하이픈 |
|
|
56
|
+
| `version` | semantic version |
|
|
57
|
+
| `min_morit_version`, `max_morit_version` | semantic version, 둘 다 필수 |
|
|
58
|
+
| `icon` | 패키지의 PNG/JPEG/WebP `assets/` 경로 |
|
|
59
|
+
| `short_description` | 최대 160자 |
|
|
60
|
+
| `description` | 최대 4,000자 |
|
|
61
|
+
| `category` | 소문자로 시작하는 2~40자 ID |
|
|
62
|
+
| `keywords` | 최대 12개의 고유한 1~40자 문자열 |
|
|
63
|
+
| URL 필드 | 사용자 정보가 없는 공개 HTTPS URL |
|
|
64
|
+
|
|
65
|
+
`cloud_project_id`는 Cloud 기능을 쓸 때 Developer Project가 채우는 UUID입니다. 프로젝트 source에
|
|
66
|
+
직접 추측해 넣지 않습니다. `required_secrets` 또는 `cloud_connection_id`를 사용하는 Connector가
|
|
67
|
+
있으면 이 값이 필요합니다.
|
|
68
|
+
|
|
69
|
+
## 권한과 데이터 정책
|
|
70
|
+
|
|
71
|
+
Manifest `permissions`는 플러그인이 요청할 수 있는 최대 범위입니다. 각 capability와 UI extension은
|
|
72
|
+
그중 자신에게 필요한 일부만 다시 선언합니다.
|
|
73
|
+
|
|
74
|
+
`data_policy`는 삭제 시 플러그인 storage 처리 방식을 정합니다.
|
|
75
|
+
|
|
76
|
+
- `purge`: 설치와 함께 저장한 플러그인 데이터를 삭제
|
|
77
|
+
- `retain`: 재설치를 위해 일반 storage를 유지
|
|
78
|
+
|
|
79
|
+
credential, OAuth state, pending notification은 두 정책과 무관하게 보안 수명주기에 따라 정리됩니다.
|
|
80
|
+
|
|
81
|
+
## Capability
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"id": "com.example.notes.search",
|
|
86
|
+
"kind": "tool",
|
|
87
|
+
"title": "노트 검색",
|
|
88
|
+
"description": "연결한 계정의 노트를 검색합니다.",
|
|
89
|
+
"permissions": ["network", "credentials"],
|
|
90
|
+
"timeout_seconds": 10,
|
|
91
|
+
"runtime": {
|
|
92
|
+
"adapter": "http_json",
|
|
93
|
+
"connector_id": "notes_api"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`kind`는 `tool`, `skill`, `provider`, `background`, `notification` 중 하나입니다. 최대 64개이며
|
|
99
|
+
timeout은 0.1~30초입니다. capability 권한은 Manifest 권한의 부분집합이어야 합니다. `tool`과
|
|
100
|
+
`skill`은 실행 가능한 runtime adapter가 필수입니다. Search `provider`는
|
|
101
|
+
`runtime.role: "search"`와 adapter가 필요합니다.
|
|
102
|
+
|
|
103
|
+
## UI extension
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"id": "com.example.notes.screen",
|
|
108
|
+
"point": "screen",
|
|
109
|
+
"title": "내 노트",
|
|
110
|
+
"order": 10,
|
|
111
|
+
"permissions": ["network", "credentials"],
|
|
112
|
+
"config": {
|
|
113
|
+
"ui_schema": 2,
|
|
114
|
+
"view": {"type": "text", "props": {"text": "노트를 검색하세요."}}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
최대 64개입니다. `point`는 `screen`, `surface`, `menu`, `action`, `card`, `settings`,
|
|
120
|
+
`workspace`, `response` 중 하나입니다. 상세 선택 기준은 [UI extension point](ui-extensions.md),
|
|
121
|
+
config 계약은 [UI Runtime v2](ui-runtime-v2.md)를 참고하세요.
|
|
122
|
+
|
|
123
|
+
## Credential과 Connector
|
|
124
|
+
|
|
125
|
+
Credential은 사용자별 비밀 값의 종류를 선언합니다.
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"id": "notes_account",
|
|
130
|
+
"label": "Notes 계정",
|
|
131
|
+
"description": "노트를 읽는 OAuth 연결",
|
|
132
|
+
"kind": "oauth_access_token",
|
|
133
|
+
"required": true,
|
|
134
|
+
"oauth_provider": "notion",
|
|
135
|
+
"allow_multiple": true
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`kind`는 `api_token` 또는 `oauth_access_token`입니다. credential이 하나라도 있으면 Manifest에
|
|
140
|
+
`credentials` 권한이 필요합니다. `allow_multiple`의 기본값은 `true`이며 Instance마다 연결을
|
|
141
|
+
따로 관리합니다.
|
|
142
|
+
|
|
143
|
+
Connector는 endpoint, credential, timeout, retry, rate limit을 capability에서 분리합니다.
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"id": "notes_api",
|
|
148
|
+
"kind": "oauth",
|
|
149
|
+
"label": "Notes API",
|
|
150
|
+
"description": "공식 API 연결",
|
|
151
|
+
"credential_id": "notes_account",
|
|
152
|
+
"cloud_connection_id": "notion",
|
|
153
|
+
"timeout_seconds": 10,
|
|
154
|
+
"retry": {"max_attempts": 3},
|
|
155
|
+
"rate_limit": {"requests": 60, "period_seconds": 60}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`kind`는 `https`, `oauth`, `api_key`, `mcp`입니다. 공개 endpoint를 직접 지정할 때는 HTTPS만
|
|
160
|
+
허용합니다. timeout은 0.1~30초, retry는 1~4회, rate-limit 기간은 1~3,600초입니다. OAuth는
|
|
161
|
+
credential이 필수이고 API key는 사용자 credential이나 선언된 Cloud secret 중 하나가 필요합니다.
|
|
162
|
+
|
|
163
|
+
Cloud Secret을 쓰려면 먼저 `required_secrets`에 대문자 환경 ID를 선언합니다.
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"id": "EXAMPLE_API_KEY",
|
|
168
|
+
"label": "Example API key",
|
|
169
|
+
"description": "서버 간 요청에 사용합니다.",
|
|
170
|
+
"required": true
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Dependency와 child package
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"id": "notes",
|
|
179
|
+
"plugin_id": "com.example.notes_core",
|
|
180
|
+
"required": true,
|
|
181
|
+
"min_version": "1.0.0",
|
|
182
|
+
"max_version": "1.999.999",
|
|
183
|
+
"package_path": "children/notes-core.mplg",
|
|
184
|
+
"exposed_capabilities": ["com.example.notes_core.search"]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`package_path`가 있으면 정확히 대응하는 signed child `.mplg`가 패키지에 있어야 합니다. 부모는
|
|
189
|
+
`child_plugin` adapter와 `exposed_capabilities`에 적힌 capability만 호출할 수 있습니다. 부모와
|
|
190
|
+
자식은 Instance, settings, credential, storage를 공유하지 않습니다.
|
|
191
|
+
|
|
192
|
+
## Slash Command
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"id": "com.example.notes.command",
|
|
197
|
+
"command": "notes",
|
|
198
|
+
"title": "노트 검색",
|
|
199
|
+
"description": "연결한 노트를 검색합니다.",
|
|
200
|
+
"capability": "com.example.notes.search",
|
|
201
|
+
"argument_template": {"limit": 10}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
최대 32개입니다. `command`는 `/` 없이 소문자·숫자로 시작하는 1~32자이며 `_`, `-`를 사용할 수
|
|
206
|
+
있습니다. 대상은 같은 플러그인의 `tool` 또는 `skill`이어야 합니다.
|
|
207
|
+
|
|
208
|
+
다음은 [Instance와 Connector 모델](instances-and-connectors.md)입니다.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# 패키징과 테스트
|
|
2
|
+
|
|
3
|
+
## `.mplg` 형식
|
|
4
|
+
|
|
5
|
+
`.mplg`는 content type `application/vnd.morit.plugin+zip`인 deterministic ZIP입니다. 최대 2 MiB,
|
|
6
|
+
entry 최대 64개이며 다음 안전 검사를 통과해야 합니다.
|
|
7
|
+
|
|
8
|
+
- 중복 entry 없음
|
|
9
|
+
- 절대 경로, `..`, 역슬래시, symlink 없음
|
|
10
|
+
- 허용된 source·asset·child package만 포함
|
|
11
|
+
- compiled manifest가 package 파일과 일치
|
|
12
|
+
- 모든 entry digest를 포함한 embedded Ed25519 signature 유효
|
|
13
|
+
|
|
14
|
+
정상 signature 검증을 제거하거나 ZIP 검사 오류를 숨겨 설치하지 않습니다.
|
|
15
|
+
|
|
16
|
+
## 기본 자동 서명
|
|
17
|
+
|
|
18
|
+
공식 Node CLI build는 publisher별 key를 안전한 로컬 디렉터리에 만들고 자동 서명한 뒤 결과를 다시
|
|
19
|
+
검증합니다.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y @morit/cli plugin build . --output ./dist/plugin.mplg
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`.mplg`에는 public key와 signature만 들어갑니다. trusted publisher 등록은 선택 사항이지만 등록된
|
|
26
|
+
publisher key가 있으면 embedded key와 일치해야 합니다.
|
|
27
|
+
|
|
28
|
+
저장소 Python pipeline에서 조직의 고정 key를 사용할 때는 다음과 같습니다.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python tools/morit_plugin.py package . \
|
|
32
|
+
--private-key /secure/example.ed25519 \
|
|
33
|
+
--output ./dist/plugin.mplg
|
|
34
|
+
python tools/morit_plugin.py verify ./dist/plugin.mplg
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`build --unsigned`는 별도 signing pipeline의 중간 파일을 만들 때만 사용합니다. unsigned package는
|
|
38
|
+
설치할 수 없으며 사용자에게 최종 `.mplg`로 제공하지 않습니다.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python tools/morit_plugin.py build . --unsigned --output ./dist/unsigned.mplg
|
|
42
|
+
python tools/morit_plugin.py sign ./dist/unsigned.mplg \
|
|
43
|
+
--private-key /secure/example.ed25519 \
|
|
44
|
+
--output ./dist/plugin.mplg
|
|
45
|
+
python tools/morit_plugin.py verify ./dist/plugin.mplg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Source ZIP과 package
|
|
49
|
+
|
|
50
|
+
Source ZIP은 편집 가능한 전체 프로젝트이고 `.mplg`는 설치에 필요한 compiled subset입니다.
|
|
51
|
+
|
|
52
|
+
| 산출물 | 용도 | 비밀 값 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `*-source.zip` | 리뷰·다른 환경에서 편집 | 포함 금지 |
|
|
55
|
+
| `*.mplg` | 앱 설치·Deployment | private key/secret 포함 금지 |
|
|
56
|
+
| `*-preview.html` | 구조 보조 점검 | 실행 코드 포함 금지 |
|
|
57
|
+
|
|
58
|
+
서로 다른 artifact를 같은 확장자나 MIME으로 제공하지 않습니다.
|
|
59
|
+
|
|
60
|
+
## 테스트 층
|
|
61
|
+
|
|
62
|
+
### 1. Source 계약
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx -y @morit/cli plugin validate .
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Manifest 필드, fragment 병합, 권한 부분집합, capability/runtime, UI tree·binding·route, Credential·
|
|
69
|
+
Connector, source entrypoint, dependency를 검사합니다.
|
|
70
|
+
|
|
71
|
+
### 2. Preview
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python tools/morit_plugin.py preview . --output ./dist/preview.html
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
정보 구조와 compiled manifest를 점검합니다. 실제 Flutter renderer 성공을 대신하지 않습니다.
|
|
78
|
+
|
|
79
|
+
### 3. Package reopen
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx -y @morit/cli plugin build . --output ./dist/plugin.mplg
|
|
83
|
+
python tools/morit_plugin.py verify ./dist/plugin.mplg
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
크기, SHA-256, entry, manifest, publisher, embedded signature를 확인합니다.
|
|
87
|
+
|
|
88
|
+
### 4. Backend/API
|
|
89
|
+
|
|
90
|
+
테스트 계정으로 설치 → 목록 → 활성화 → permission/settings → Connection → capability → 삭제 순서를
|
|
91
|
+
실행합니다. 위험한 ZIP, 위조 signature, 등록된 publisher key 불일치, 잘못된 manifest를 각각 차단하고
|
|
92
|
+
실패 뒤 부분 DB row와 package file이 남지 않는지 확인합니다.
|
|
93
|
+
|
|
94
|
+
### 5. 앱 실사용
|
|
95
|
+
|
|
96
|
+
- 앱 데이터 삭제 후 첫 설치
|
|
97
|
+
- Marketplace 조회·새로고침·재시도
|
|
98
|
+
- local file 설치
|
|
99
|
+
- 활성화와 권한 허용·거부
|
|
100
|
+
- 화면 loading·empty·error·retry
|
|
101
|
+
- 동일 package 재설치와 version update
|
|
102
|
+
- OAuth 여러 Connection과 재연결
|
|
103
|
+
- notification 실제 표시
|
|
104
|
+
- 삭제와 재설치
|
|
105
|
+
|
|
106
|
+
### 6. 회귀 검사
|
|
107
|
+
|
|
108
|
+
SDK, Python Host, Local MCP, Remote MCP가 같은 contract를 반환하고 같은 fixture를 허용·거부하는지
|
|
109
|
+
확인합니다. 문서 예제 JSON도 source 검증 과정에서 실행해 구현과 함께 변경합니다.
|
|
110
|
+
|
|
111
|
+
## 실패 시 원칙
|
|
112
|
+
|
|
113
|
+
validate, build, upload, DB install 단계 중 하나가 실패하면 성공 메시지나 artifact 링크를 반환하지
|
|
114
|
+
않습니다. 설치는 임시 package 검증 후 DB와 파일을 commit하고, 중간 실패 시 둘 다 정리합니다.
|
|
115
|
+
구체 오류 분류는 [오류 해결](troubleshooting.md)을 참고하세요.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# 권한, 설정, 저장소, 알림
|
|
2
|
+
|
|
3
|
+
## 권한 목록
|
|
4
|
+
|
|
5
|
+
| permission | 허용하는 작업 |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `network` | Host가 보호하는 외부 HTTPS/MCP 요청 |
|
|
8
|
+
| `user_data_read` | Morit 사용자 데이터 읽기 |
|
|
9
|
+
| `user_data_write` | Morit 사용자 데이터 변경 |
|
|
10
|
+
| `attachments_read` | 사용자가 선택한 첨부 파일 읽기 |
|
|
11
|
+
| `file_write` | 사용자에게 반환할 파일 artifact 작성 |
|
|
12
|
+
| `ai_tool_call` | AI Tool 실행 경계 사용 |
|
|
13
|
+
| `notifications` | 표시·예약·취소 알림 요청 |
|
|
14
|
+
| `background` | Host scheduler에서 자동 실행 |
|
|
15
|
+
| `account_read` | 필요한 계정 기본 정보 읽기 |
|
|
16
|
+
| `storage` | 플러그인 전용 데이터 저장 |
|
|
17
|
+
| `credentials` | Credential/Connection 사용 |
|
|
18
|
+
|
|
19
|
+
Manifest는 요청 가능한 최대 권한, capability와 UI extension은 실제 필요한 부분집합을 선언합니다.
|
|
20
|
+
사용자가 허용하지 않은 권한은 runtime에서 다시 차단됩니다. 화면을 볼 수 있다는 사실과 작업 권한은
|
|
21
|
+
동일하지 않습니다.
|
|
22
|
+
|
|
23
|
+
## Settings
|
|
24
|
+
|
|
25
|
+
Settings는 default Instance에 저장되는 작은 사용자 환경설정입니다. UI Runtime state를 유지하려면
|
|
26
|
+
`initial_state`에 key를 선언하고 해당 control 또는 `set_state`에 `persist: true`를 사용합니다.
|
|
27
|
+
|
|
28
|
+
적합한 값:
|
|
29
|
+
|
|
30
|
+
- 기본 기간, 정렬 방식, 표시 옵션
|
|
31
|
+
- 알림 on/off와 사용자 선택 시간
|
|
32
|
+
- 공개 school code처럼 민감하지 않은 외부 식별자
|
|
33
|
+
|
|
34
|
+
저장하면 안 되는 값:
|
|
35
|
+
|
|
36
|
+
- OAuth access/refresh token, API key
|
|
37
|
+
- 파일 원문이나 큰 목록
|
|
38
|
+
- 다른 플러그인의 설정
|
|
39
|
+
- 서버가 검증하지 않은 실행 코드
|
|
40
|
+
|
|
41
|
+
Settings 전체는 32 KiB 이하입니다. UI state의 client 저장 경계는 더 작을 수 있으므로 화면에 필요한
|
|
42
|
+
최소 key만 유지합니다.
|
|
43
|
+
|
|
44
|
+
## Plugin storage
|
|
45
|
+
|
|
46
|
+
`storage`는 사용자·Installation/default Instance namespace로 격리됩니다. package가 업데이트되어도
|
|
47
|
+
같은 Instance의 정상 데이터는 유지할 수 있지만 generation이 바뀐 오래된 실행은 새 상태를 쓰지
|
|
48
|
+
못합니다.
|
|
49
|
+
|
|
50
|
+
삭제 시 `data_policy`가 일반 storage의 `purge` 또는 `retain`을 정합니다. credential, OAuth state,
|
|
51
|
+
pending Host action은 별도 보안 수명주기를 따르며 일반 storage retention으로 보존하지 않습니다.
|
|
52
|
+
|
|
53
|
+
## Network
|
|
54
|
+
|
|
55
|
+
외부 통신은 `http_json`, `mcp_http`, 지원되는 Host adapter를 통과합니다.
|
|
56
|
+
|
|
57
|
+
- 공개 HTTPS만 허용
|
|
58
|
+
- redirect 비허용
|
|
59
|
+
- DNS/IP와 SSRF 정책 적용
|
|
60
|
+
- Connector timeout 최대 30초
|
|
61
|
+
- retry 최대 4회
|
|
62
|
+
- Instance별 rate limit
|
|
63
|
+
- credential은 Host가 요청 직전에 주입
|
|
64
|
+
|
|
65
|
+
endpoint, Authorization header, secret을 UI state나 capability 결과에 복사하지 않습니다.
|
|
66
|
+
|
|
67
|
+
Capability 결과 URL을 `image` 또는 `avatar`에 표시하는 경우에도 같은 권한 경계를 사용합니다.
|
|
68
|
+
Manifest의 `network` 요청과 활성 Instance의 `network` grant가 모두 필요합니다. Flutter 앱은 외부
|
|
69
|
+
이미지를 직접 요청하지 않고 인증된 Host proxy를 호출하며, Host가 public HTTPS·SSRF·redirect·이미지
|
|
70
|
+
형식·512 KiB·4096 px·16 MP 제한을 적용합니다. 선언과 상세 제한은
|
|
71
|
+
[UI Runtime v2](ui-runtime-v2.md), endpoint는 [수명주기와 HTTP API](lifecycle-and-api.md)를
|
|
72
|
+
참고하세요.
|
|
73
|
+
|
|
74
|
+
## Notification Host action
|
|
75
|
+
|
|
76
|
+
Capability 결과의 `data.host_actions`는 Host가 서버에서 다시 확인하고 durable outbox에 기록합니다.
|
|
77
|
+
알림을 직접 OS API로 호출하지 않습니다.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"type": "schedule_notification",
|
|
82
|
+
"id": 41001,
|
|
83
|
+
"key": "school_life:morning:2026-08-14",
|
|
84
|
+
"title": "내일 학교 일정",
|
|
85
|
+
"body": "1교시는 수학입니다.",
|
|
86
|
+
"at_millis": 1786640400000,
|
|
87
|
+
"category": "reminder",
|
|
88
|
+
"visibility": "private",
|
|
89
|
+
"silent": false
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
지원 type:
|
|
94
|
+
|
|
95
|
+
- `show_notification`: 즉시 표시
|
|
96
|
+
- `schedule_notification`: `at_millis`에 예약
|
|
97
|
+
- `cancel_notification`: 같은 `id`와 `key` 취소
|
|
98
|
+
|
|
99
|
+
공통 한도는 `id` 0~2,147,483,647, `key` 최대 240자, `title` 160자, `body` 500자,
|
|
100
|
+
`big_text` 2,000자입니다. `category`는 `general`, `reminder`, `progress`, `status`,
|
|
101
|
+
`visibility`는 `private`, `public`, `secret`입니다. 예약은 현재부터 최대 366일 안입니다.
|
|
102
|
+
|
|
103
|
+
progress:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"current": 3,
|
|
108
|
+
"max": 10,
|
|
109
|
+
"indeterminate": false
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`max`는 1~1,000,000이고 `current`는 0~max입니다. 진행 알림은 실제 작업 상태와 일치해야 하며 완료된
|
|
114
|
+
뒤 ongoing 상태로 남기지 않습니다.
|
|
115
|
+
|
|
116
|
+
## Durable delivery
|
|
117
|
+
|
|
118
|
+
Host는 pending action을 lease한 뒤 플랫폼 gateway에 전달하고 결과를 ACK합니다. 앱이 중단되어도
|
|
119
|
+
lease가 만료되면 재전달할 수 있으므로 `key`를 안정적인 중복 제거 키로 만듭니다. 권한 철회,
|
|
120
|
+
Instance disable/delete, package generation 변경은 해당 owner의 pending action을 폐기합니다.
|
|
121
|
+
|
|
122
|
+
현재 알림 gateway 실사용 검증은 Android 기준입니다. iOS/Desktop은 같은 Host action 계약 위에
|
|
123
|
+
플랫폼 adapter를 구현해야 합니다. 지원되지 않는 플랫폼에서 알림 실패를 원본 데이터 저장 성공으로
|
|
124
|
+
위장하지 않되, 알림 하나 때문에 읽기 화면 전체를 깨뜨리지 않습니다.
|
|
125
|
+
|
|
126
|
+
## Background
|
|
127
|
+
|
|
128
|
+
Background capability는 `background` 권한과 15~10,080분의 `interval_minutes`가 필요합니다.
|
|
129
|
+
Host는 enabled, granted permission, generation, 연결 상태를 확인한 뒤 실행합니다. 앱이 꺼져 있을 때
|
|
130
|
+
정확한 시각 실행을 보장하는 cron으로 설명하지 않습니다. 사용자에게 주기, 마지막 실행, 오류,
|
|
131
|
+
끄기 동작을 settings에서 제공합니다.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Android, iOS, Desktop 호환
|
|
2
|
+
|
|
3
|
+
Morit 앱은 현재 Android-first입니다. Plugin UI와 capability 계약은 플랫폼 중립 JSON이지만, 모든
|
|
4
|
+
Host 기능이 모든 플랫폼에서 구현되었다는 뜻은 아닙니다.
|
|
5
|
+
|
|
6
|
+
## 현재 지원 범위
|
|
7
|
+
|
|
8
|
+
| 영역 | Android | iOS | Desktop |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| Manifest·서명·설치 계약 | 지원 | Host 구현 필요 | Host 구현 필요 |
|
|
11
|
+
| Material 3 Runtime tree | 지원 | 공용 Flutter renderer 재사용 목표 | 공용 Flutter renderer 재사용 목표 |
|
|
12
|
+
| capability와 Connector 호출 | 지원 | Host 실행 경계 구현 필요 | Host 실행 경계 구현 필요 |
|
|
13
|
+
| 일반 Plugin storage/settings | 지원 | Host 저장 adapter 필요 | Host 저장 adapter 필요 |
|
|
14
|
+
| 알림 예약·표시 | Android gateway 지원 | iOS gateway 필요 | OS별 gateway 필요 |
|
|
15
|
+
| Android 공유·다운로드 네이티브 기능 | 지원 | 동일 동작 아님 | 동일 동작 아님 |
|
|
16
|
+
|
|
17
|
+
문서나 배포 설명에서 iOS·Desktop을 현재 검증 완료로 표현하지 않습니다. 플랫폼 지원은 실제 앱
|
|
18
|
+
빌드와 기기 테스트 결과로 표시합니다.
|
|
19
|
+
|
|
20
|
+
## 플랫폼 중립으로 작성하기
|
|
21
|
+
|
|
22
|
+
- Dart, JavaScript, HTML, WebView, native view를 UI fragment에 넣지 않습니다.
|
|
23
|
+
- 색상 코드와 픽셀 고정 화면 대신 Host token과 반응형 속성을 사용합니다.
|
|
24
|
+
- 파일 경로는 POSIX package 상대 경로만 사용합니다.
|
|
25
|
+
- 외부 앱 실행이나 OS 설정 화면 이동을 capability 성공의 필수 조건으로 만들지 않습니다.
|
|
26
|
+
- notification은 Host action으로 요청하고 플랫폼 채널 이름을 manifest에 넣지 않습니다.
|
|
27
|
+
- touch 전용 표현 대신 “선택”, “열기”, “실행”처럼 입력 장치와 무관한 문구를 씁니다.
|
|
28
|
+
|
|
29
|
+
## 반응형 입력과 접근성
|
|
30
|
+
|
|
31
|
+
Android의 좁은 세로 화면을 최소 기준으로 설계하되, 넓은 창에서는 무조건 빈 공간을 채우지
|
|
32
|
+
않습니다. `stack_at`과 `min_item_width`로 구조만 바꾸고 읽는 순서는 유지합니다. 키보드 focus와
|
|
33
|
+
스크린 리더 순서는 JSON children 순서를 따르므로 시각적 배치와 의미 순서를 다르게 만들지 않습니다.
|
|
34
|
+
|
|
35
|
+
`icon`만 있는 action은 의미를 전달하기 어렵습니다. 주요 행동에는 텍스트 `label`을 제공하고,
|
|
36
|
+
색만으로 상태를 구분하지 않습니다. 시스템 글자 크기가 커져도 핵심 문구와 행동이 잘리지 않도록
|
|
37
|
+
중요 텍스트에 과도한 `max_lines`를 쓰지 않습니다.
|
|
38
|
+
|
|
39
|
+
## 기능 저하 원칙
|
|
40
|
+
|
|
41
|
+
특정 플랫폼 adapter가 없을 때 전체 플러그인을 깨뜨리지 않습니다.
|
|
42
|
+
|
|
43
|
+
- UI는 읽을 수 있는 상태와 지원 범위 안내를 유지합니다.
|
|
44
|
+
- 해당 action만 비활성화하거나 명확한 오류로 종료합니다.
|
|
45
|
+
- capability는 성공으로 위장하지 않습니다.
|
|
46
|
+
- 알림을 만들지 못해도 원본 일정·데이터 저장 결과는 별도로 판단합니다.
|
|
47
|
+
- 재시도가 의미 있는 경우에만 재시도 행동을 제공합니다.
|
|
48
|
+
|
|
49
|
+
## 출시 전 플랫폼 체크
|
|
50
|
+
|
|
51
|
+
각 지원 플랫폼에서 다음을 실제로 확인합니다.
|
|
52
|
+
|
|
53
|
+
1. 설치·업데이트·삭제
|
|
54
|
+
2. 첫 화면, 빈 데이터, 긴 텍스트, 큰 글자 크기
|
|
55
|
+
3. 화면 이동과 뒤로가기/창 닫기
|
|
56
|
+
4. 권한 허용·거부·철회
|
|
57
|
+
5. OAuth 연결·만료·재연결
|
|
58
|
+
6. background와 notification의 실제 전달
|
|
59
|
+
7. 오프라인·timeout·재시도
|
|
60
|
+
8. 키보드와 스크린 리더 순서
|
|
61
|
+
|
|
62
|
+
실행하지 않은 항목은 “미검증”으로 기록합니다.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 프로젝트 구조와 fragment
|
|
2
|
+
|
|
3
|
+
## 권장 디렉터리
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
my-plugin/
|
|
7
|
+
├─ manifest.json
|
|
8
|
+
├─ README.md
|
|
9
|
+
├─ tools/*.json
|
|
10
|
+
├─ skills/*.json
|
|
11
|
+
├─ search/*.json
|
|
12
|
+
├─ notifications/*.json
|
|
13
|
+
├─ background/*.json
|
|
14
|
+
├─ ui/*.json
|
|
15
|
+
├─ credentials/*.json
|
|
16
|
+
├─ slash_commands/*.json
|
|
17
|
+
├─ src/**/*.py
|
|
18
|
+
├─ assets/*.{json,md,txt,png,jpg,jpeg,gif,webp}
|
|
19
|
+
├─ children/*.mplg
|
|
20
|
+
├─ morit-plugin.json
|
|
21
|
+
└─ dist/
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`morit-plugin.json`과 `dist/`는 개발 상태와 산출물이므로 설치 패키지의 기능 계약이 아닙니다.
|
|
25
|
+
비밀 값도 두 위치에 저장하지 않습니다.
|
|
26
|
+
|
|
27
|
+
## fragment 병합 규칙
|
|
28
|
+
|
|
29
|
+
각 fragment 파일은 UTF-8 JSON 객체 하나입니다. 빌드할 때 파일명을 정렬한 순서로 다음 manifest
|
|
30
|
+
배열에 추가합니다.
|
|
31
|
+
|
|
32
|
+
| 디렉터리 | 대상 배열 | 자동으로 채우는 값 |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `capabilities/` | `capabilities` | 없음 |
|
|
35
|
+
| `tools/` | `capabilities` | `kind: "tool"` |
|
|
36
|
+
| `skills/` | `capabilities` | `kind: "skill"` |
|
|
37
|
+
| `search/` | `capabilities` | `kind: "provider"`, `runtime.role: "search"` |
|
|
38
|
+
| `notifications/` | `capabilities` | `kind: "notification"` |
|
|
39
|
+
| `background/` | `capabilities` | `kind: "background"` |
|
|
40
|
+
| `ui/` | `ui_extensions` | 없음 |
|
|
41
|
+
| `credentials/` | `credentials` | 없음 |
|
|
42
|
+
| `slash_commands/` | `slash_commands` | 없음 |
|
|
43
|
+
|
|
44
|
+
같은 값이 fragment에 이미 있으면서 자동 값과 다르면 빌드가 실패합니다. manifest 배열과 fragment
|
|
45
|
+
배열은 합쳐지므로 ID는 전체에서 유일해야 합니다.
|
|
46
|
+
|
|
47
|
+
## source와 패키지에 들어가는 파일
|
|
48
|
+
|
|
49
|
+
검증은 프로젝트의 안전한 상대 POSIX 경로만 읽습니다. 절대 경로, `..`, 역슬래시, Windows 예약명,
|
|
50
|
+
심볼릭 링크는 사용할 수 없습니다. 프로젝트 전체 한도는 1 MiB, 파일 수는 64개, 파일 하나는
|
|
51
|
+
512 KiB입니다. 최종 `.mplg`는 2 MiB 이하입니다.
|
|
52
|
+
|
|
53
|
+
빌드가 설치 패키지에 포함하는 항목은 다음뿐입니다.
|
|
54
|
+
|
|
55
|
+
- fragment를 병합해 만든 `manifest.json`
|
|
56
|
+
- 선택적 `README.md`
|
|
57
|
+
- 선언된 `sandbox_python` entrypoint와 연결된 `src/**/*.py`
|
|
58
|
+
- 허용 확장자의 `assets/`
|
|
59
|
+
- dependency와 정확히 일치하는 `children/*.mplg`
|
|
60
|
+
- `signature.json`
|
|
61
|
+
|
|
62
|
+
fragment 원본은 compiled manifest에 병합되므로 `.mplg` 안에 중복해 넣지 않습니다. `src/*.py`는
|
|
63
|
+
각각 정확히 한 `sandbox_python` capability가 entrypoint로 선언해야 하며, 선언되지 않은 Python
|
|
64
|
+
파일이나 여러 capability가 공유하는 entrypoint는 거부됩니다.
|
|
65
|
+
|
|
66
|
+
## 텍스트와 바이너리
|
|
67
|
+
|
|
68
|
+
JSON, Markdown, 텍스트, Python은 엄격한 UTF-8입니다. PNG/JPEG/WebP/GIF와 child `.mplg`는
|
|
69
|
+
바이너리 그대로 보존됩니다. Cloud sync의 JSON 경계를 지날 때만 바이너리 envelope로 변환되며,
|
|
70
|
+
pull·source ZIP·build 결과에서는 원래 바이트로 복원됩니다.
|
|
71
|
+
|
|
72
|
+
UI에서 참조하는 package 이미지는 최대 512 KiB이고 가로·세로가 각각 4096 px 이하여야 합니다.
|
|
73
|
+
정적 이미지는 1 frame, GIF/WebP 애니메이션은 최대 128 frame이며 `width × height × frame 수`로
|
|
74
|
+
계산한 frame 합산 pixel은 최대 16,000,000입니다. 확장자·MIME에 대응하는 magic byte와 실제 이미지
|
|
75
|
+
포맷도 일치해야 합니다.
|
|
76
|
+
|
|
77
|
+
### 웹 프로젝트에서 아이콘 올리기
|
|
78
|
+
|
|
79
|
+
developers.moring.co의 **플러그인 프로젝트 관리 → 정보 → 앱 아이콘**에서는 PNG, JPEG, WebP
|
|
80
|
+
파일을 직접 선택하거나 끌어 놓을 수 있습니다. 업로드가 완료되면 이미지는
|
|
81
|
+
`assets/plugin-icon.*`에 저장되고 `manifest.icon`도 같은 revision에서 함께 갱신됩니다. 기존에 이
|
|
82
|
+
화면에서 올린 아이콘을 교체하거나 제거할 때도 파일과 manifest가 원자적으로 함께 반영됩니다.
|
|
83
|
+
|
|
84
|
+
아이콘은 512 KiB 이하, 가로·세로 각각 4096 px 이하, 전체 16,000,000 pixel 이하만 허용됩니다.
|
|
85
|
+
확장자만 바꾼 파일이나 실제 포맷이 다른 이미지는 서버 검증에서 거부됩니다. MCP가 바이너리 파일을
|
|
86
|
+
직접 전송하지 못하더라도 이 화면에서 한 번 올린 아이콘은 프로젝트 source에 포함되므로 이후 MCP,
|
|
87
|
+
CLI pull·build에서도 원래 이미지 바이트와 `manifest.icon` 경로가 그대로 유지됩니다.
|
|
88
|
+
|
|
89
|
+
## 파일별 책임
|
|
90
|
+
|
|
91
|
+
- `manifest.json`: 패키지 신원, 버전, 전역 권한, 연결과 데이터 정책
|
|
92
|
+
- capability fragment: AI나 UI가 실행할 한 가지 작업
|
|
93
|
+
- UI fragment: Host가 표시할 화면과 capability 참조
|
|
94
|
+
- credential/connector: 사용자 계정 또는 Cloud 비밀 값과 외부 endpoint의 연결
|
|
95
|
+
- `src/`: 검증되고 격리된 Python 작업만 포함
|
|
96
|
+
- `assets/`: 아이콘·정적 데이터·설명 자료
|
|
97
|
+
- `children/`: 함께 설치할 별도 서명 플러그인
|
|
98
|
+
|
|
99
|
+
화면 문구, 외부 endpoint, 비밀 값, 사용자 설정을 한 JSON에 섞지 마세요. 바뀌는 주기와 접근 권한이
|
|
100
|
+
다른 정보는 각각 UI, Connector, Secret, settings에 둡니다.
|
|
101
|
+
|
|
102
|
+
다음은 [Manifest 레퍼런스](manifest.md)입니다.
|