> [!IMPORTANT]
-> `v0.2.0` is the current stable release, providing a **stable Windows x64 build**, an **unsigned macOS preview**, and five bundled Theme Schema v4 themes. Windows includes an x64 portable ZIP and per-user Setup; macOS includes separate Apple Silicon ARM64 and Intel x64 DMGs/portable archives. Every artifact bundles Node.js and requires neither Git nor development dependencies. macOS has not completed the real-ChatGPT visual loop, Developer ID signing, or notarization. Use the standard Control-click → **Open** flow below and do not disable Gatekeeper. Save your work and **fully quit the regular ChatGPT app** before applying or restoring a theme. OpenChatGPTSkin manages only the ChatGPT instance it launches and never modifies `WindowsApps`, `Codex.app`, `app.asar`, account settings, or API configuration.
+> `v0.3.0-alpha.1` is the unified Go Host candidate for Windows x64, macOS ARM64, and macOS x64. Its six installer/portable artifacts include five Theme Schema v4 themes and one Go business host; no user package bundles Node.js or requires Git, Go, or development dependencies. The theme loop, data upgrade, and v0.2.0 rollback have passed real-device acceptance on Windows and both Mac architectures. macOS artifacts remain unsigned and unnotarized, so use the standard Control-click → **Open** flow without disabling Gatekeeper. Save your work and **fully quit the regular ChatGPT app** before applying a theme. OpenChatGPTSkin manages only the ChatGPT instance it launches and never modifies `WindowsApps`, `Codex.app`, `app.asar`, account settings, or API configuration.
## Contents
@@ -81,7 +81,7 @@ Themes are data, not arbitrary code. An `.ocskin` package cannot contain JavaScr
| Five ready-to-use built-in themes | Complete |
| Windows Runtime launch/switch/pause/restore | Stable |
| Windows x64 portable ZIP and per-user Setup | Stable |
-| macOS ARM64/x64 DMG and Runtime launch/switch/restore | Unsigned preview; real-Mac acceptance pending |
+| macOS ARM64/x64 DMG and Runtime launch/switch/restore | Unsigned preview; both architectures accepted on real Macs |
| Theme Studio editing/preview/version/import/export/apply | Stable |
| Codex plugin-market installation | Not available yet |
| Automatic updates, SEA single-file executable, theme marketplace | Planned |
@@ -168,7 +168,7 @@ Portrait and decoration assets carry separate authorization identifiers and sour
### Windows Setup (recommended)
-1. Download `OpenChatGPTSkin_0.2.0_windows_x64_Setup.exe` and `checksums.txt` from [GitHub Release v0.2.0](https://github.com/u2bo/OpenChatGPTSkin/releases/tag/v0.2.0).
+1. Download `OpenChatGPTSkin_0.3.0-alpha.1_windows_x64_Setup.exe` and `checksums.txt` from [GitHub Release v0.3.0-alpha.1](https://github.com/u2bo/OpenChatGPTSkin/releases/tag/v0.3.0-alpha.1).
2. Verify SHA-256, then run Setup. It installs for the current user under `%LOCALAPPDATA%\Programs\OpenChatGPTSkin` and does not request administrator privileges.
3. Start OpenChatGPTSkin from the Start menu. The production Theme Studio opens in your default browser only after its local health check succeeds.
@@ -176,35 +176,35 @@ The installer is unsigned, so Windows SmartScreen may warn. Download only from t
### Windows portable ZIP
-Download `OpenChatGPTSkin_0.2.0_windows_x64.zip`, verify it, extract it to a stable writable directory, and double-click `OpenChatGPTSkin.cmd`. The portable build does not register an installation and needs no global Node.js or Git. Personal themes remain under `%LOCALAPPDATA%\OpenChatGPTSkin`, outside the program directory.
+Download `OpenChatGPTSkin_0.3.0-alpha.1_windows_x64.zip`, verify it, extract it to a stable writable directory, and double-click `OpenChatGPTSkin.exe`. The portable build does not register an installation and needs no global Node.js, Go, or Git. Personal themes remain under `%LOCALAPPDATA%\OpenChatGPTSkin`, outside the program directory.
### macOS DMG (unsigned developer preview)
-1. On Apple Silicon (M-series), download `OpenChatGPTSkin_0.2.0_macos_arm64.dmg`. On an Intel Mac, download `OpenChatGPTSkin_0.2.0_macos_x64.dmg`. Intel x64 compatibility depends on an official ChatGPT build for that architecture and has not completed real-device validation.
+1. On Apple Silicon (M-series), download `OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.dmg`. On an Intel Mac, download `OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.dmg`. Both targets have passed acceptance on matching real hardware and the official ChatGPT app.
2. Verify SHA-256 as shown below, open the DMG, and drag `OpenChatGPTSkin.app` to Applications.
3. On first launch, Control-click the app, choose **Open**, and confirm the standard macOS prompt. Do not disable Gatekeeper or use `xattr` to remove quarantine metadata.
4. Theme Studio opens in the default browser after its health check succeeds. Replacing or deleting the `.app` keeps personal themes, drafts, and Runtime state under `~/Library/Application Support/OpenChatGPTSkin`.
-Developers can also download `OpenChatGPTSkin_0.2.0_macos_arm64.tar.gz` or `OpenChatGPTSkin_0.2.0_macos_x64.tar.gz`. Most users should choose the DMG.
+Developers can also download `OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.tar.gz` or `OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.tar.gz`. Each archive contains the complete `OpenChatGPTSkin.app`; most users should choose the DMG.
-Maintainers can open **Actions → Build and Release → Run workflow** and manually trigger `workflow_dispatch`. GitHub-hosted Windows, ARM64 macOS, and Intel macOS runners build Windows x64, macOS ARM64, and macOS x64 test artifacts. Download `windows-release`, `macos-arm64-release`, `macos-x64-release`, and their diagnostics from the completed run. A manual run never creates a tag or GitHub Release.
+Maintainers can open **Actions → Build and Release → Run workflow** and manually trigger `workflow_dispatch`. Three native runners build and accept the Go Host, then merge the result as `go-release-combined`. A manual run never creates a tag or GitHub Release.
### Verify downloads
Run from the download directory:
```powershell
-Get-FileHash .\OpenChatGPTSkin_0.2.0_windows_x64.zip -Algorithm SHA256
-Get-FileHash .\OpenChatGPTSkin_0.2.0_windows_x64_Setup.exe -Algorithm SHA256
+Get-FileHash .\OpenChatGPTSkin_0.3.0-alpha.1_windows_x64.zip -Algorithm SHA256
+Get-FileHash .\OpenChatGPTSkin_0.3.0-alpha.1_windows_x64_Setup.exe -Algorithm SHA256
Get-Content .\checksums.txt
```
macOS Terminal:
```bash
-shasum -a 256 OpenChatGPTSkin_0.2.0_macos_arm64.dmg
+shasum -a 256 OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.dmg
# On Intel:
-shasum -a 256 OpenChatGPTSkin_0.2.0_macos_x64.dmg
+shasum -a 256 OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.dmg
cat checksums.txt
```
@@ -212,7 +212,7 @@ Each hash must exactly match the corresponding line in `checksums.txt`. Do not r
### Install from source
-Source development requires Windows 11 or macOS, official Codex Desktop, Node.js `>= 22.0.0`, and npm. Git can be replaced with a downloaded source archive.
+Source development requires Windows 11 or macOS, official Codex Desktop, Go `1.25.12`, Node.js `>= 22.0.0`, and npm. Node is used only to build the frontend, contracts, and CDP Adapter; it is not shipped to users.
Clone or download the repository from GitHub, then run from its root:
@@ -233,7 +233,7 @@ Windows developers can generate the same portable ZIP, per-user Setup, and SHA-2
npm run release:windows
```
-The first run only requires Node.js 22, npm, and Git. If Inno Setup 6 is missing, the script downloads a pinned version from the official GitHub Release, verifies its pinned SHA-256, and uses it in portable mode from the temporary build directory without administrator access or a permanent installation. It validates and reuses complete existing dependencies, incrementally repairs missing or inconsistent dependencies from `package-lock.json`, then runs the full verification and Theme Studio build, fetches the official bundled Node.js Runtime, validates the staged payload, and packages the release. Final files are written to `artifacts/windows-x64/`. Failed builds retain and print their temporary directory for diagnosis; successful builds clean it automatically. The command intentionally skips the CI installer lifecycle test that manipulates local installation registration and the personal data directory.
+Local release builds require Go `1.25.12`, Node.js 22, npm, and Inno Setup 6. The command builds Theme Studio and the single Go Host, then produces a Node-free stage, ZIP, Setup, and SHA-256 file under `artifacts/windows-x64/`. End users need none of these development tools.
When upgrading from the pre-rename development build, the first CLI or Theme Studio start atomically adopts the previous personal themes, drafts, and Runtime state only when the new-brand data directory does not exist. If both directories exist, the new directory wins and neither side is merged or overwritten.
@@ -244,7 +244,7 @@ Installing a newer Setup, replacing a portable directory, or replacing the macOS
### Theme Studio (recommended)
1. Save your work and choose **Quit Codex** from the Codex menu or system tray. Make sure the regular app is fully closed.
-2. Windows Setup users launch from the Start menu, portable users double-click `OpenChatGPTSkin.cmd`, macOS users launch `OpenChatGPTSkin.app` from Applications, and source users run:
+2. Windows Setup users launch from the Start menu, portable users double-click `OpenChatGPTSkin.exe`, macOS users launch `OpenChatGPTSkin.app` from Applications, and source users run:
```powershell
npm run studio:dev
@@ -309,29 +309,20 @@ npm run runtime -- restore
- `restore` restores the official appearance and waits for a normal managed-Codex exit to finish cleanup.
- Do not use Task Manager to force-close Codex while restore is pending.
-See [Windows Runtime and Compatibility](docs/runtime-windows.md) and [macOS Runtime and Acceptance](docs/runtime-macos.en.md) for the platform safety boundaries. macOS DMGs complete package, bundled-Runtime, Theme Studio, and four-theme acceptance on native ARM64/x64 runners. Real-Codex Runtime probes and visual-loop acceptance still require the manual real-device checklist.
+See [Windows Runtime and Compatibility](docs/runtime-windows.md) and [macOS Runtime and Acceptance](docs/runtime-macos.en.md) for the platform safety boundaries. The three native runners verify package structure, the single Go Host, Theme Studio, all five built-ins, and the Node-free manifest. Real-Codex visual and lifecycle acceptance still follows the manual checklist on each target platform.
-### Compatibility probe and real-app acceptance (Windows)
+### Real-app acceptance after a Codex update
-After a Codex update, fully quit the regular app and run the two-phase compatibility probe:
+The old Node Host commands `runtime:probe` and `runtime:acceptance` were removed during the Go cutover and are no longer executable entry points. After a Codex update, use a test workspace with no private projects or sensitive chats and:
-```powershell
-npm run runtime:probe -- --record-evidence
-# Quit the managed Codex instance normally from the Codex menu
-npm run runtime:probe -- --finalize
-```
-
-For a release candidate, run the full Runtime acceptance flow:
-
-```powershell
-npm run runtime:acceptance -- --begin
-# Quit managed Codex normally, then start official Codex normally from Start
-npm run runtime:acceptance -- --finalize
-```
+1. Fully quit regular Codex and check all five built-ins, a custom theme, `pause`, `resume`, and `restore`.
+2. Quit the managed instance from the Codex menu and verify Controller/control-endpoint cleanup.
+3. Start official Codex normally and verify that it has no inherited remote-debugging flags and keeps the official appearance.
+4. Record Codex/OpenChatGPTSkin/OS versions, results, and sanitized screenshots.
-Acceptance evidence must remain sanitized and must not contain PIDs, ports, paths, command lines, project names, chat content, or screenshots.
+Published evidence must not contain PIDs, ports, usernames, absolute paths, command lines, project names, or chat content.
-macOS packages receive automated acceptance on native CI runners; the real-Codex visual and Runtime loop still uses a manual checklist. On a real Mac, verify `codesign`, `spctl`, Unix-socket permissions, all five built-in themes, restore, and a regular Codex restart as described in [macOS Runtime and Acceptance](docs/runtime-macos.en.md).
+See the full [Windows checklist](docs/runtime-windows.md) and [macOS checklist](docs/runtime-macos.en.md).
## FAQ
@@ -379,8 +370,7 @@ packages/theme-schema/ Theme Schema v4, migrations, and visual model
packages/theme-core/ Validation, catalog, archive, storage
packages/cdp-adapter/ Codex UI surface recognition and compilation
packages/theme-studio-core/ Theme Studio contracts and validation
-runtime/windows/ Desktop Runtime, Controller, recovery (historical package path)
-runtime/theme-studio-service/ Local Theme Studio service
+host/go/ Single Go Studio, Controller, Runtime, and platform adapters
themes/builtin/ Built-in themes and asset provenance
tests/ Schema, Runtime, UI, and documentation tests
```
@@ -400,7 +390,8 @@ UI adaptation changes must include deterministic page fixtures/tests. Theme cont
## Documentation
-- [v0.2.0 Release Notes](docs/releases/v0.2.0.md)
+- [v0.3.0-alpha.1 Release Notes](docs/releases/v0.3.0-alpha.1.md)
+- [v0.2.0 Historical Release Notes](docs/releases/v0.2.0.md)
- [v0.1.0 Historical Release Notes](docs/releases/v0.1.0.md)
- [v0.1.0-alpha.1 Historical Release Notes](docs/releases/v0.1.0-alpha.1.md)
- [Custom Theme Guide](docs/custom-theme-guide.en.md)
diff --git a/README.md b/README.md
index 5f4fdc1..575982d 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
[简体中文](README.md) · [English](README.en.md)
-
-
-
+
+
+


[](https://linux.do/)
@@ -46,7 +46,7 @@
> [!IMPORTANT]
-> `v0.2.0` 是当前正式版本,提供 **Windows x64 正式版**和 **macOS 未签名预览版**,并内置五个 Theme Schema v4 主题。Windows 包含 x64 便携 ZIP 与用户级 Setup;macOS 分别提供 Apple Silicon ARM64 和 Intel x64 DMG/便携包。所有产物都内置 Node.js,无需安装 Git 或开发依赖。macOS 尚未完成真实 ChatGPT 视觉闭环、Developer ID 签名和公证;请按下文通过系统标准“右键 → 打开”确认,不要关闭 Gatekeeper。应用或恢复主题前,请保存工作并**完全退出普通 ChatGPT**。OpenChatGPTSkin 只管理自己启动的 ChatGPT 实例,不会强制结束已有 ChatGPT,也不会修改 `WindowsApps`、`Codex.app`、`app.asar`、账号或 API 配置。
+> `v0.3.0-alpha.1` 是统一 Go Host 候选版本,提供 Windows x64、macOS ARM64 与 macOS x64 六类安装/便携产物,并内置五个 Theme Schema v4 主题。用户包只包含一个 Go 业务宿主,不捆绑 Node.js,也不要求安装 Git、Go 或开发依赖。Windows 与两种 Mac 架构的主题闭环、数据升级和 v0.2.0 回滚均已完成实机验收。macOS 产物仍未使用 Developer ID 正式签名或公证;请按下文通过系统标准“右键 → 打开”确认,不要关闭 Gatekeeper。应用主题前,请保存工作并**完全退出普通 ChatGPT**。OpenChatGPTSkin 只管理自己启动的 ChatGPT 实例,不会强制结束已有 ChatGPT,也不会修改 `WindowsApps`、`Codex.app`、`app.asar`、账号或 API 配置。
## 目录
@@ -81,7 +81,7 @@ OpenChatGPTSkin 由三个相互约束的部分组成:
| 五个可直接使用的内置主题 | 已完成 |
| Windows Runtime 启动、切换、暂停、恢复 | 正式版 |
| Windows x64 便携 ZIP 与用户级 Setup | 正式版 |
-| macOS ARM64/x64 DMG、Runtime 启动/切换/恢复 | 未签名预览,实机验收待完成 |
+| macOS ARM64/x64 DMG、Runtime 启动/切换/恢复 | 未签名预览,双架构实机验收通过 |
| Theme Studio 编辑、预览、版本、导入导出、应用 | 正式版 |
| Codex 插件市场安装 | 尚未提供 |
| 自动更新、SEA 单文件程序、主题市场 | 规划中 |
@@ -168,7 +168,7 @@ OpenChatGPTSkin 的目标不是在首页覆盖一张背景图。Runtime 使用
### Windows Setup(推荐)
-1. 在 [GitHub Releases](https://github.com/u2bo/OpenChatGPTSkin/releases/tag/v0.2.0) 下载 `OpenChatGPTSkin_0.2.0_windows_x64_Setup.exe` 和 `checksums.txt`。
+1. 在 [GitHub Releases](https://github.com/u2bo/OpenChatGPTSkin/releases/tag/v0.3.0-alpha.1) 下载 `OpenChatGPTSkin_0.3.0-alpha.1_windows_x64_Setup.exe` 和 `checksums.txt`。
2. 校验 SHA-256 后双击 Setup。安装范围为当前用户,默认目录是 `%LOCALAPPDATA%\Programs\OpenChatGPTSkin`,不请求管理员权限。
3. 从开始菜单启动 OpenChatGPTSkin;生产 Theme Studio 健康启动后会自动打开默认浏览器。
@@ -176,35 +176,35 @@ OpenChatGPTSkin 的目标不是在首页覆盖一张背景图。Runtime 使用
### Windows 便携 ZIP
-下载 `OpenChatGPTSkin_0.2.0_windows_x64.zip`,校验后解压到可写且稳定的目录,双击 `OpenChatGPTSkin.cmd`。便携版不会注册安装信息,也不依赖全局 Node.js 或 Git;个人主题仍写入 `%LOCALAPPDATA%\OpenChatGPTSkin`,不会写入程序目录。
+下载 `OpenChatGPTSkin_0.3.0-alpha.1_windows_x64.zip`,校验后解压到可写且稳定的目录,双击 `OpenChatGPTSkin.exe`。便携版不会注册安装信息,也不依赖全局 Node.js、Go 或 Git;个人主题仍写入 `%LOCALAPPDATA%\OpenChatGPTSkin`,不会写入程序目录。
### macOS DMG(未签名开发者预览)
-1. Apple Silicon(M 系列)下载 `OpenChatGPTSkin_0.2.0_macos_arm64.dmg`;Intel Mac 下载 `OpenChatGPTSkin_0.2.0_macos_x64.dmg`。Intel x64 兼容性取决于官方 ChatGPT 是否提供对应架构版本,目前尚未完成实机验证。
+1. Apple Silicon(M 系列)下载 `OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.dmg`;Intel Mac 下载 `OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.dmg`。两个架构均已在对应真实设备与官方 ChatGPT 上完成验收。
2. 先按下方命令核对 SHA-256,再打开 DMG,将 `OpenChatGPTSkin.app` 拖入 Applications。
3. 首次启动时按住 Control 点击或右键点击应用,选择“打开”,再确认 macOS 标准提示。不要关闭 Gatekeeper,也不要使用 `xattr` 移除隔离属性。
4. Theme Studio 健康启动后会自动打开默认浏览器。替换或删除 `.app` 不会删除 `~/Library/Application Support/OpenChatGPTSkin` 下的个人主题、草稿和 Runtime 状态。
-开发者还可以下载同架构的 `OpenChatGPTSkin_0.2.0_macos_arm64.tar.gz` 或 `OpenChatGPTSkin_0.2.0_macos_x64.tar.gz`。普通用户优先使用 DMG。
+开发者还可以下载同架构的 `OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.tar.gz` 或 `OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.tar.gz`。压缩包内同样是完整 `OpenChatGPTSkin.app`;普通用户优先使用 DMG。
-维护者可以进入仓库 **Actions → Build and Release → Run workflow** 手动触发 `workflow_dispatch`。GitHub 托管的 Windows、ARM64 macOS 和 Intel macOS Runner 会构建 Windows x64、macOS ARM64 和 macOS x64 测试产物;完成后下载 `windows-release`、`macos-arm64-release`、`macos-x64-release` 及对应 diagnostics。手动运行不会创建 Tag 或 GitHub Release。
+维护者可以进入仓库 **Actions → Build and Release → Run workflow** 手动触发 `workflow_dispatch`。三个原生 Runner 会分别构建并验收 Go Host,随后合并为 `go-release-combined`;手动运行不会创建 Tag 或 GitHub Release。
### 校验下载文件
在下载目录运行:
```powershell
-Get-FileHash .\OpenChatGPTSkin_0.2.0_windows_x64.zip -Algorithm SHA256
-Get-FileHash .\OpenChatGPTSkin_0.2.0_windows_x64_Setup.exe -Algorithm SHA256
+Get-FileHash .\OpenChatGPTSkin_0.3.0-alpha.1_windows_x64.zip -Algorithm SHA256
+Get-FileHash .\OpenChatGPTSkin_0.3.0-alpha.1_windows_x64_Setup.exe -Algorithm SHA256
Get-Content .\checksums.txt
```
macOS 终端:
```bash
-shasum -a 256 OpenChatGPTSkin_0.2.0_macos_arm64.dmg
+shasum -a 256 OpenChatGPTSkin_0.3.0-alpha.1_macos_arm64.dmg
# Intel Mac 使用:
-shasum -a 256 OpenChatGPTSkin_0.2.0_macos_x64.dmg
+shasum -a 256 OpenChatGPTSkin_0.3.0-alpha.1_macos_x64.dmg
cat checksums.txt
```
@@ -212,7 +212,7 @@ cat checksums.txt
### 从源码安装
-源码开发需要 Windows 11 或 macOS、官方 Codex Desktop、Node.js `>= 22.0.0` 和 npm;Git 可由源码压缩包替代。
+源码开发需要 Windows 11 或 macOS、官方 Codex Desktop、Go `1.25.12`、Node.js `>= 22.0.0` 和 npm;Node 只用于前端、Contract 与 CDP Adapter 构建,不进入用户发布包。
从 GitHub 页面克隆或下载仓库,然后在仓库根目录运行:
@@ -233,7 +233,7 @@ Windows 开发者可以在仓库根目录用一条命令生成与 CI 相同结
npm run release:windows
```
-首次运行前只需要 Node.js 22、npm 与 Git;如果本机没有 Inno Setup 6,脚本会从官方 GitHub Release 下载固定版本、校验固定 SHA-256,并在临时目录中以便携模式使用,不需要管理员权限或永久安装。随后脚本会校验并复用完整的现有依赖,仅在依赖缺失或不一致时依据 `package-lock.json` 增量修复,再完成完整验证、Theme Studio 构建、官方 Node.js Runtime 下载、发布载荷验收和打包。最终产物位于 `artifacts/windows-x64/`。如果构建失败,临时目录会保留并打印路径用于排障;成功后会自动清理临时文件。该命令不会运行会触碰本机安装注册与个人数据目录的 CI 安装生命周期测试。
+本地构建需要 Go `1.25.12`、Node.js 22、npm 和 Inno Setup 6。命令会构建 Theme Studio 与单一 Go Host,生成 Node-free Stage、ZIP、Setup 和 SHA-256;最终产物位于 `artifacts/windows-x64/`。用户不需要安装这些开发工具。
从重命名前的开发版本升级时,首次启动 CLI 或 Theme Studio 会在新品牌数据目录不存在的前提下,原子迁移上一版本的个人主题、草稿和 Runtime 状态。若新旧目录同时存在,新目录优先,程序不会自动合并或覆盖任何一边。
@@ -244,7 +244,7 @@ npm run release:windows
### 使用 Theme Studio(推荐)
1. 保存正在进行的工作,通过 Codex 菜单或系统托盘执行“退出 / Quit Codex”,确认普通 Codex 已完全退出。
-2. Windows Setup 用户从开始菜单启动,便携版双击 `OpenChatGPTSkin.cmd`;macOS 用户从“应用程序”启动 `OpenChatGPTSkin.app`;源码用户运行:
+2. Windows Setup 用户从开始菜单启动,便携版双击 `OpenChatGPTSkin.exe`;macOS 用户从“应用程序”启动 `OpenChatGPTSkin.app`;源码用户运行:
```powershell
npm run studio:dev
@@ -309,29 +309,20 @@ npm run runtime -- restore
- `restore`:恢复官方外观,并等待用户正常退出受管理 Codex 完成清理;
- 不要使用任务管理器强制结束恢复中的 Codex。
-完整安全边界见 [Windows Runtime 说明](docs/runtime-windows.md) 与 [macOS Runtime 说明](docs/runtime-macos.md)。macOS DMG 会在原生 ARM64/x64 Runner 完成包结构、内置 Runtime、Theme Studio 和四主题自动验收;真实 Codex 的 Runtime Probe 与视觉闭环仍须按 macOS 文档在真实设备手动完成。
+完整安全边界见 [Windows Runtime 说明](docs/runtime-windows.md) 与 [macOS Runtime 说明](docs/runtime-macos.md)。三个原生 Runner 会验证包结构、单一 Go Host、Theme Studio、五个内置主题及 Node-free manifest;真实 Codex 的视觉和生命周期闭环仍按对应平台文档在真实设备手动验收。
-### 兼容性 Probe 与真实验收(Windows)
+### Codex 更新后的真实验收
-Codex 升级后,先在完全退出普通 Codex 的前提下运行两阶段兼容性 Probe:
+旧 Node Host 的 `runtime:probe` 与 `runtime:acceptance` 已随 Go cutover 删除,不再作为可执行入口。Codex 升级后,在无私人项目或敏感聊天的测试工作区完成以下检查:
-```powershell
-npm run runtime:probe -- --record-evidence
-# 使用 Codex 的“退出 / Quit Codex”正常退出受管理实例
-npm run runtime:probe -- --finalize
-```
-
-发布候选版本可以进一步运行完整 Runtime 验收:
-
-```powershell
-npm run runtime:acceptance -- --begin
-# 正常退出受管理 Codex,并从开始菜单正常启动官方 Codex
-npm run runtime:acceptance -- --finalize
-```
+1. 完全退出普通 Codex,依次检查五个内置主题、自定义主题、`pause`、`resume` 与 `restore`;
+2. 从 Codex 菜单正常退出受管理实例,确认 Controller 和本地控制端点完成清理;
+3. 正常启动官方 Codex,确认未继承远程调试参数且保持官方外观;
+4. 记录 Codex/OpenChatGPTSkin/系统版本、结果和脱敏截图。
-验收证据必须脱敏,不得包含 PID、端口、路径、命令行、项目名、聊天内容或截图。
+公开的验收记录不得包含 PID、端口、用户名、绝对路径、命令行、项目名或聊天内容。
-macOS 安装包会在原生 CI 中完成自动验收;真实 Codex 的视觉与 Runtime 闭环仍使用手动清单。请在真实 Mac 上核对 `codesign`、`spctl`、Unix socket 权限、五个内置主题、恢复流程和普通 Codex 重启,详见 [macOS Runtime 说明](docs/runtime-macos.md)。
+Windows 与 macOS 的完整检查项分别见 [Windows Runtime 说明](docs/runtime-windows.md) 和 [macOS Runtime 说明](docs/runtime-macos.md)。
## 常见问题
@@ -379,8 +370,7 @@ packages/theme-schema/ Theme Schema v4、迁移与视觉模型
packages/theme-core/ 校验、目录、打包、存储
packages/cdp-adapter/ Codex UI surface 识别与主题编译
packages/theme-studio-core/ Theme Studio 合约与校验
-runtime/windows/ Desktop Runtime、Controller、恢复(保留历史包路径)
-runtime/theme-studio-service/ 本地 Theme Studio 服务
+host/go/ 单一 Go Studio、Controller、Runtime 与平台适配
themes/builtin/ 五个内置主题及素材来源记录
tests/ Schema、Runtime、UI 和文档测试
```
@@ -400,7 +390,8 @@ npm run build
## 更多文档
-- [v0.2.0 发布说明](docs/releases/v0.2.0.md)
+- [v0.3.0-alpha.1 发布说明](docs/releases/v0.3.0-alpha.1.md)
+- [v0.2.0 历史发布说明](docs/releases/v0.2.0.md)
- [v0.1.0 历史发布说明](docs/releases/v0.1.0.md)
- [v0.1.0-alpha.1 历史发布说明](docs/releases/v0.1.0-alpha.1.md)
- [自定义主题指南](docs/custom-theme-guide.md)
diff --git a/apps/theme-studio/package.json b/apps/theme-studio/package.json
index 29738b9..e7baa45 100644
--- a/apps/theme-studio/package.json
+++ b/apps/theme-studio/package.json
@@ -1,6 +1,6 @@
{
"name": "@open-chatgpt-skin/theme-studio",
- "version": "0.2.0",
+ "version": "0.3.0-alpha.1",
"private": true,
"type": "module",
"scripts": {
@@ -8,8 +8,8 @@
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.node.json --noEmit"
},
"dependencies": {
- "@open-chatgpt-skin/theme-schema": "0.2.0",
- "@open-chatgpt-skin/theme-studio-core": "0.2.0",
+ "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1",
+ "@open-chatgpt-skin/theme-studio-core": "0.3.0-alpha.1",
"@phosphor-icons/react": "^2.1.10",
"react": "19.0.0",
"react-dom": "19.0.0"
diff --git a/contracts/baseline/v0.2.0/release-assets.json b/contracts/baseline/v0.2.0/release-assets.json
new file mode 100644
index 0000000..f9f3daf
--- /dev/null
+++ b/contracts/baseline/v0.2.0/release-assets.json
@@ -0,0 +1,172 @@
+{
+ "schemaVersion": 1,
+ "baselineVersion": "0.2.0",
+ "release": {
+ "tag": "v0.2.0",
+ "publishedAt": "2026-07-24T06:54:04Z",
+ "assets": [
+ {
+ "name": "OpenChatGPTSkin_0.2.0_windows_x64_Setup.exe",
+ "platform": "windows",
+ "arch": "x64",
+ "kind": "setup",
+ "bytes": 34083496,
+ "sha256": "2919f5564270932222c2dfd75d776ab08af32922dc600ec64bec687b9b26fadc"
+ },
+ {
+ "name": "OpenChatGPTSkin_0.2.0_windows_x64.zip",
+ "platform": "windows",
+ "arch": "x64",
+ "kind": "zip",
+ "bytes": 44744668,
+ "sha256": "a4d36b5c36479179222dfc1c77bad33fb51358c1b7a90cbc84f13e2fb67fd4b5"
+ },
+ {
+ "name": "OpenChatGPTSkin_0.2.0_macos_arm64.dmg",
+ "platform": "macos",
+ "arch": "arm64",
+ "kind": "dmg",
+ "bytes": 59251126,
+ "sha256": "9d5c0cbbe888c9fb8262800d94fd08c997e7115a5e74dfc1484321237268245d"
+ },
+ {
+ "name": "OpenChatGPTSkin_0.2.0_macos_arm64.tar.gz",
+ "platform": "macos",
+ "arch": "arm64",
+ "kind": "tar.gz",
+ "bytes": 48772737,
+ "sha256": "7ff9db996a31a59b766a20d0f2302a58294ed20cb0b1b015f1ba94a83e1408b6"
+ },
+ {
+ "name": "OpenChatGPTSkin_0.2.0_macos_x64.dmg",
+ "platform": "macos",
+ "arch": "x64",
+ "kind": "dmg",
+ "bytes": 62221496,
+ "sha256": "5e9f08c624ec65244fe3a761cfc961da35bbd5eafbf397dafb4a9f0f8214ca94"
+ },
+ {
+ "name": "OpenChatGPTSkin_0.2.0_macos_x64.tar.gz",
+ "platform": "macos",
+ "arch": "x64",
+ "kind": "tar.gz",
+ "bytes": 51253278,
+ "sha256": "9c7fe62b8bcbfc69962be1804f0af115a1b31b4e9a8cc044220045e616ca0cf5"
+ },
+ {
+ "name": "checksums.txt",
+ "platform": "all",
+ "arch": "all",
+ "kind": "checksums",
+ "bytes": 632,
+ "sha256": "ba4da68cdccd7c04ad9e5b847f07ec0c553beea3e5e3d4e256c397574d28def3"
+ }
+ ]
+ },
+ "stages": [
+ {
+ "target": "windows-x64",
+ "sourceAsset": "OpenChatGPTSkin_0.2.0_windows_x64.zip",
+ "totalBytes": 115070815,
+ "composition": {
+ "apps": 502939,
+ "LICENSE": 1577,
+ "node_modules": 24632575,
+ "launcher": 204,
+ "README.en.md": 24611,
+ "README.md": 23125,
+ "release-manifest.json": 115757,
+ "runtime": 85347134,
+ "themes": 4422893
+ }
+ },
+ {
+ "target": "macos-arm64",
+ "sourceAsset": "OpenChatGPTSkin_0.2.0_macos_arm64.tar.gz",
+ "totalBytes": 136974297,
+ "composition": {
+ "apps": 502927,
+ "LICENSE": 1546,
+ "node_modules": 21105393,
+ "launcher": 282,
+ "README.en.md": 24611,
+ "README.md": 23125,
+ "release-manifest.json": 116339,
+ "runtime": 110777181,
+ "themes": 4422893
+ }
+ },
+ {
+ "target": "macos-x64",
+ "sourceAsset": "OpenChatGPTSkin_0.2.0_macos_x64.tar.gz",
+ "totalBytes": 141958550,
+ "composition": {
+ "apps": 502927,
+ "LICENSE": 1546,
+ "node_modules": 23002262,
+ "launcher": 282,
+ "README.en.md": 24611,
+ "README.md": 23125,
+ "release-manifest.json": 116315,
+ "runtime": 113864589,
+ "themes": 4422893
+ }
+ }
+ ],
+ "contracts": {
+ "studioProtocol": 2,
+ "runtimeControl": 1,
+ "theme": 4,
+ "draft": 1,
+ "runtimeState": 2,
+ "trustedInstallCache": 1,
+ "themeStore": 1,
+ "controllerLock": 1,
+ "releaseManifest": 1,
+ "themeCatalog": 1
+ },
+ "themes": {
+ "catalog": {
+ "bytes": 1718,
+ "sha256": "db222609e1eb0df84a001ff3823171a20dc000f8f91bec1155a852de059c7749"
+ },
+ "directoryHashAlgorithm": "sha256-path-length-content-v1",
+ "builtins": [
+ {
+ "id": "future-idol-cyan",
+ "path": "builtin/future-idol-cyan",
+ "bytes": 443292,
+ "fileCount": 10,
+ "sha256": "cc55d0c4ddc7fc17667b7a1763599e11f565dc4c066a226aa2082ad8c4e6d35e"
+ },
+ {
+ "id": "glacier-aurora",
+ "path": "builtin/glacier-aurora",
+ "bytes": 370100,
+ "fileCount": 10,
+ "sha256": "50692ab6612c5c8a09dacc883403263d739811e145f13e08339461daa4930f6a"
+ },
+ {
+ "id": "mountain-mist",
+ "path": "builtin/mountain-mist",
+ "bytes": 253055,
+ "fileCount": 10,
+ "sha256": "de63ea7d9de6c759b56eb444f15b4cfb2d9ea24c2962fd9cfb917b0b36fcdd8a"
+ },
+ {
+ "id": "rose-carpet-star",
+ "path": "builtin/rose-carpet-star",
+ "bytes": 339181,
+ "fileCount": 10,
+ "sha256": "51208967070e6337fb7111f6722ad596bab0240c9e948c404df13e5563c8ce54"
+ },
+ {
+ "id": "yua-mikami-starlight",
+ "path": "builtin/yua-mikami-starlight",
+ "bytes": 3015547,
+ "fileCount": 15,
+ "sha256": "cb3c6b37c64edd5dd67038bd8123510ecd6c14823bfb703d746451cd216962b5"
+ }
+ ]
+ }
+}
diff --git a/contracts/data/v1/cases/compatibility.json b/contracts/data/v1/cases/compatibility.json
new file mode 100644
index 0000000..c4a84d0
--- /dev/null
+++ b/contracts/data/v1/cases/compatibility.json
@@ -0,0 +1,83 @@
+{
+ "cases": [
+ {
+ "name": "draft v1",
+ "schema": "draftRecord",
+ "valid": true,
+ "version": 1
+ },
+ {
+ "name": "theme store v1",
+ "schema": "themeStore",
+ "valid": true,
+ "version": 1
+ },
+ {
+ "name": "runtime state v2",
+ "schema": "runtimeState",
+ "valid": true,
+ "version": 2
+ },
+ {
+ "name": "trusted install cache v1",
+ "schema": "trustedInstall",
+ "valid": true,
+ "version": 1
+ },
+ {
+ "name": "controller lock v1",
+ "schema": "controllerLock",
+ "valid": true,
+ "version": 1
+ },
+ {
+ "name": "release manifest v2",
+ "schema": "releaseManifest",
+ "valid": true,
+ "version": 2
+ },
+ {
+ "expectedErrorCode": "DATA_SCHEMA_INVALID",
+ "expectedPath": "/schemaVersion",
+ "name": "unknown draft version",
+ "schema": "draftRecord",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "DATA_SCHEMA_INVALID",
+ "expectedPath": "/past",
+ "name": "dirty draft without history",
+ "schema": "draftRecord",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "RUNTIME_INVALID_STATE",
+ "expectedPath": "/recentRequests/0/requestId",
+ "name": "runtime request ID mismatch",
+ "schema": "runtimeState",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "CODEX_IDENTITY_INVALID",
+ "expectedPath": "/packagePublisher",
+ "name": "trusted cache identity mismatch",
+ "schema": "trustedInstall",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "RUNTIME_SESSION_STALE",
+ "expectedPath": "/startedAt",
+ "name": "lock without process identity",
+ "schema": "controllerLock",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "RELEASE_MANIFEST_INVALID",
+ "expectedPath": "/files/0/sha256",
+ "name": "release file hash malformed",
+ "schema": "releaseManifest",
+ "valid": false
+ }
+ ],
+ "schemaVersion": 1
+}
diff --git a/contracts/data/v1/schemas/index.json b/contracts/data/v1/schemas/index.json
new file mode 100644
index 0000000..6e92665
--- /dev/null
+++ b/contracts/data/v1/schemas/index.json
@@ -0,0 +1,4353 @@
+{
+ "schemas": {
+ "controllerLock": {
+ "$ref": "#/definitions/controllerLock",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "controllerLock": {
+ "additionalProperties": false,
+ "properties": {
+ "pid": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "startedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "pid",
+ "startedAt"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "draftRecord": {
+ "$ref": "#/definitions/draftRecord",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "draftRecord": {
+ "additionalProperties": false,
+ "properties": {
+ "dirty": {
+ "type": "boolean"
+ },
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "future": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "maxItems": 50,
+ "type": "array"
+ },
+ "past": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "maxItems": 50,
+ "type": "array"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "savedRef": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "theme": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "draftId",
+ "theme",
+ "revision",
+ "updatedAt",
+ "savedRef",
+ "dirty",
+ "past",
+ "future"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "persistedDraftRecord": {
+ "$ref": "#/definitions/persistedDraftRecord",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "persistedDraftRecord": {
+ "additionalProperties": false,
+ "properties": {
+ "dirty": {
+ "type": "boolean"
+ },
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "future": {
+ "items": {},
+ "maxItems": 50,
+ "type": "array"
+ },
+ "past": {
+ "items": {},
+ "maxItems": 50,
+ "type": "array"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "savedRef": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "theme": {},
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "draftId",
+ "revision",
+ "updatedAt",
+ "savedRef",
+ "dirty",
+ "past",
+ "future"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "recentRuntimeRequest": {
+ "$ref": "#/definitions/recentRuntimeRequest",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "recentRuntimeRequest": {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "enum": [
+ "launch",
+ "status",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ "completedAt": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "response": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "result": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "anyOf": [
+ {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ {
+ "const": "stopped",
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "result"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "error": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "CODEX_NOT_INSTALLED",
+ "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP",
+ "CODEX_IDENTITY_INVALID",
+ "CODEX_ALREADY_RUNNING_UNMANAGED",
+ "CODEX_LAUNCH_FAILED",
+ "CODEX_WINDOW_ACTIVATION_FAILED",
+ "PROCESS_INSPECTION_DENIED",
+ "CDP_NOT_READY",
+ "CDP_ENDPOINT_UNSAFE",
+ "CDP_PROCESS_MISMATCH",
+ "CDP_TARGET_NOT_FOUND",
+ "CDP_TARGET_AMBIGUOUS",
+ "ADAPTER_INCOMPATIBLE",
+ "THEME_APPLY_FAILED",
+ "THEME_VERIFY_FAILED",
+ "THEME_CLEANUP_FAILED",
+ "THEME_SWITCH_FAILED",
+ "THEME_ROLLBACK_FAILED",
+ "THEME_RUNTIME_TOO_LARGE",
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_HOME_WELCOME_UNSUPPORTED",
+ "THEME_REQUIRED_LAYER_UNRESOLVED",
+ "THEME_NOT_FOUND",
+ "THEME_NOT_READY",
+ "RUNTIME_SESSION_STALE",
+ "RUNTIME_INVALID_STATE",
+ "RUNTIME_BUSY",
+ "RUNTIME_ENVIRONMENT_INVALID",
+ "RUNTIME_CONTROL_UNAVAILABLE",
+ "RESTORE_AWAITING_EXIT",
+ "PROBE_EXIT_PENDING",
+ "PROBE_FINALIZE_REQUIRED",
+ "PROBE_PENDING_SESSION_INVALID",
+ "INTERNAL"
+ ],
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "ok": {
+ "const": false,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "error"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "required": [
+ "requestId",
+ "command",
+ "response",
+ "completedAt"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "releaseManifest": {
+ "$ref": "#/definitions/releaseManifest",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "releaseManifest": {
+ "additionalProperties": false,
+ "properties": {
+ "cdpAdapter": {
+ "additionalProperties": false,
+ "properties": {
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "contracts": {
+ "additionalProperties": false,
+ "properties": {
+ "data": {
+ "const": 1,
+ "type": "number"
+ },
+ "runtime": {
+ "const": 1,
+ "type": "number"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ },
+ "studio": {
+ "const": 2,
+ "type": "number"
+ },
+ "theme": {
+ "const": 4,
+ "type": "number"
+ }
+ },
+ "required": [
+ "studio",
+ "runtime",
+ "theme",
+ "data",
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "files": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "path": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "bytes",
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "host": {
+ "additionalProperties": false,
+ "properties": {
+ "commit": {
+ "pattern": "^[0-9a-f]{40}$",
+ "type": "string"
+ },
+ "dirty": {
+ "type": "boolean"
+ },
+ "entry": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "path": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "bytes",
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "goVersion": {
+ "pattern": "^go\\d+\\.\\d+(?:\\.\\d+)?$",
+ "type": "string"
+ },
+ "language": {
+ "const": "go",
+ "type": "string"
+ }
+ },
+ "required": [
+ "language",
+ "goVersion",
+ "commit",
+ "dirty",
+ "entry"
+ ],
+ "type": "object"
+ },
+ "image": {
+ "additionalProperties": false,
+ "properties": {
+ "cgo": {
+ "const": false,
+ "type": "boolean"
+ },
+ "implementation": {
+ "maxLength": 160,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "implementation",
+ "cgo"
+ ],
+ "type": "object"
+ },
+ "product": {
+ "const": "OpenChatGPTSkin",
+ "type": "string"
+ },
+ "roles": {
+ "items": [
+ {
+ "const": "studio",
+ "type": "string"
+ },
+ {
+ "const": "controller",
+ "type": "string"
+ },
+ {
+ "const": "runtime",
+ "type": "string"
+ }
+ ],
+ "maxItems": 3,
+ "minItems": 3,
+ "type": "array"
+ },
+ "schemaVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "sidecars": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "path": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "path",
+ "bytes",
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "maxItems": 0,
+ "type": "array"
+ },
+ "target": {
+ "enum": [
+ "windows-x64",
+ "macos-arm64",
+ "macos-x64"
+ ],
+ "type": "string"
+ },
+ "themes": {
+ "additionalProperties": false,
+ "properties": {
+ "builtins": {
+ "items": {
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "catalogSchemaVersion": {
+ "const": 1,
+ "type": "number"
+ }
+ },
+ "required": [
+ "catalogSchemaVersion",
+ "builtins"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z.-]+)?$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "product",
+ "version",
+ "target",
+ "roles",
+ "host",
+ "contracts",
+ "cdpAdapter",
+ "themes",
+ "image",
+ "sidecars",
+ "files"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "runtimeState": {
+ "$ref": "#/definitions/runtimeState",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "runtimeState": {
+ "additionalProperties": false,
+ "properties": {
+ "adapter": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "version": {
+ "const": 1,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "cdp": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "const": "127.0.0.1",
+ "type": "string"
+ },
+ "port": {
+ "maximum": 65535,
+ "minimum": 1,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "host",
+ "port"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "codex": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "executablePath": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "packageRoot": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "packageVersion": {
+ "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$",
+ "type": "string"
+ },
+ "rootPid": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "startedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "rootPid",
+ "startedAt",
+ "executablePath",
+ "packageRoot",
+ "packageVersion"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "createdAt": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "pendingOperation": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "candidateTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "kind": {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ "previousAppliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "previousSelectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "previousStatus": {
+ "anyOf": [
+ {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "startedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "requestId",
+ "startedAt",
+ "previousStatus",
+ "previousSelectedTheme",
+ "previousAppliedTheme",
+ "candidateTheme"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "recentRequests": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "enum": [
+ "launch",
+ "status",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ "completedAt": {
+ "format": "date-time",
+ "type": "string"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "response": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "result": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "anyOf": [
+ {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ {
+ "const": "stopped",
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "result"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "error": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "CODEX_NOT_INSTALLED",
+ "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP",
+ "CODEX_IDENTITY_INVALID",
+ "CODEX_ALREADY_RUNNING_UNMANAGED",
+ "CODEX_LAUNCH_FAILED",
+ "CODEX_WINDOW_ACTIVATION_FAILED",
+ "PROCESS_INSPECTION_DENIED",
+ "CDP_NOT_READY",
+ "CDP_ENDPOINT_UNSAFE",
+ "CDP_PROCESS_MISMATCH",
+ "CDP_TARGET_NOT_FOUND",
+ "CDP_TARGET_AMBIGUOUS",
+ "ADAPTER_INCOMPATIBLE",
+ "THEME_APPLY_FAILED",
+ "THEME_VERIFY_FAILED",
+ "THEME_CLEANUP_FAILED",
+ "THEME_SWITCH_FAILED",
+ "THEME_ROLLBACK_FAILED",
+ "THEME_RUNTIME_TOO_LARGE",
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_HOME_WELCOME_UNSUPPORTED",
+ "THEME_REQUIRED_LAYER_UNRESOLVED",
+ "THEME_NOT_FOUND",
+ "THEME_NOT_READY",
+ "RUNTIME_SESSION_STALE",
+ "RUNTIME_INVALID_STATE",
+ "RUNTIME_BUSY",
+ "RUNTIME_ENVIRONMENT_INVALID",
+ "RUNTIME_CONTROL_UNAVAILABLE",
+ "RESTORE_AWAITING_EXIT",
+ "PROBE_EXIT_PENDING",
+ "PROBE_FINALIZE_REQUIRED",
+ "PROBE_PENDING_SESSION_INVALID",
+ "INTERNAL"
+ ],
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "ok": {
+ "const": false,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "error"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "required": [
+ "requestId",
+ "command",
+ "response",
+ "completedAt"
+ ],
+ "type": "object"
+ },
+ "maxItems": 32,
+ "type": "array"
+ },
+ "runtime": {
+ "additionalProperties": false,
+ "properties": {
+ "pid": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ },
+ "startedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "pid",
+ "startedAt"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "selectedTheme": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ "sessionId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "sessionId",
+ "status",
+ "runtime",
+ "codex",
+ "cdp",
+ "adapter",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "pendingOperation",
+ "recentRequests",
+ "createdAt",
+ "updatedAt"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "themeRef": {
+ "$ref": "#/definitions/themeRef",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "themeRef": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "themeStore": {
+ "$ref": "#/definitions/themeStore",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "themeStore": {
+ "additionalProperties": false,
+ "properties": {
+ "active": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "previous": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "active",
+ "previous",
+ "updatedAt"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "trustedInstall": {
+ "$ref": "#/definitions/trustedInstall",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "trustedInstall": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "appId": {
+ "const": "App",
+ "type": "string"
+ },
+ "catalogSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "catalogSignerCommonName": {
+ "const": "50BDFD77-8903-4850-9FFE-6E8522F64D5B",
+ "type": "string"
+ },
+ "entryBlockMapValid": {
+ "const": true,
+ "type": "boolean"
+ },
+ "entryPath": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "entryPoint": {
+ "const": "Windows.FullTrustApplication",
+ "type": "string"
+ },
+ "entryRelativePath": {
+ "const": "app/ChatGPT.exe",
+ "type": "string"
+ },
+ "identityName": {
+ "const": "OpenAI.Codex",
+ "type": "string"
+ },
+ "packagePublisher": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "packageRoot": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "packageSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "packageSignerCommonName": {
+ "const": "50BDFD77-8903-4850-9FFE-6E8522F64D5B",
+ "type": "string"
+ },
+ "packageVersion": {
+ "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$",
+ "type": "string"
+ },
+ "resourceSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "resourceSignerCommonName": {
+ "const": "OpenAI OpCo, LLC",
+ "type": "string"
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "verifiedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "packageRoot",
+ "entryPath",
+ "identityName",
+ "packageVersion",
+ "packagePublisher",
+ "appId",
+ "entryRelativePath",
+ "entryPoint",
+ "packageSignatureStatus",
+ "packageSignerCommonName",
+ "catalogSignatureStatus",
+ "catalogSignerCommonName",
+ "entryBlockMapValid",
+ "resourceSignatureStatus",
+ "resourceSignerCommonName",
+ "verifiedAt"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "appId": {
+ "const": "com.openai.codex",
+ "type": "string"
+ },
+ "catalogSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "catalogSignerCommonName": {
+ "const": "Notarized Developer ID",
+ "type": "string"
+ },
+ "entryBlockMapValid": {
+ "const": true,
+ "type": "boolean"
+ },
+ "entryPath": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "entryPoint": {
+ "const": "macOS.Application",
+ "type": "string"
+ },
+ "entryRelativePath": {
+ "pattern": "^Contents\\/MacOS\\/[^/]+$",
+ "type": "string"
+ },
+ "identityName": {
+ "const": "OpenAI.Codex",
+ "type": "string"
+ },
+ "packagePublisher": {
+ "const": "2DC432GLL2",
+ "type": "string"
+ },
+ "packageRoot": {
+ "pattern": "\\/Codex\\.app$",
+ "type": "string"
+ },
+ "packageSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "packageSignerCommonName": {
+ "const": "2DC432GLL2",
+ "type": "string"
+ },
+ "packageVersion": {
+ "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$",
+ "type": "string"
+ },
+ "resourceSignatureStatus": {
+ "const": "Valid",
+ "type": "string"
+ },
+ "resourceSignerCommonName": {
+ "const": "OpenAI, L.L.C.",
+ "type": "string"
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "verifiedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "packageRoot",
+ "entryPath",
+ "identityName",
+ "packageVersion",
+ "packagePublisher",
+ "appId",
+ "entryRelativePath",
+ "entryPoint",
+ "packageSignatureStatus",
+ "packageSignerCommonName",
+ "catalogSignatureStatus",
+ "catalogSignerCommonName",
+ "entryBlockMapValid",
+ "resourceSignatureStatus",
+ "resourceSignerCommonName",
+ "verifiedAt"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ }
+ }
+ },
+ "schemaVersion": 1
+}
diff --git a/contracts/runtime/v1/cases/semantics.json b/contracts/runtime/v1/cases/semantics.json
new file mode 100644
index 0000000..ad4324d
--- /dev/null
+++ b/contracts/runtime/v1/cases/semantics.json
@@ -0,0 +1,127 @@
+{
+ "cases": [
+ {
+ "command": "status",
+ "expectedConcurrency": "parallel-with-mutation",
+ "name": "status during mutation",
+ "valid": true
+ },
+ {
+ "command": "switch",
+ "expectedBehavior": "replay-stored-response",
+ "name": "duplicate request replay",
+ "valid": true
+ },
+ {
+ "command": "pause",
+ "expectedErrorCode": "RUNTIME_INVALID_STATE",
+ "expectedPath": "/requestId",
+ "name": "request ID reused for another command",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "RUNTIME_CONTROL_UNAVAILABLE",
+ "expectedPath": "/frame/length",
+ "name": "oversized frame",
+ "valid": false
+ },
+ {
+ "command": "restore",
+ "expectedBehavior": "after-response",
+ "name": "restore callback after response",
+ "valid": true
+ },
+ {
+ "command": "launch",
+ "expectedErrorCode": "THEME_NOT_FOUND",
+ "expectedPath": "/params/themeVersion",
+ "name": "personal theme without version",
+ "valid": false
+ }
+ ],
+ "protocolVersion": 1,
+ "recoveryCases": [
+ {
+ "expectedStatus": "active",
+ "name": "stable active appearance",
+ "sourceStatus": "active",
+ "valid": true
+ },
+ {
+ "expectedStatus": "paused",
+ "name": "stable paused appearance",
+ "sourceStatus": "paused",
+ "valid": true
+ },
+ {
+ "expectedStatus": "restored-awaiting-exit",
+ "name": "interrupted launch restores official appearance",
+ "operation": "launch",
+ "valid": true
+ },
+ {
+ "expectedStatus": "restored-awaiting-exit",
+ "name": "interrupted restore retries official appearance",
+ "operation": "restore",
+ "valid": true
+ },
+ {
+ "cleanupSucceeded": false,
+ "expectedStatus": "recovery-required",
+ "name": "failed cleanup marks appearance unknown",
+ "valid": true
+ },
+ {
+ "expectedErrorCode": "RUNTIME_SESSION_STALE",
+ "expectedPath": "/codex/startedAt",
+ "name": "managed process identity changed",
+ "valid": false
+ }
+ ],
+ "stateTransitions": {
+ "active": [
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring"
+ ],
+ "launching": [
+ "launching",
+ "active",
+ "recovery-required",
+ "restored-awaiting-exit"
+ ],
+ "paused": [
+ "paused",
+ "active",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring"
+ ],
+ "paused-incompatible": [
+ "paused-incompatible",
+ "paused",
+ "active",
+ "recovery-required",
+ "restoring"
+ ],
+ "recovery-required": [
+ "recovery-required",
+ "restoring"
+ ],
+ "restored-awaiting-exit": [
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "restored-cleanup-required": [
+ "restored-cleanup-required"
+ ],
+ "restoring": [
+ "restoring",
+ "active",
+ "recovery-required",
+ "restored-awaiting-exit"
+ ]
+ }
+}
diff --git a/contracts/runtime/v1/frames.json b/contracts/runtime/v1/frames.json
new file mode 100644
index 0000000..3c5c6b9
--- /dev/null
+++ b/contracts/runtime/v1/frames.json
@@ -0,0 +1,66 @@
+{
+ "afterResponseCommands": [
+ "launch",
+ "restore"
+ ],
+ "commands": [
+ "launch",
+ "status",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "errors": [
+ "CODEX_NOT_INSTALLED",
+ "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP",
+ "CODEX_IDENTITY_INVALID",
+ "CODEX_ALREADY_RUNNING_UNMANAGED",
+ "CODEX_LAUNCH_FAILED",
+ "CODEX_WINDOW_ACTIVATION_FAILED",
+ "PROCESS_INSPECTION_DENIED",
+ "CDP_NOT_READY",
+ "CDP_ENDPOINT_UNSAFE",
+ "CDP_PROCESS_MISMATCH",
+ "CDP_TARGET_NOT_FOUND",
+ "CDP_TARGET_AMBIGUOUS",
+ "ADAPTER_INCOMPATIBLE",
+ "THEME_APPLY_FAILED",
+ "THEME_VERIFY_FAILED",
+ "THEME_CLEANUP_FAILED",
+ "THEME_SWITCH_FAILED",
+ "THEME_ROLLBACK_FAILED",
+ "THEME_RUNTIME_TOO_LARGE",
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_HOME_WELCOME_UNSUPPORTED",
+ "THEME_REQUIRED_LAYER_UNRESOLVED",
+ "THEME_NOT_FOUND",
+ "THEME_NOT_READY",
+ "RUNTIME_SESSION_STALE",
+ "RUNTIME_INVALID_STATE",
+ "RUNTIME_BUSY",
+ "RUNTIME_ENVIRONMENT_INVALID",
+ "RUNTIME_CONTROL_UNAVAILABLE",
+ "RESTORE_AWAITING_EXIT",
+ "PROBE_EXIT_PENDING",
+ "PROBE_FINALIZE_REQUIRED",
+ "PROBE_PENDING_SESSION_INVALID",
+ "INTERNAL"
+ ],
+ "frame": {
+ "byteOrder": "little-endian",
+ "encoding": "json",
+ "lengthPrefixBytes": 4,
+ "maxPayloadBytes": 65536
+ },
+ "mutationConcurrency": "serialized",
+ "protocolVersion": 1,
+ "readOnlyCommands": [
+ "status"
+ ],
+ "requestIdSemantics": "same-id-same-command-replays; same-id-different-command-rejected",
+ "statusConcurrency": "parallel-with-mutation"
+}
diff --git a/contracts/runtime/v1/schemas/index.json b/contracts/runtime/v1/schemas/index.json
new file mode 100644
index 0000000..36576c1
--- /dev/null
+++ b/contracts/runtime/v1/schemas/index.json
@@ -0,0 +1,673 @@
+{
+ "schemas": {
+ "error": {
+ "$ref": "#/definitions/error",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "error": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "CODEX_NOT_INSTALLED",
+ "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP",
+ "CODEX_IDENTITY_INVALID",
+ "CODEX_ALREADY_RUNNING_UNMANAGED",
+ "CODEX_LAUNCH_FAILED",
+ "CODEX_WINDOW_ACTIVATION_FAILED",
+ "PROCESS_INSPECTION_DENIED",
+ "CDP_NOT_READY",
+ "CDP_ENDPOINT_UNSAFE",
+ "CDP_PROCESS_MISMATCH",
+ "CDP_TARGET_NOT_FOUND",
+ "CDP_TARGET_AMBIGUOUS",
+ "ADAPTER_INCOMPATIBLE",
+ "THEME_APPLY_FAILED",
+ "THEME_VERIFY_FAILED",
+ "THEME_CLEANUP_FAILED",
+ "THEME_SWITCH_FAILED",
+ "THEME_ROLLBACK_FAILED",
+ "THEME_RUNTIME_TOO_LARGE",
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_HOME_WELCOME_UNSUPPORTED",
+ "THEME_REQUIRED_LAYER_UNRESOLVED",
+ "THEME_NOT_FOUND",
+ "THEME_NOT_READY",
+ "RUNTIME_SESSION_STALE",
+ "RUNTIME_INVALID_STATE",
+ "RUNTIME_BUSY",
+ "RUNTIME_ENVIRONMENT_INVALID",
+ "RUNTIME_CONTROL_UNAVAILABLE",
+ "RESTORE_AWAITING_EXIT",
+ "PROBE_EXIT_PENDING",
+ "PROBE_FINALIZE_REQUIRED",
+ "PROBE_PENDING_SESSION_INVALID",
+ "INTERNAL"
+ ],
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "request": {
+ "$ref": "#/definitions/request",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "request": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "launch",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {
+ "themeId": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "themeVersion": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "themeId"
+ ],
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "status",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "switch",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {
+ "themeId": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "themeVersion": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "themeId"
+ ],
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "pause",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "resume",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "command": {
+ "const": "restore",
+ "type": "string"
+ },
+ "params": {
+ "additionalProperties": false,
+ "properties": {},
+ "type": "object"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "command",
+ "params"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ }
+ },
+ "response": {
+ "$ref": "#/definitions/response",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "response": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "result": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "anyOf": [
+ {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ {
+ "const": "stopped",
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "result"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "error": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "enum": [
+ "CODEX_NOT_INSTALLED",
+ "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP",
+ "CODEX_IDENTITY_INVALID",
+ "CODEX_ALREADY_RUNNING_UNMANAGED",
+ "CODEX_LAUNCH_FAILED",
+ "CODEX_WINDOW_ACTIVATION_FAILED",
+ "PROCESS_INSPECTION_DENIED",
+ "CDP_NOT_READY",
+ "CDP_ENDPOINT_UNSAFE",
+ "CDP_PROCESS_MISMATCH",
+ "CDP_TARGET_NOT_FOUND",
+ "CDP_TARGET_AMBIGUOUS",
+ "ADAPTER_INCOMPATIBLE",
+ "THEME_APPLY_FAILED",
+ "THEME_VERIFY_FAILED",
+ "THEME_CLEANUP_FAILED",
+ "THEME_SWITCH_FAILED",
+ "THEME_ROLLBACK_FAILED",
+ "THEME_RUNTIME_TOO_LARGE",
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_HOME_WELCOME_UNSUPPORTED",
+ "THEME_REQUIRED_LAYER_UNRESOLVED",
+ "THEME_NOT_FOUND",
+ "THEME_NOT_READY",
+ "RUNTIME_SESSION_STALE",
+ "RUNTIME_INVALID_STATE",
+ "RUNTIME_BUSY",
+ "RUNTIME_ENVIRONMENT_INVALID",
+ "RUNTIME_CONTROL_UNAVAILABLE",
+ "RESTORE_AWAITING_EXIT",
+ "PROBE_EXIT_PENDING",
+ "PROBE_FINALIZE_REQUIRED",
+ "PROBE_PENDING_SESSION_INVALID",
+ "INTERNAL"
+ ],
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "ok": {
+ "const": false,
+ "type": "boolean"
+ },
+ "protocolVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "requestId": {
+ "format": "uuid",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "requestId",
+ "ok",
+ "error"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ }
+ },
+ "status": {
+ "$ref": "#/definitions/status",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "status": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "anyOf": [
+ {
+ "enum": [
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ },
+ {
+ "const": "stopped",
+ "type": "string"
+ }
+ ]
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "schemaVersion": 1
+}
diff --git a/contracts/studio/v2/cases/http.json b/contracts/studio/v2/cases/http.json
new file mode 100644
index 0000000..8a29dfb
--- /dev/null
+++ b/contracts/studio/v2/cases/http.json
@@ -0,0 +1,48 @@
+{
+ "cases": [
+ {
+ "expectedStatus": 200,
+ "name": "authenticated bootstrap",
+ "valid": true
+ },
+ {
+ "expectedStatus": 204,
+ "name": "one-time session exchange",
+ "valid": true
+ },
+ {
+ "expectedErrorCode": "STUDIO_SESSION_INVALID",
+ "expectedPath": "/api/session",
+ "expectedStatus": 401,
+ "name": "reused bootstrap token",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "STUDIO_ORIGIN_REJECTED",
+ "expectedPath": "/headers/origin",
+ "expectedStatus": 403,
+ "name": "cross-origin mutation",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "STUDIO_REQUEST_TOO_LARGE",
+ "expectedPath": "/body",
+ "expectedStatus": 413,
+ "name": "oversized binary upload",
+ "valid": false
+ },
+ {
+ "expectedStatus": 200,
+ "name": "binary export",
+ "responseKind": "binary",
+ "valid": true
+ },
+ {
+ "expectedStatus": 200,
+ "name": "runtime status stream",
+ "responseKind": "sse",
+ "valid": true
+ }
+ ],
+ "protocolVersion": 2
+}
diff --git a/contracts/studio/v2/routes.json b/contracts/studio/v2/routes.json
new file mode 100644
index 0000000..da268d7
--- /dev/null
+++ b/contracts/studio/v2/routes.json
@@ -0,0 +1,253 @@
+{
+ "bodyLimits": {
+ "archiveBytes": 33554432,
+ "imageBytes": 52428800,
+ "jsonBytes": 262144,
+ "sessionJsonBytes": 16384
+ },
+ "errorStatus": {
+ "INTERNAL": 500,
+ "RUNTIME_STATUS_UNAVAILABLE": 503,
+ "STUDIO_APPLY_FAILED": 409,
+ "STUDIO_ASSET_INVALID": 422,
+ "STUDIO_DELETE_FAILED": 422,
+ "STUDIO_DRAFT_CONFLICT": 409,
+ "STUDIO_DRAFT_INVALID": 422,
+ "STUDIO_DRAFT_NOT_FOUND": 404,
+ "STUDIO_EXPORT_INVALID": 422,
+ "STUDIO_IMPORT_INVALID": 422,
+ "STUDIO_ORIGIN_REJECTED": 403,
+ "STUDIO_REQUEST_INVALID": 400,
+ "STUDIO_REQUEST_TOO_LARGE": 413,
+ "STUDIO_SAVE_FAILED": 422,
+ "STUDIO_SESSION_INVALID": 401
+ },
+ "protocolVersion": 2,
+ "responsePolicies": {
+ "binary": {
+ "cacheControl": "no-store",
+ "contentType": "route-defined",
+ "contentTypeOptions": "nosniff"
+ },
+ "html": {
+ "cacheControl": "no-store",
+ "contentType": "text/html; charset=utf-8",
+ "contentTypeOptions": "nosniff"
+ },
+ "json": {
+ "cacheControl": "no-store",
+ "contentType": "application/json; charset=utf-8",
+ "contentTypeOptions": "nosniff"
+ },
+ "sse": {
+ "cacheControl": "no-store",
+ "contentType": "text/event-stream; charset=utf-8",
+ "contentTypeOptions": "nosniff"
+ }
+ },
+ "routes": [
+ {
+ "id": "home",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/",
+ "response": "html",
+ "successStatus": 200
+ },
+ {
+ "id": "session",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/session",
+ "request": "sessionExchange",
+ "response": "none",
+ "successStatus": 204
+ },
+ {
+ "id": "bootstrap",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/bootstrap",
+ "response": "bootstrap",
+ "successStatus": 200
+ },
+ {
+ "id": "themes",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/themes",
+ "response": "themeLibrary",
+ "successStatus": 200
+ },
+ {
+ "id": "applySavedTheme",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/themes/apply",
+ "request": "themeRef",
+ "response": "applyResult",
+ "successStatus": 200
+ },
+ {
+ "id": "deleteTheme",
+ "method": "DELETE",
+ "originRequired": true,
+ "path": "/api/themes/{id}",
+ "request": "deleteTheme",
+ "response": "themeLibrary",
+ "successStatus": 200
+ },
+ {
+ "id": "createDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts",
+ "request": "createDraft",
+ "response": "draft",
+ "successStatus": 201
+ },
+ {
+ "id": "latestDraft",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/drafts/latest",
+ "response": "nullableDraft",
+ "successStatus": 200
+ },
+ {
+ "id": "draft",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/drafts/{draftId}",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "updateDraft",
+ "method": "PUT",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}",
+ "request": "updateDraft",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "undoDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/undo",
+ "request": "draftCommand",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "redoDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/redo",
+ "request": "draftCommand",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "validateDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/validate",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "saveDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/save",
+ "request": "draftCommand",
+ "response": "saveResult",
+ "successStatus": 200
+ },
+ {
+ "id": "applyDraft",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/apply",
+ "request": "draftCommand",
+ "response": "applyResult",
+ "successStatus": 200
+ },
+ {
+ "id": "uploadAsset",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/drafts/{draftId}/assets",
+ "request": "binaryAsset",
+ "response": "draft",
+ "successStatus": 200
+ },
+ {
+ "id": "importTheme",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/import",
+ "request": "binaryArchive",
+ "response": "draft",
+ "successStatus": 201
+ },
+ {
+ "id": "exportTheme",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/export",
+ "request": "themeRefQuery",
+ "response": "binaryArchive",
+ "successStatus": 200
+ },
+ {
+ "id": "runtime",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/runtime",
+ "response": "runtimeStatus",
+ "successStatus": 200
+ },
+ {
+ "id": "restoreRuntime",
+ "method": "POST",
+ "originRequired": true,
+ "path": "/api/runtime/restore",
+ "response": "runtimeStatus",
+ "successStatus": 200
+ },
+ {
+ "id": "draftAsset",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/draft-asset",
+ "response": "binaryAsset",
+ "successStatus": 200
+ },
+ {
+ "id": "themePreview",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/theme-preview",
+ "response": "binaryImage",
+ "successStatus": 200
+ },
+ {
+ "id": "events",
+ "method": "GET",
+ "originRequired": false,
+ "path": "/api/events",
+ "response": "sseRuntimeStatus",
+ "successStatus": 200
+ }
+ ],
+ "securityHeaders": [
+ "Content-Security-Policy",
+ "Cross-Origin-Opener-Policy",
+ "Referrer-Policy",
+ "X-Content-Type-Options",
+ "X-Frame-Options"
+ ]
+}
diff --git a/contracts/studio/v2/schemas/index.json b/contracts/studio/v2/schemas/index.json
new file mode 100644
index 0000000..1b89849
--- /dev/null
+++ b/contracts/studio/v2/schemas/index.json
@@ -0,0 +1,5042 @@
+{
+ "schemas": {
+ "applyResult": {
+ "$ref": "#/definitions/applyResult",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "applyResult": {
+ "additionalProperties": false,
+ "properties": {
+ "draft": {
+ "additionalProperties": false,
+ "properties": {
+ "assetUrls": {
+ "additionalProperties": {
+ "pattern": "^\\/api\\/",
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "dirty": {
+ "type": "boolean"
+ },
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "issues": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "pattern": "^[A-Z0-9_]+$",
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "path": {
+ "maxLength": 240,
+ "type": "string"
+ },
+ "severity": {
+ "enum": [
+ "error",
+ "warning"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "path",
+ "message",
+ "severity"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "redoAvailable": {
+ "type": "boolean"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "savedRef": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "theme": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "undoAvailable": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "draftId",
+ "theme",
+ "revision",
+ "updatedAt",
+ "savedRef",
+ "dirty",
+ "undoAvailable",
+ "redoAvailable",
+ "issues",
+ "assetUrls"
+ ],
+ "type": "object"
+ },
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ "runtime": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "enum": [
+ "stopped",
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "draft",
+ "ref",
+ "runtime"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "bootstrap": {
+ "$ref": "#/definitions/bootstrap",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "bootstrap": {
+ "additionalProperties": false,
+ "properties": {
+ "capabilities": {
+ "items": {
+ "enum": [
+ "studio-shell",
+ "theme-library",
+ "draft-editing",
+ "asset-upload",
+ "version-save",
+ "theme-import-export",
+ "theme-delete",
+ "runtime-apply",
+ "runtime-restore"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "protocolVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "repositoryUrl": {
+ "anyOf": [
+ {
+ "format": "uri",
+ "pattern": "^https\\:\\/\\/github\\.com\\/",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null
+ },
+ "runtime": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "enum": [
+ "stopped",
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ },
+ "studioVersion": {
+ "pattern": "^\\d+\\.\\d+\\.\\d+(?:-(?:alpha|beta|rc)\\.\\d+)?$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "studioVersion",
+ "capabilities",
+ "runtime"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "createDraft": {
+ "$ref": "#/definitions/createDraft",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "createDraft": {
+ "additionalProperties": false,
+ "properties": {
+ "conflictResolution": {
+ "enum": [
+ "load-existing",
+ "overwrite-existing"
+ ],
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "source": {
+ "additionalProperties": false,
+ "properties": {
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "enum": [
+ "builtin",
+ "personal",
+ "recipe"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "source",
+ "ref"
+ ],
+ "type": "object"
+ },
+ "themeId": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "deleteTheme": {
+ "$ref": "#/definitions/deleteTheme",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "deleteTheme": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "draft": {
+ "$ref": "#/definitions/draft",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "draft": {
+ "additionalProperties": false,
+ "properties": {
+ "assetUrls": {
+ "additionalProperties": {
+ "pattern": "^\\/api\\/",
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "dirty": {
+ "type": "boolean"
+ },
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "issues": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "pattern": "^[A-Z0-9_]+$",
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "path": {
+ "maxLength": 240,
+ "type": "string"
+ },
+ "severity": {
+ "enum": [
+ "error",
+ "warning"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "path",
+ "message",
+ "severity"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "redoAvailable": {
+ "type": "boolean"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "savedRef": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "theme": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "undoAvailable": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "draftId",
+ "theme",
+ "revision",
+ "updatedAt",
+ "savedRef",
+ "dirty",
+ "undoAvailable",
+ "redoAvailable",
+ "issues",
+ "assetUrls"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "draftCommand": {
+ "$ref": "#/definitions/draftCommand",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "draftCommand": {
+ "additionalProperties": false,
+ "properties": {
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "expectedRevision": {
+ "minimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "draftId",
+ "expectedRevision"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "event": {
+ "$ref": "#/definitions/event",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "event": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "runtime-status",
+ "type": "string"
+ },
+ "protocolVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "runtime": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "enum": [
+ "stopped",
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ },
+ "sequence": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "sequence",
+ "kind",
+ "runtime"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "kind": {
+ "const": "draft-updated",
+ "type": "string"
+ },
+ "protocolVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "sequence": {
+ "exclusiveMinimum": 0,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "protocolVersion",
+ "sequence",
+ "kind",
+ "draftId",
+ "revision"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ }
+ },
+ "exportedTheme": {
+ "$ref": "#/definitions/exportedTheme",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "exportedTheme": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {},
+ "fileName": {
+ "pattern": "^[a-z0-9-]+-\\d+\\.\\d+\\.\\d+\\.ocskin$",
+ "type": "string"
+ },
+ "mimeType": {
+ "const": "application/vnd.open-chatgpt-skin+zip",
+ "type": "string"
+ }
+ },
+ "required": [
+ "fileName",
+ "mimeType",
+ "bytes"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "importTheme": {
+ "$ref": "#/definitions/importTheme",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "importTheme": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {},
+ "fileName": {
+ "maxLength": 160,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "fileName",
+ "bytes"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "runtimeStatus": {
+ "$ref": "#/definitions/runtimeStatus",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "runtimeStatus": {
+ "additionalProperties": false,
+ "properties": {
+ "appliedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "controllerAvailable": {
+ "type": "boolean"
+ },
+ "nextAction": {
+ "maxLength": 500,
+ "type": "string"
+ },
+ "operation": {
+ "anyOf": [
+ {
+ "enum": [
+ "launch",
+ "switch",
+ "pause",
+ "resume",
+ "restore"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "packageVersion": {
+ "anyOf": [
+ {
+ "maxLength": 40,
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "selectedTheme": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "skinApplied": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "status": {
+ "enum": [
+ "stopped",
+ "launching",
+ "active",
+ "paused",
+ "paused-incompatible",
+ "recovery-required",
+ "restoring",
+ "restored-awaiting-exit",
+ "restored-cleanup-required"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "status",
+ "controllerAvailable",
+ "selectedTheme",
+ "appliedTheme",
+ "skinApplied",
+ "packageVersion",
+ "operation",
+ "nextAction"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "saveResult": {
+ "$ref": "#/definitions/saveResult",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "saveResult": {
+ "additionalProperties": false,
+ "properties": {
+ "draft": {
+ "additionalProperties": false,
+ "properties": {
+ "assetUrls": {
+ "additionalProperties": {
+ "pattern": "^\\/api\\/",
+ "type": "string"
+ },
+ "type": "object"
+ },
+ "dirty": {
+ "type": "boolean"
+ },
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "issues": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "pattern": "^[A-Z0-9_]+$",
+ "type": "string"
+ },
+ "message": {
+ "maxLength": 500,
+ "minLength": 1,
+ "type": "string"
+ },
+ "path": {
+ "maxLength": 240,
+ "type": "string"
+ },
+ "severity": {
+ "enum": [
+ "error",
+ "warning"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "path",
+ "message",
+ "severity"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "redoAvailable": {
+ "type": "boolean"
+ },
+ "revision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "savedRef": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "theme": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ },
+ "undoAvailable": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "format": "date-time",
+ "type": "string"
+ }
+ },
+ "required": [
+ "draftId",
+ "theme",
+ "revision",
+ "updatedAt",
+ "savedRef",
+ "dirty",
+ "undoAvailable",
+ "redoAvailable",
+ "issues",
+ "assetUrls"
+ ],
+ "type": "object"
+ },
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "draft",
+ "ref"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "sessionExchange": {
+ "$ref": "#/definitions/sessionExchange",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "sessionExchange": {
+ "additionalProperties": false,
+ "properties": {
+ "token": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "token"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "themeLibrary": {
+ "$ref": "#/definitions/themeLibrary",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "themeLibrary": {
+ "additionalProperties": false,
+ "properties": {
+ "themes": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "description": {
+ "maxLength": 240,
+ "type": "string"
+ },
+ "homepage": {
+ "anyOf": [
+ {
+ "format": "uri",
+ "pattern": "^https\\:\\/\\/",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "previewUrl": {
+ "anyOf": [
+ {
+ "pattern": "^\\/api\\/",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "ready": {
+ "type": "boolean"
+ },
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "enum": [
+ "builtin",
+ "personal",
+ "recipe"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "ref",
+ "name",
+ "author",
+ "homepage",
+ "source",
+ "ready",
+ "localOnly",
+ "previewUrl"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "themes"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "themeRef": {
+ "$ref": "#/definitions/themeRef",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "themeRef": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "version"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "updateDraft": {
+ "$ref": "#/definitions/updateDraft",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "updateDraft": {
+ "additionalProperties": false,
+ "properties": {
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "expectedRevision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "theme": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "draftId",
+ "expectedRevision",
+ "theme"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "uploadAsset": {
+ "$ref": "#/definitions/uploadAsset",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "uploadAsset": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "bytes": {},
+ "draftId": {
+ "format": "uuid",
+ "type": "string"
+ },
+ "expectedRevision": {
+ "minimum": 0,
+ "type": "integer"
+ },
+ "fileName": {
+ "maxLength": 160,
+ "minLength": 1,
+ "type": "string"
+ },
+ "mimeType": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "slot": {
+ "enum": [
+ "background",
+ "portrait",
+ "decoration",
+ "ui-font",
+ "code-font",
+ "display-font",
+ "composition-layer",
+ "profile-avatar",
+ "suggestion-card1",
+ "suggestion-card2",
+ "suggestion-card3",
+ "suggestion-card4",
+ "project-icon1",
+ "project-icon2",
+ "project-icon3",
+ "project-icon4"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "draftId",
+ "expectedRevision",
+ "slot",
+ "fileName",
+ "mimeType",
+ "bytes"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "schemaVersion": 2
+}
diff --git a/contracts/theme/v4/archive-cases.json b/contracts/theme/v4/archive-cases.json
new file mode 100644
index 0000000..0c9a5d6
--- /dev/null
+++ b/contracts/theme/v4/archive-cases.json
@@ -0,0 +1,109 @@
+{
+ "cases": [
+ {
+ "name": "valid archive",
+ "valid": true
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_ENTRY_UNSAFE",
+ "expectedPath": "/entries/../secret",
+ "name": "unsafe traversal entry",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_ENTRY_DUPLICATE",
+ "expectedPath": "/entries",
+ "name": "case-folded duplicate entry",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_ENTRY_UNSUPPORTED",
+ "expectedPath": "/entries/run.js",
+ "name": "unsupported entry",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "PACKAGE_EXPANDED_TOO_LARGE",
+ "expectedPath": "/expandedBytes",
+ "name": "expanded size limit",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_REQUIRED_FILE_MISSING",
+ "expectedPath": "/entries/theme.json",
+ "name": "missing required files",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_MANIFEST_MISMATCH",
+ "expectedPath": "/manifest/files",
+ "name": "manifest mismatch",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "ARCHIVE_HASH_MISMATCH",
+ "expectedPath": "/manifest/files/sha256",
+ "name": "hash mismatch",
+ "valid": false
+ }
+ ],
+ "limits": {
+ "archiveBytes": 33554432,
+ "expandedBytes": 33554432
+ },
+ "manifestSchema": {
+ "$ref": "#/definitions/OcskinManifest",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "OcskinManifest": {
+ "additionalProperties": false,
+ "properties": {
+ "files": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "bytes": {
+ "maximum": 33554432,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "sha256": {
+ "pattern": "^[0-9a-f]{64}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "bytes",
+ "sha256"
+ ],
+ "type": "object"
+ },
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "themeId": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "themeVersion": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "themeId",
+ "themeVersion",
+ "files"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "schemaVersion": 1
+}
diff --git a/contracts/theme/v4/draft-schema.json b/contracts/theme/v4/draft-schema.json
new file mode 100644
index 0000000..e298293
--- /dev/null
+++ b/contracts/theme/v4/draft-schema.json
@@ -0,0 +1,902 @@
+{
+ "schema": {
+ "$ref": "#/definitions/ThemeDraftDocument",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDraftDocument": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "schemaVersion": 4
+}
diff --git a/contracts/theme/v4/migrations.json b/contracts/theme/v4/migrations.json
new file mode 100644
index 0000000..cfd2f45
--- /dev/null
+++ b/contracts/theme/v4/migrations.json
@@ -0,0 +1,2696 @@
+{
+ "defaults": {
+ "appearance": "auto",
+ "background": {
+ "safeArea": "auto",
+ "taskMode": "full",
+ "taskOpacity": 0.82
+ },
+ "composition": {
+ "layers": []
+ },
+ "interfaceImages": {
+ "profileAvatarSize": 24,
+ "projectIconSize": 16,
+ "suggestionIconSize": 20
+ },
+ "surfaces": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ }
+ },
+ "sources": [
+ {
+ "addsSemanticColors": true,
+ "addsV4Defaults": true,
+ "version": 1
+ },
+ {
+ "addsSemanticColors": false,
+ "addsV4Defaults": true,
+ "version": 2
+ },
+ {
+ "addsSemanticColors": false,
+ "addsV4Defaults": true,
+ "version": 3
+ },
+ {
+ "addsSemanticColors": false,
+ "addsV4Defaults": true,
+ "version": 4
+ }
+ ],
+ "sourceSchemas": {
+ "v1": {
+ "$ref": "#/definitions/ThemeDocumentV1",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDocumentV1": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 1,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "v2": {
+ "$ref": "#/definitions/ThemeDocumentV2",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDocumentV2": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 2,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "v3": {
+ "$ref": "#/definitions/ThemeDocumentV3",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDocumentV3": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 3,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "v4": {
+ "$ref": "#/definitions/ThemeDocumentV4Input",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDocumentV4Input": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "targetVersion": 4
+}
diff --git a/contracts/theme/v4/schema.json b/contracts/theme/v4/schema.json
new file mode 100644
index 0000000..1139e87
--- /dev/null
+++ b/contracts/theme/v4/schema.json
@@ -0,0 +1,909 @@
+{
+ "errorCodes": [
+ "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "THEME_WELCOME_INVALID",
+ "THEME_DISPLAY_FONT_MISSING",
+ "THEME_COMPOSITION_INVALID",
+ "THEME_SCHEMA_INVALID"
+ ],
+ "schema": {
+ "$ref": "#/definitions/ThemeDocument",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "ThemeDocument": {
+ "additionalProperties": false,
+ "properties": {
+ "appearance": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "light",
+ "dark"
+ ],
+ "type": "string"
+ },
+ "assets": {
+ "additionalProperties": false,
+ "properties": {
+ "background": {
+ "type": "string"
+ },
+ "decorations": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "fonts": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "propertyNames": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
+ },
+ "type": "object"
+ },
+ "portrait": {
+ "type": "string"
+ },
+ "profileAvatar": {
+ "type": "string"
+ },
+ "projectIcons": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 12,
+ "minItems": 1,
+ "type": "array"
+ },
+ "suggestionIcons": {
+ "additionalProperties": false,
+ "properties": {
+ "card1": {
+ "type": "string"
+ },
+ "card2": {
+ "type": "string"
+ },
+ "card3": {
+ "type": "string"
+ },
+ "card4": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "author": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "background": {
+ "additionalProperties": false,
+ "properties": {
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "brightness": {
+ "maximum": 1.5,
+ "minimum": 0.3,
+ "type": "number"
+ },
+ "overlay": {
+ "maximum": 0.9,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "safeArea": {
+ "default": "auto",
+ "enum": [
+ "auto",
+ "left",
+ "center",
+ "right",
+ "none"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "taskMode": {
+ "default": "full",
+ "enum": [
+ "auto",
+ "full",
+ "ambient",
+ "banner",
+ "off"
+ ],
+ "type": "string"
+ },
+ "taskOpacity": {
+ "default": 0.82,
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "positionX",
+ "positionY",
+ "scale",
+ "blur",
+ "brightness",
+ "overlay"
+ ],
+ "type": "object"
+ },
+ "colors": {
+ "additionalProperties": false,
+ "properties": {
+ "accent": {
+ "type": "string"
+ },
+ "border": {
+ "type": "string"
+ },
+ "codeText": {
+ "type": "string"
+ },
+ "danger": {
+ "type": "string"
+ },
+ "info": {
+ "type": "string"
+ },
+ "inputText": {
+ "type": "string"
+ },
+ "link": {
+ "type": "string"
+ },
+ "muted": {
+ "type": "string"
+ },
+ "panel": {
+ "type": "string"
+ },
+ "placeholder": {
+ "type": "string"
+ },
+ "secondary": {
+ "type": "string"
+ },
+ "success": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ },
+ "textSecondary": {
+ "type": "string"
+ },
+ "warning": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "accent",
+ "secondary",
+ "text",
+ "muted",
+ "panel",
+ "border",
+ "success",
+ "warning",
+ "danger",
+ "info",
+ "textSecondary",
+ "link",
+ "inputText",
+ "placeholder",
+ "codeText"
+ ],
+ "type": "object"
+ },
+ "composition": {
+ "additionalProperties": false,
+ "properties": {
+ "layers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "asset": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "const": "portrait",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "kind": {
+ "const": "decoration",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind",
+ "assetKey"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "id": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "required": {
+ "type": "boolean"
+ },
+ "rotation": {
+ "maximum": 180,
+ "minimum": -180,
+ "type": "number"
+ },
+ "surface": {
+ "enum": [
+ "viewport",
+ "main",
+ "home-hero",
+ "suggestions"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1.5,
+ "minimum": 0.02,
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "asset",
+ "surface",
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "opacity",
+ "rotation",
+ "required"
+ ],
+ "type": "object"
+ },
+ "maxItems": 24,
+ "type": "array"
+ }
+ },
+ "required": [
+ "layers"
+ ],
+ "type": "object"
+ },
+ "decorations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "assetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "intensity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "opacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "placement": {
+ "enum": [
+ "background",
+ "corners",
+ "hero",
+ "cards"
+ ],
+ "type": "string"
+ },
+ "scale": {
+ "maximum": 3,
+ "minimum": 0.25,
+ "type": "number"
+ },
+ "type": {
+ "enum": [
+ "particles",
+ "ribbon",
+ "butterflies",
+ "polaroid",
+ "badge",
+ "sparkles",
+ "image"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "type",
+ "enabled",
+ "intensity"
+ ],
+ "type": "object"
+ },
+ "maxItems": 16,
+ "type": "array"
+ },
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "home": {
+ "additionalProperties": false,
+ "properties": {
+ "welcome": {
+ "additionalProperties": false,
+ "properties": {
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "anchor": {
+ "enum": [
+ "top-left",
+ "top-center",
+ "top-right",
+ "center-left",
+ "center",
+ "center-right",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right"
+ ],
+ "type": "string"
+ },
+ "hideNativeIcon": {
+ "default": false,
+ "type": "boolean"
+ },
+ "positionX": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "positionY": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "textAlign": {
+ "enum": [
+ "left",
+ "center",
+ "right"
+ ],
+ "type": "string"
+ },
+ "width": {
+ "maximum": 1,
+ "minimum": 0.2,
+ "type": "number"
+ }
+ },
+ "required": [
+ "anchor",
+ "positionX",
+ "positionY",
+ "width",
+ "textAlign"
+ ],
+ "type": "object"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "lines": {
+ "items": {
+ "type": "string"
+ },
+ "maxItems": 3,
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "lines"
+ ],
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "required": [
+ "localized"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "welcome"
+ ],
+ "type": "object"
+ },
+ "id": {
+ "maxLength": 80,
+ "minLength": 3,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "interfaceImages": {
+ "additionalProperties": false,
+ "properties": {
+ "profileAvatarSize": {
+ "maximum": 48,
+ "minimum": 16,
+ "type": "integer"
+ },
+ "projectIconSize": {
+ "maximum": 32,
+ "minimum": 12,
+ "type": "integer"
+ },
+ "suggestionIconSize": {
+ "maximum": 64,
+ "minimum": 16,
+ "type": "integer"
+ }
+ },
+ "required": [
+ "profileAvatarSize",
+ "suggestionIconSize",
+ "projectIconSize"
+ ],
+ "type": "object"
+ },
+ "kind": {
+ "enum": [
+ "theme",
+ "recipe"
+ ],
+ "type": "string"
+ },
+ "layout": {
+ "additionalProperties": false,
+ "properties": {
+ "cardColumns": {
+ "maximum": 4,
+ "minimum": 2,
+ "type": "integer"
+ },
+ "composerWidth": {
+ "maximum": 1,
+ "minimum": 0.5,
+ "type": "number"
+ },
+ "heroHeight": {
+ "maximum": 560,
+ "minimum": 180,
+ "type": "integer"
+ },
+ "moduleGap": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "modules": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "align": {
+ "enum": [
+ "start",
+ "center",
+ "end",
+ "stretch"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "enum": [
+ "sidebar",
+ "topbar",
+ "hero",
+ "suggestions",
+ "project-picker",
+ "composer",
+ "task-background",
+ "content-layer"
+ ],
+ "type": "string"
+ },
+ "order": {
+ "maximum": 7,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "size": {
+ "enum": [
+ "compact",
+ "regular",
+ "expanded"
+ ],
+ "type": "string"
+ },
+ "spacing": {
+ "maximum": 48,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "visible": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "id",
+ "order",
+ "visible",
+ "size",
+ "align",
+ "spacing"
+ ],
+ "type": "object"
+ },
+ "maxItems": 8,
+ "minItems": 8,
+ "type": "array"
+ },
+ "sidebarDensity": {
+ "enum": [
+ "compact",
+ "comfortable"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "heroHeight",
+ "cardColumns",
+ "composerWidth",
+ "sidebarDensity",
+ "moduleGap",
+ "modules"
+ ],
+ "type": "object"
+ },
+ "metadata": {
+ "additionalProperties": false,
+ "properties": {
+ "homepage": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ },
+ "localized": {
+ "additionalProperties": {
+ "additionalProperties": false,
+ "properties": {
+ "description": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "propertyNames": {
+ "enum": [
+ "zh-CN",
+ "en"
+ ]
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "name": {
+ "maxLength": 80,
+ "minLength": 1,
+ "type": "string"
+ },
+ "rights": {
+ "additionalProperties": false,
+ "properties": {
+ "attribution": {
+ "maxLength": 240,
+ "minLength": 1,
+ "type": "string"
+ },
+ "licenseId": {
+ "maxLength": 100,
+ "minLength": 1,
+ "type": "string"
+ },
+ "localOnly": {
+ "type": "boolean"
+ },
+ "source": {
+ "format": "uri",
+ "maxLength": 500,
+ "type": "string"
+ }
+ },
+ "required": [
+ "licenseId",
+ "localOnly"
+ ],
+ "type": "object"
+ },
+ "schemaVersion": {
+ "const": 4,
+ "type": "number"
+ },
+ "surfaces": {
+ "additionalProperties": false,
+ "default": {
+ "baseOpacity": 0.68,
+ "blur": 0,
+ "elevatedOpacity": 0.92,
+ "terminalOpacity": 0.82
+ },
+ "properties": {
+ "baseOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "blur": {
+ "maximum": 30,
+ "minimum": 0,
+ "type": "number"
+ },
+ "elevatedOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ },
+ "terminalOpacity": {
+ "maximum": 1,
+ "minimum": 0,
+ "type": "number"
+ }
+ },
+ "required": [
+ "baseOpacity",
+ "elevatedOpacity",
+ "terminalOpacity",
+ "blur"
+ ],
+ "type": "object"
+ },
+ "typography": {
+ "additionalProperties": false,
+ "properties": {
+ "codeFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "codeFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "codeSize": {
+ "maximum": 22,
+ "minimum": 11,
+ "type": "number"
+ },
+ "codeWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "displayFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "displayFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "displayLetterSpacing": {
+ "maximum": 0.2,
+ "minimum": -0.05,
+ "type": "number"
+ },
+ "displayLineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.1,
+ "type": "number"
+ },
+ "displaySize": {
+ "maximum": 72,
+ "minimum": 20,
+ "type": "number"
+ },
+ "displayWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ },
+ "lineHeight": {
+ "maximum": 1.8,
+ "minimum": 1.2,
+ "type": "number"
+ },
+ "scale": {
+ "maximum": 1.3,
+ "minimum": 0.85,
+ "type": "number"
+ },
+ "uiFamily": {
+ "maxLength": 120,
+ "minLength": 1,
+ "type": "string"
+ },
+ "uiFontAssetKey": {
+ "maxLength": 40,
+ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
+ "type": "string"
+ },
+ "uiSize": {
+ "maximum": 22,
+ "minimum": 12,
+ "type": "number"
+ },
+ "uiWeight": {
+ "enum": [
+ 400,
+ 500,
+ 600,
+ 700
+ ],
+ "type": "number"
+ }
+ },
+ "required": [
+ "uiFamily",
+ "codeFamily",
+ "scale",
+ "uiSize",
+ "codeSize",
+ "uiWeight",
+ "codeWeight",
+ "lineHeight",
+ "displayFamily",
+ "displaySize",
+ "displayWeight",
+ "displayLineHeight",
+ "displayLetterSpacing"
+ ],
+ "type": "object"
+ },
+ "version": {
+ "pattern": "^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "schemaVersion",
+ "kind",
+ "id",
+ "name",
+ "version",
+ "author",
+ "assets",
+ "colors",
+ "typography",
+ "background",
+ "decorations",
+ "layout",
+ "rights",
+ "interfaceImages",
+ "composition"
+ ],
+ "type": "object"
+ }
+ }
+ },
+ "schemaVersion": 4
+}
diff --git a/contracts/theme/v4/semantic-cases.json b/contracts/theme/v4/semantic-cases.json
new file mode 100644
index 0000000..7477503
--- /dev/null
+++ b/contracts/theme/v4/semantic-cases.json
@@ -0,0 +1,64 @@
+{
+ "cases": [
+ {
+ "expectedSchemaVersion": 4,
+ "name": "complete v4 theme",
+ "valid": true
+ },
+ {
+ "expectedSchemaVersion": 4,
+ "name": "v1 semantic color migration",
+ "sourceVersion": 1,
+ "valid": true
+ },
+ {
+ "expectedSchemaVersion": 4,
+ "name": "v2 migration",
+ "sourceVersion": 2,
+ "valid": true
+ },
+ {
+ "expectedSchemaVersion": 4,
+ "name": "v3 migration",
+ "sourceVersion": 3,
+ "valid": true
+ },
+ {
+ "expectedErrorCode": "THEME_SCHEMA_VERSION_UNSUPPORTED",
+ "expectedPath": "/schemaVersion",
+ "name": "future schema version",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "THEME_WELCOME_INVALID",
+ "expectedPath": "/home/welcome",
+ "name": "unsafe welcome placeholder",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "THEME_DISPLAY_FONT_MISSING",
+ "expectedPath": "/typography/displayFontAssetKey",
+ "name": "missing display font",
+ "valid": false
+ },
+ {
+ "expectedErrorCode": "THEME_COMPOSITION_INVALID",
+ "expectedPath": "/composition/layers/0/asset",
+ "name": "undeclared composition asset",
+ "valid": false
+ },
+ {
+ "documentKind": "draft",
+ "name": "draft may omit background",
+ "valid": true
+ },
+ {
+ "documentKind": "theme",
+ "expectedErrorCode": "THEME_SCHEMA_INVALID",
+ "expectedPath": "/assets/background",
+ "name": "saved theme requires background",
+ "valid": false
+ }
+ ],
+ "schemaVersion": 4
+}
diff --git a/docs/go-host-gate-a.md b/docs/go-host-gate-a.md
new file mode 100644
index 0000000..6d68f15
--- /dev/null
+++ b/docs/go-host-gate-a.md
@@ -0,0 +1,30 @@
+# Go Host Gate A 决策记录
+
+## 决策
+
+**状态:本地 cutover 已收口;本次不推送、不发布。**
+
+2026-07-24,项目负责人明确要求不以 Gate A 阻断后续 Go Host tickets。因此允许继续实现 Ticket 06–16,并保持 `feat/go-host` 作为隔离开发分支。
+
+后续 Ticket 06–16 已按该决定继续实施。默认开发、Runtime、构建和 Release workflow 已切换到 Go,Node 生产 Host 与下载/stage/launcher 已删除;是否推送或创建 Release 仍由项目负责人单独决定。
+
+## 已确认的证据
+
+- v0.2.0 发布、协议、主题与数据格式基线已冻结。
+- Studio v2、Runtime v1、Theme v4、Data v1 与 Release Manifest v2 contract 已生成;v0.2.0 Node 结果只以 reviewed golden/corpus 只读保留,不再执行旧 Node Host。
+- 单 Go 二进制的 Studio、Controller、Runtime 已在 Windows 本地完成单元、合约、类型、构建和原生包测试。
+- 项目负责人已明确确认 Windows、macOS ARM64/x64、五主题、自定义主题、恢复、v0.2.0 数据升级及 Go → Node 回滚实机验收成功。该条证据来自人工验收确认,不冒充当前会话运行的自动化证据。
+- 选定图片实现为 CGo-free 的 `gen2brain-webp-wasm2go-nodynamic-plus-internal-pipeline`,不引入长期 native sidecar。
+- Go 工具链已固定为 `1.25.12`;2026-07-25 在 `host/go` 执行 `govulncheck ./...`,结果为 0 个代码可达漏洞。
+
+## 发布前仍需完成
+
+- 在同一候选提交上运行 GitHub `windows-latest`、`macos-15` 和 `macos-15-intel` 原生矩阵,生成并验收六类产物;当前本地会话没有运行这些远端 Jobs。
+- 合并三平台报告后重新生成 `checksums.txt`,确认六类产物精确 SHA-256,再决定是否创建 prerelease。
+- Codex 更新后重新执行 Windows/macOS 真实设备清单,不能沿用旧版本视觉结论。
+
+## 不变量
+
+- 不引入长期 Node fallback、Node/Go 双写或平台功能分叉。
+- 不把开发连续实施误报为真实设备兼容或可发布版本。
+- 任何安全拒绝、未知数据或原生验收失败都保持显式错误和可恢复证据。
diff --git a/docs/releases/v0.3.0-alpha.1.md b/docs/releases/v0.3.0-alpha.1.md
new file mode 100644
index 0000000..9ff47bd
--- /dev/null
+++ b/docs/releases/v0.3.0-alpha.1.md
@@ -0,0 +1,36 @@
+# 0.3.0-alpha.1
+
+OpenChatGPTSkin `0.3.0-alpha.1` 将生产 Theme Studio、Runtime Controller 和平台执行路径统一切换为一个跨平台 Go Host。React/Vite 前端、Theme Schema、Contract 作者源和 TypeScript CDP Adapter 继续保留,但用户安装包不再包含 Node Runtime 或 Node 业务 `node_modules`。
+
+## 主要变化
+
+- Windows x64、macOS ARM64、macOS x64 使用同一 Go 业务实现和 `studio | controller | runtime` 多进程角色。
+- 保留五个内置主题、自定义主题、图片处理、草稿、版本、导入导出以及 launch/switch/pause/resume/restore 完整闭环。
+- Windows Setup/ZIP 与 macOS DMG/tar.gz 均为 Node-free 产物;macOS 压缩包包含真实 `OpenChatGPTSkin.app` 和目标架构 Mach-O。
+- `release-manifest.json` 记录 Go 工具链、commit、entry/file SHA-256、Contract/CDP 哈希、图片实现和主题清单。
+- 已在真实 Windows、macOS ARM64 和 macOS x64 上完成应用、恢复、v0.2.0 数据升级与 Go → Node 回滚验收。
+
+## 已验证基线
+
+- Go 工具链:`1.25.12`。
+- Windows 官方应用基线:`OpenAI.Codex 26.721.4979.0`。
+- Runtime control contract v1、Studio HTTP contract v2、Theme Schema v4、Data contract v1。
+- 五个内置主题:`future-idol-cyan`、`glacier-aurora`、`mountain-mist`、`rose-carpet-star`、`yua-mikami-starlight`。
+- Go `1.25.12` 的 `govulncheck ./...` 结果为 0 个代码可达漏洞;依赖中不可达的报告项不被误报为当前代码可利用。
+
+## macOS 未签名限制
+
+本版本不包含 Apple Developer ID 正式签名或 notarization。首次打开请使用系统标准的 Control-click/右键 → **打开**流程;不要关闭 Gatekeeper,也不要使用 `xattr` 移除隔离属性。OpenChatGPTSkin 对官方 ChatGPT/Codex 的签名、Team ID 和 notarization 身份校验不会因此放宽。
+
+## 回滚到 v0.2.0
+
+1. 在 Theme Studio 中恢复原始皮肤,并正常退出受管理的 ChatGPT。
+2. 卸载或删除 `0.3.0-alpha.1` 程序文件;保留 `%LOCALAPPDATA%\OpenChatGPTSkin` 或 `~/Library/Application Support/OpenChatGPTSkin`。
+3. 重新安装 [v0.2.0](https://github.com/u2bo/OpenChatGPTSkin/releases/tag/v0.2.0)。
+4. v0.2.0 Node Host 可以读取 Go 写入的兼容草稿、版本、素材和主题数据。
+
+## Known limitations
+
+- 官方 ChatGPT/Codex 更新可能改变内部 UI;未知 surface 会安全拒绝应用,而不会注入未经验证的 fallback。
+- 项目没有自动更新、主题市场或 Codex 插件市场安装能力。
+- 安装器和 macOS App Bundle 尚未进行商业代码签名。
diff --git a/docs/runtime-macos.en.md b/docs/runtime-macos.en.md
index e4f9ac6..60592e5 100644
--- a/docs/runtime-macos.en.md
+++ b/docs/runtime-macos.en.md
@@ -82,7 +82,7 @@ Enumerating macOS windows normally requires Accessibility consent. OpenChatGPTSk
| Release-package acceptance | ZIP, Setup, and installer lifecycle | Payload, `.tar.gz`, `.app`, Mach-O, and DMG mount |
| Real-Codex probe/visual acceptance | Available | Not automated; use the manual checklist below |
-The historical path and package name `runtime/windows` / `@open-chatgpt-skin/windows-runtime` remain for compatibility and to avoid an unrelated repository-wide move. Its Controller and public provider interface are now platform-neutral.
+Starting with `v0.3.0-alpha.1`, production Studio, Controller, and Runtime roles come from the same implementation under `host/go`. TypeScript remains only for the frontend, Theme/Contract authoring sources, and the CDP Adapter; there is no Node business host or platform fallback.
## Real-Mac acceptance checklist
@@ -103,12 +103,12 @@ Use a test account/workspace with no private projects or sensitive chats:
5. Run `restore`, verify the official appearance, and quit from the Codex menu so cleanup can finish.
6. During the run, `ls -l /tmp/OpenChatGPTSkin-*.sock` must show a current-user `srw-------` socket. The endpoint must disappear after Controller exit.
7. Start official Codex normally. It must not inherit `--remote-debugging-address` or `--remote-debugging-port`, and the regular app must remain unthemed.
-8. Record Codex/macOS versions, four-theme results, restore result, and sanitized screenshots. Do not record PIDs, ports, usernames, paths, command lines, project names, or chat content.
+8. Record Codex/macOS versions, five-theme results, restore result, and sanitized screenshots. Do not record PIDs, ports, usernames, paths, command lines, project names, or chat content.
-`npm run runtime:probe` and `npm run runtime:acceptance` currently return `RUNTIME_ENVIRONMENT_INVALID` on macOS. That is an explicit platform boundary, not a silent downgrade.
+The old Node Host commands `runtime:probe` and `runtime:acceptance` have been removed. Current acceptance combines Go unit/contract/native-package automation with the real-device observations in this checklist; no hidden fallback claims platform support.
## Known risks
- Codex updates may change signing metadata, process structure, or DOM surface contracts and require adapter updates plus new acceptance.
- The first real-Mac run must confirm that the Apple Team ID in code matches the current official Codex signature. If it differs, verify the official source first; never weaken the policy to accept any valid signature.
-- Do not claim complete macOS compatibility or full real-app UI validation before this checklist is completed on a Mac.
+- Repeat this checklist after every Codex update; do not extrapolate real-device results from an older Codex version.
diff --git a/docs/runtime-macos.md b/docs/runtime-macos.md
index 694aeb5..8c40302 100644
--- a/docs/runtime-macos.md
+++ b/docs/runtime-macos.md
@@ -88,7 +88,7 @@ macOS 窗口枚举通常需要 Accessibility 权限。OpenChatGPTSkin 不为换
| 发布包自动验收 | ZIP、Setup 与安装生命周期 | payload、`.tar.gz`、`.app`、Mach-O 与 DMG 挂载 |
| 真实 Codex Probe/视觉验收 | 已提供 | 尚未自动化,使用下方手动清单 |
-仓库暂时保留历史目录和包名 `runtime/windows` / `@open-chatgpt-skin/windows-runtime`,避免在本次平台适配中进行无关的大范围搬迁;其中 Controller 和公共接口已经平台中立。
+`v0.3.0-alpha.1` 起生产 Studio、Controller 与 Runtime 由 `host/go` 的同一实现提供;TypeScript 仅保留前端、Theme/Contract 作者源与 CDP Adapter,不存在 Node 业务 Host 或平台 fallback。
## 真实 Mac 验收清单
@@ -115,12 +115,12 @@ macOS 窗口枚举通常需要 Accessibility 权限。OpenChatGPTSkin 不为换
运行期间应为当前用户所有、权限 `srw-------`;Controller 退出后对应 socket 应被删除。
7. 正常启动官方 Codex,确认没有继承 `--remote-debugging-address` 或 `--remote-debugging-port`,且普通应用不受主题影响。
-8. 记录 Codex 版本、macOS 版本、四主题结果、恢复结果和脱敏截图;不要记录 PID、端口、用户名、路径、命令行、项目名或聊天内容。
+8. 记录 Codex 版本、macOS 版本、五主题结果、恢复结果和脱敏截图;不要记录 PID、端口、用户名、路径、命令行、项目名或聊天内容。
-`npm run runtime:probe` 与 `npm run runtime:acceptance` 当前会在 macOS 返回 `RUNTIME_ENVIRONMENT_INVALID`,这是明确的平台边界,不是静默降级。
+旧 Node Host 的 `runtime:probe` 与 `runtime:acceptance` 已删除。当前验收以 Go 单元/合约/原生包自动检查加本清单的真实设备观察为准,不通过隐藏 fallback 伪造平台能力。
## 已知风险
- Codex 更新可能改变 bundle 签名信息、进程结构或 DOM surface contract,需要更新适配并重新验收;
- 首次实机验收必须确认代码中的 Apple Team ID 与当前官方 Codex 签名一致;不一致时应先核对官方来源,不能放宽为“接受任意有效签名”;
-- 真实 Mac 验收完成前,不应发布“macOS 已完全兼容”或“所有 UI 已实机验证”的声明。
+- Codex 升级后必须重新执行本清单,不能把旧版本的实机结果外推到新版本。
diff --git a/docs/runtime-windows.md b/docs/runtime-windows.md
index f451a28..df96fb3 100644
--- a/docs/runtime-windows.md
+++ b/docs/runtime-windows.md
@@ -1,114 +1,73 @@
-# Windows Runtime、发布包与兼容性门
+# Windows Runtime、发布包与兼容性验收
-> Runtime 控制器更新(以本节为准):当前仓库提供开发者预览命令与自动化测试,但尚未在本文档中声明真实 Codex 的视觉验收或本机 `runtime:acceptance` 证据已经完成。下面较早的 Probe 流程仍用于 Codex 包升级后的兼容性检查。
+[返回 README](../README.md)
-## Windows 发布包
+`v0.3.0-alpha.1` 的 Windows 生产包使用单一 Go Host 承担 Theme Studio、Controller 与 Runtime 角色。便携 ZIP 和当前用户 Setup 均不包含 Node.js 或 Node 业务 `node_modules`;React/Vite 前端、Theme/Contract 作者源与 TypeScript CDP Adapter 只在构建阶段使用。
-`v0.1.0` 提供 Windows x64 便携 ZIP 与用户级 Setup,当时的发布验收读取四个内置主题。当前源码的后续构建会通过同一 Release staging、固定 Node.js 22 Runtime 和平台匹配的 `sharp` 校验五个内置主题,并继续覆盖完整文件 manifest、生产 UI、一次性 session、真实图片处理、`.ocskin` 保存导出和正常关闭。Setup 额外验证当前用户静默安装、覆盖安装与默认卸载均保留个人主题和草稿。
+## 安装与数据目录
-Setup 默认安装到 `%LOCALAPPDATA%\Programs\OpenChatGPTSkin`,不要求管理员权限;程序数据位于 `%LOCALAPPDATA%\OpenChatGPTSkin`。交互式卸载会询问是否同时删除个人数据,默认选择为“否”,确认删除时会再次明确提示不可恢复。未签名安装包可能触发 SmartScreen,应先用 Release 的 `checksums.txt` 验证 SHA-256。
+- Setup 默认安装到 `%LOCALAPPDATA%\Programs\OpenChatGPTSkin`,不请求管理员权限;
+- 便携 ZIP 解压后运行 `OpenChatGPTSkin.exe`;
+- 用户数据位于 `%LOCALAPPDATA%\OpenChatGPTSkin`,覆盖安装和默认卸载不会删除个人主题、草稿或版本;
+- 未签名安装包可能触发 SmartScreen,只应从项目 GitHub Release 下载并先核对 `checksums.txt`。
-## Runtime 控制器开发者用法
+发布包必须包含 `release-manifest.json` schema v2。Manifest 记录 Go Host 版本/提交/入口 Hash、Contract Hash、CDP Adapter Hash、五个内置主题和 Stage 中每个文件的 SHA-256,并明确声明空 sidecar;验收会拒绝 `node.exe`、`node` 或 `node_modules`。
-前提:Windows 11、Node.js `>=22`、依赖已安装。先保存工作并完全退出普通 Codex;若发现普通实例仍在运行,Runtime 会拒绝接管,不会结束该实例。
+## 源码开发命令
-新版 Codex 在官方 AUMID 激活期间可能短暂创建第二个同包根进程,或把窗口与 CDP
-交接给新根。Runtime 会先检查直接启动的实例,并在 AUMID 前给予窗口最多 5 秒的
-收敛时间;当唯一可信根、可见窗口和原回环 CDP 所有权已经成立时,不再执行冗余
-AUMID 激活,避免制造第二个根进程。只有该收敛期结束后窗口仍未就绪时才进入 AUMID
-流程。该流程不依赖 PID 必须保持不变,并要求最终根唯一、
-入口路径仍等于已验证的官方 Appx 入口、可见窗口属于最终根进程树、原回环 CDP 端口
-也属于最终根进程树。只有这些条件同时成立才会更新受管会话并继续应用主题。
+源码开发需要 Windows 11、Go `1.25.12`、Node.js `>=22` 与 npm。Node 只用于前端、Contract 和 Adapter 构建,不进入用户包。
```powershell
npm run runtime -- list-themes
-npm run runtime -- import --theme-file "D:\\Themes\\personal-theme.ocskin"
+npm run runtime -- import --theme-file "D:\Themes\personal-theme.ocskin"
npm run runtime -- launch --theme mountain-mist
npm run runtime -- switch --theme glacier-aurora
+npm run runtime -- launch --theme personal-theme --version 1.0.0
npm run runtime -- pause
npm run runtime -- resume
+npm run runtime -- status
npm run runtime -- restore
```
-可用主题为 `future-idol-cyan`、`rose-carpet-star`、`mountain-mist`、`glacier-aurora`。`import --theme-file` 会使用现有 `.ocskin` 安全校验并原子安装个人主题,不启动 Controller;导入后使用 `launch` 或 `switch` 的 `--theme Vite development server is unavailable.
")) + } + return proxy, nil +} + +func validateViteOrigin(value string) (*url.URL, error) { + parsed, err := url.Parse(value) + if err != nil || parsed.Scheme != "http" || parsed.Hostname() == "" || parsed.Port() == "" || + parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || (parsed.Path != "" && parsed.Path != "/") { + return nil, studioError{code: "STUDIO_ORIGIN_REJECTED", message: "Vite origin must be a loopback HTTP origin with a port", statusCode: http.StatusBadRequest} + } + if parsed.Hostname() != "127.0.0.1" && parsed.Hostname() != "localhost" { + return nil, studioError{code: "STUDIO_ORIGIN_REJECTED", message: "Vite origin must use loopback", statusCode: http.StatusBadRequest} + } + port, err := strconv.Atoi(parsed.Port()) + if err != nil || port < 1 || port > 65535 { + return nil, studioError{code: "STUDIO_ORIGIN_REJECTED", message: "Vite origin port is invalid", statusCode: http.StatusBadRequest} + } + return parsed, nil +} + +func (state *handlerState) writeJSON(response http.ResponseWriter, status int, body any) { + response.Header().Set("Content-Type", "application/json; charset=utf-8") + response.Header().Set("Cache-Control", "no-store") + response.Header().Set("X-Content-Type-Options", "nosniff") + response.WriteHeader(status) + if err := json.NewEncoder(response).Encode(body); err != nil { + return + } +} + +func (state *handlerState) writeBytes(response http.ResponseWriter, status int, asset themerepo.Asset) { + response.Header().Set("Content-Type", asset.MIMEType) + response.Header().Set("Content-Length", strconv.Itoa(len(asset.Bytes))) + response.Header().Set("Cache-Control", "no-store") + response.Header().Set("X-Content-Type-Options", "nosniff") + response.WriteHeader(status) + _, _ = response.Write(asset.Bytes) +} + +func (state *handlerState) writeError(response http.ResponseWriter, err error) { + status := http.StatusInternalServerError + code := "INTERNAL" + message := "The Studio request could not be completed." + var studioErr studioError + if errors.As(err, &studioErr) { + status, code, message = studioErr.statusCode, studioErr.code, studioErr.message + } + var repositoryErr themerepo.Error + if errors.As(err, &repositoryErr) { + status, code, message = http.StatusUnprocessableEntity, repositoryErr.Code, repositoryErr.Message + if repositoryErr.Code == "THEME_NOT_FOUND" { + status = http.StatusNotFound + } + } + var workspaceErr workspace.Error + if errors.As(err, &workspaceErr) { + status, code, message = http.StatusUnprocessableEntity, workspaceErr.Code, workspaceErr.Message + if workspaceErr.Code == "STUDIO_DRAFT_NOT_FOUND" { + status = http.StatusNotFound + } + if workspaceErr.Code == "STUDIO_DRAFT_CONFLICT" { + status = http.StatusConflict + } + } + state.writeJSON(response, status, map[string]any{"error": map[string]string{"code": code, "message": message}}) +} + +func (state *handlerState) serveEvents(response http.ResponseWriter, request *http.Request) error { + flusher, ok := response.(http.Flusher) + if !ok { + return studioError{code: "INTERNAL", message: "Streaming is unavailable", statusCode: http.StatusInternalServerError} + } + state.mu.Lock() + if state.activeSSE >= state.maxSSEClients { + state.mu.Unlock() + return studioError{code: "RUNTIME_STATUS_UNAVAILABLE", message: "Too many Runtime status streams are open", statusCode: http.StatusServiceUnavailable} + } + state.activeSSE++ + state.mu.Unlock() + defer func() { + state.mu.Lock() + state.activeSSE-- + state.mu.Unlock() + }() + response.Header().Set("Content-Type", "text/event-stream; charset=utf-8") + response.Header().Set("Cache-Control", "no-store") + response.Header().Set("Connection", "keep-alive") + response.WriteHeader(http.StatusOK) + write := func() error { + state.mu.Lock() + state.eventSequence++ + sequence := state.eventSequence + state.mu.Unlock() + payload, err := json.Marshal(map[string]any{ + "protocolVersion": 2, "sequence": sequence, "kind": "runtime-status", "runtime": state.runtimeStatus(), + }) + if err != nil { + return err + } + if _, err := fmt.Fprintf(response, "data: %s\n\n", payload); err != nil { + return err + } + flusher.Flush() + return nil + } + if err := write(); err != nil { + return nil + } + ticker := time.NewTicker(eventInterval) + defer ticker.Stop() + for { + select { + case <-request.Context().Done(): + return nil + case <-ticker.C: + if err := write(); err != nil { + return nil + } + } + } +} diff --git a/host/go/internal/studio/server_test.go b/host/go/internal/studio/server_test.go new file mode 100644 index 0000000..63abc84 --- /dev/null +++ b/host/go/internal/studio/server_test.go @@ -0,0 +1,322 @@ +package studio + +import ( + "bytes" + "context" + "encoding/json" + stdimage "image" + "image/color" + "image/png" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/u2bo/OpenChatGPTSkin/host/go/internal/themerepo" +) + +func studioFixture(t *testing.T) Config { + t.Helper() + root := t.TempDir() + themeDirectory := filepath.Join(root, "themes", "builtin", "mountain-mist") + if err := os.MkdirAll(themeDirectory, 0o700); err != nil { + t.Fatal(err) + } + catalog := `{"schemaVersion":1,"builtins":[{"id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","kind":"theme","path":"builtin/mountain-mist","ready":true,"localOnly":false,"licenseId":"LicenseRef-Test","preview":"builtin/mountain-mist/preview.webp"}],"recipes":[]}` + theme := `{"schemaVersion":4,"kind":"theme","appearance":"light","id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","author":"OpenChatGPTSkin","assets":{"background":"assets/background.webp"},"colors":{},"typography":{},"background":{},"decorations":[],"layout":{},"rights":{"localOnly":false}}` + if err := os.WriteFile(filepath.Join(root, "themes", "catalog.json"), []byte(catalog), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(themeDirectory, "theme.json"), []byte(theme), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(themeDirectory, "preview.webp"), []byte("RIFF0000WEBP"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(themeDirectory, "assets"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(themeDirectory, "assets", "background.webp"), fixturePNG(t), 0o600); err != nil { + t.Fatal(err) + } + return Config{ + IndexHTML: []byte(``), + ThemeRoot: filepath.Join(root, "themes"), PersonalRoot: filepath.Join(root, "theme-store"), DraftRoot: filepath.Join(root, "theme-studio-drafts"), StudioVersion: "0.2.0", + } +} + +func fixturePNG(t *testing.T) []byte { + t.Helper() + image := stdimage.NewNRGBA(stdimage.Rect(0, 0, 8, 4)) + image.SetNRGBA(1, 1, color.NRGBA{R: 80, G: 120, B: 255, A: 255}) + var output bytes.Buffer + if err := png.Encode(&output, image); err != nil { + t.Fatal(err) + } + return output.Bytes() +} + +func sessionClient(t *testing.T, server *RunningServer) *http.Client { + t.Helper() + jar, err := cookiejar.New(nil) + if err != nil { + t.Fatal(err) + } + client := &http.Client{Jar: jar} + request, err := http.NewRequest(http.MethodPost, server.Origin+"/api/session", strings.NewReader(`{"token":"`+strings.TrimPrefix(server.BootstrapURL, server.Origin+"/#bootstrap=")+`"}`)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Origin", server.Origin) + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusNoContent { + t.Fatalf("session status = %d", response.StatusCode) + } + return client +} + +func TestStudioServesOnlyLoopbackSecureSessionAndThemeLibrary(t *testing.T) { + server, err := Start(context.Background(), studioFixture(t)) + if err != nil { + t.Fatal(err) + } + defer server.Close() + response, err := http.Get(server.Origin + "/") + if err != nil { + t.Fatal(err) + } + contents, _ := io.ReadAll(response.Body) + response.Body.Close() + if response.Header.Get("Content-Security-Policy") == "" || strings.Contains(string(contents), cspNoncePlaceholder) { + t.Fatal("production UI has no complete CSP nonce") + } + client := sessionClient(t, server) + libraryResponse, err := client.Get(server.Origin + "/api/themes") + if err != nil { + t.Fatal(err) + } + defer libraryResponse.Body.Close() + if libraryResponse.StatusCode != http.StatusOK { + t.Fatalf("theme library status = %d", libraryResponse.StatusCode) + } + var library themerepo.Library + if err := json.NewDecoder(libraryResponse.Body).Decode(&library); err != nil { + t.Fatal(err) + } + if len(library.Themes) != 1 || library.Themes[0].PreviewURL == nil { + t.Fatalf("library = %+v", library) + } +} + +func TestStudioRejectsReplayCrossOriginAndUnauthenticatedAPI(t *testing.T) { + server, err := Start(context.Background(), studioFixture(t)) + if err != nil { + t.Fatal(err) + } + defer server.Close() + response, err := http.Get(server.Origin + "/api/themes") + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusUnauthorized { + t.Fatalf("unauthenticated status = %d", response.StatusCode) + } + token := strings.TrimPrefix(server.BootstrapURL, server.Origin+"/#bootstrap=") + request, err := http.NewRequest(http.MethodPost, server.Origin+"/api/session", strings.NewReader(`{"token":"`+token+`"}`)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Origin", "http://127.0.0.1:1") + response, err = http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusForbidden { + t.Fatalf("cross-origin status = %d", response.StatusCode) + } + _ = sessionClient(t, server) + request, err = http.NewRequest(http.MethodPost, server.Origin+"/api/session", strings.NewReader(`{"token":"`+token+`"}`)) + if err != nil { + t.Fatal(err) + } + request.Header.Set("Origin", server.Origin) + response, err = http.DefaultClient.Do(request) + if err != nil { + t.Fatal(err) + } + response.Body.Close() + if response.StatusCode != http.StatusUnauthorized { + t.Fatalf("replayed session status = %d", response.StatusCode) + } +} + +func TestStudioDevProxyAcceptsOnlyLoopbackAndDoesNotForwardSessionState(t *testing.T) { + for _, origin := range []string{ + "https://127.0.0.1:5173", "http://example.com:5173", "http://127.0.0.1", "http://127.0.0.1:5173/path", + } { + if _, err := validateViteOrigin(origin); err == nil { + t.Fatalf("invalid Vite origin was accepted: %s", origin) + } + } + upstream := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { + if request.Header.Get("Cookie") != "" || request.Header.Get("Authorization") != "" || request.Header.Get("Origin") != "" { + t.Fatal("Studio session state was forwarded to Vite") + } + response.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = response.Write([]byte("Vite")) + })) + defer upstream.Close() + config := studioFixture(t) + config.IndexHTML = nil + config.ViteOrigin = upstream.URL + server, err := Start(context.Background(), config) + if err != nil { + t.Fatal(err) + } + defer server.Close() + response, err := http.Get(server.Origin + "/") + if err != nil { + t.Fatal(err) + } + contents, _ := io.ReadAll(response.Body) + response.Body.Close() + if response.StatusCode != http.StatusOK || !strings.Contains(string(contents), `property="csp-nonce"`) { + t.Fatalf("dev proxy response = %d %s", response.StatusCode, contents) + } + api, err := http.Get(server.Origin + "/api/themes") + if err != nil { + t.Fatal(err) + } + api.Body.Close() + if api.StatusCode != http.StatusUnauthorized { + t.Fatalf("API was forwarded to Vite: %d", api.StatusCode) + } +} + +func TestStudioDraftRoutesPersistAndExportPersonalVersion(t *testing.T) { + server, err := Start(context.Background(), studioFixture(t)) + if err != nil { + t.Fatal(err) + } + defer server.Close() + client := sessionClient(t, server) + create, err := http.NewRequest(http.MethodPost, server.Origin+"/api/drafts", strings.NewReader(`{"source":{"source":"builtin","ref":{"id":"mountain-mist","version":"1.3.0"}}}`)) + if err != nil { + t.Fatal(err) + } + create.Header.Set("Origin", server.Origin) + create.Header.Set("Content-Type", "application/json") + response, err := client.Do(create) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusCreated { + response.Body.Close() + t.Fatalf("create status = %d", response.StatusCode) + } + var created struct { + DraftID string `json:"draftId"` + Revision int `json:"revision"` + } + if err := json.NewDecoder(response.Body).Decode(&created); err != nil { + response.Body.Close() + t.Fatal(err) + } + response.Body.Close() + save, err := http.NewRequest(http.MethodPost, server.Origin+"/api/drafts/"+created.DraftID+"/save", strings.NewReader(`{"expectedRevision":0}`)) + if err != nil { + t.Fatal(err) + } + save.Header.Set("Origin", server.Origin) + save.Header.Set("Content-Type", "application/json") + response, err = client.Do(save) + if err != nil { + t.Fatal(err) + } + if response.StatusCode != http.StatusOK { + response.Body.Close() + t.Fatalf("save status = %d", response.StatusCode) + } + var saved struct { + Ref themerepo.Ref `json:"ref"` + } + if err := json.NewDecoder(response.Body).Decode(&saved); err != nil { + response.Body.Close() + t.Fatal(err) + } + response.Body.Close() + if saved.Ref != (themerepo.Ref{ID: "mountain-mist-custom", Version: "1.0.0"}) { + t.Fatalf("saved ref = %+v", saved.Ref) + } + response, err = client.Get(server.Origin + "/api/export?id=" + saved.Ref.ID + "&version=" + saved.Ref.Version) + if err != nil { + t.Fatal(err) + } + archive, _ := io.ReadAll(response.Body) + response.Body.Close() + if response.StatusCode != http.StatusOK || len(archive) == 0 || response.Header.Get("Content-Type") != "application/vnd.open-chatgpt-skin+zip" { + t.Fatalf("export status=%d bytes=%d type=%q", response.StatusCode, len(archive), response.Header.Get("Content-Type")) + } +} + +func TestStudioAppliesOnlySavedThemeAndReturnsRuntimeStatus(t *testing.T) { + config := studioFixture(t) + applied := themerepo.Ref{} + config.ApplyTheme = func(_ context.Context, ref themerepo.Ref) (RuntimeStatus, error) { + applied = ref + active := true + return RuntimeStatus{Status: "active", ControllerAvailable: true, SelectedTheme: &ref, AppliedTheme: &ref, SkinApplied: &active, NextAction: "Theme is active."}, nil + } + config.RestoreTheme = func(context.Context) (RuntimeStatus, error) { + active := false + return RuntimeStatus{Status: "restored-awaiting-exit", ControllerAvailable: true, SkinApplied: &active, NextAction: "Waiting for normal exit."}, nil + } + server, err := Start(context.Background(), config) + if err != nil { + t.Fatal(err) + } + defer server.Close() + client := sessionClient(t, server) + apply, err := http.NewRequest(http.MethodPost, server.Origin+"/api/themes/apply", strings.NewReader(`{"id":"mountain-mist","version":"1.3.0"}`)) + if err != nil { + t.Fatal(err) + } + apply.Header.Set("Origin", server.Origin) + apply.Header.Set("Content-Type", "application/json") + response, err := client.Do(apply) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK || applied != (themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}) { + t.Fatalf("apply status=%d ref=%+v", response.StatusCode, applied) + } + var runtime RuntimeStatus + if err := json.NewDecoder(response.Body).Decode(&runtime); err != nil || runtime.Status != "active" || runtime.SkinApplied == nil || !*runtime.SkinApplied { + t.Fatalf("runtime=%+v error=%v", runtime, err) + } + restore, err := http.NewRequest(http.MethodPost, server.Origin+"/api/runtime/restore", nil) + if err != nil { + t.Fatal(err) + } + restore.Header.Set("Origin", server.Origin) + response, err = client.Do(restore) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("restore status=%d", response.StatusCode) + } +} diff --git a/host/go/internal/themerepo/archive.go b/host/go/internal/themerepo/archive.go new file mode 100644 index 0000000..6256798 --- /dev/null +++ b/host/go/internal/themerepo/archive.go @@ -0,0 +1,987 @@ +package themerepo + +import ( + "archive/zip" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + "time" +) + +const ( + maxArchiveBytes = 32 * 1024 * 1024 + maxExpandedBytes = 32 * 1024 * 1024 + maxAssetBytes = 16 * 1024 * 1024 + maxFontBytes = 5 * 1024 * 1024 +) + +type Bundle struct { + Ref Ref + Document []byte + Files map[string][]byte +} + +type archiveManifest struct { + SchemaVersion int `json:"schemaVersion"` + ThemeID string `json:"themeId"` + ThemeVersion string `json:"themeVersion"` + Files map[string]struct { + Bytes int `json:"bytes"` + SHA256 string `json:"sha256"` + } `json:"files"` +} + +// ValidateArchive verifies that an .ocskin archive can be unpacked without +// persisting anything. It is used by compatibility verification and request +// validation before an import is committed. +func ValidateArchive(contents []byte) error { + _, err := unpack(contents) + return err +} + +// ValidateDocument verifies a Theme Schema v1-v4 document, including the +// normalisation path used for imported archives. +func ValidateDocument(contents []byte) error { + _, _, err := normalizeDocument(contents) + return err +} + +// ValidateDraftDocument permits a temporarily incomplete draft (for example a +// background slot intentionally cleared in the editor) but keeps all schema, +// asset-path and composition safety checks in force. Saving an archive/theme +// still calls ValidateDocument and therefore requires a background. +func ValidateDraftDocument(contents []byte) error { + _, _, err := normalizeDraftDocument(contents) + return err +} + +// NormalizeDocument returns the canonical Schema v4 encoding together with +// its identity. Workspace persistence uses this exact normalisation boundary +// so Go-written draft records remain consumable by the v0.2 Node rollback host. +func NormalizeDocument(contents []byte) ([]byte, Ref, error) { + normalized, header, err := normalizeDocument(contents) + if err != nil { + return nil, Ref{}, err + } + return normalized, Ref{ID: header.ID, Version: header.Version}, nil +} + +// NormalizeDraftDocument mirrors the Node draft parser: it produces canonical +// Schema v4 JSON while allowing a missing background until explicit save. +func NormalizeDraftDocument(contents []byte) ([]byte, Ref, error) { + normalized, header, err := normalizeDraftDocument(contents) + if err != nil { + return nil, Ref{}, err + } + return normalized, Ref{ID: header.ID, Version: header.Version}, nil +} + +// AssetPaths returns the complete declared asset set in stable order. +func AssetPaths(document []byte) ([]string, error) { + return assetPaths(document) +} + +// ErrorCode exposes stable, user-safe theme repository error codes to the +// host boundary without leaking filesystem or archive implementation details. +func ErrorCodeFrom(err error) string { + if err == nil { + return "" + } + var themeError Error + if errors.As(err, &themeError) { + return themeError.Code + } + return "INTERNAL" +} + +func (repository *Repository) ImportArchive(contents []byte) (Ref, error) { + bundle, err := unpack(contents) + if err != nil { + return Ref{}, err + } + return repository.InstallPersonal(bundle) +} + +func (repository *Repository) Export(source string, ref Ref) ([]byte, error) { + bundle, err := repository.Read(source, ref) + if err != nil { + return nil, err + } + return pack(bundle) +} + +func (repository *Repository) Read(source string, ref Ref) (Bundle, error) { + if !validRef(ref) { + return Bundle{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme reference is invalid"} + } + switch source { + case "builtin": + entry, err := repository.findBuiltin(ref) + if err != nil { + return Bundle{}, err + } + path, err := repository.safePath(entry.Path) + if err != nil { + return Bundle{}, err + } + return readBundleDirectory(path, ref) + case "personal": + return repository.readPersonal(ref) + default: + return Bundle{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme source is unavailable"} + } +} + +func (repository *Repository) InstallPersonal(bundle Bundle) (Ref, error) { + if repository.personalRoot == "" { + return Ref{}, Error{Code: "STUDIO_IMPORT_INVALID", Message: "Personal theme storage is unavailable"} + } + normalized, err := normalizeBundle(bundle) + if err != nil { + return Ref{}, err + } + target, err := repository.personalPath(normalized.Ref) + if err != nil { + return Ref{}, err + } + if _, err := os.Lstat(target); err == nil { + existing, readErr := readBundleDirectory(target, normalized.Ref) + if readErr != nil { + return Ref{}, readErr + } + if bundlesEqual(existing, normalized) { + return normalized.Ref, nil + } + return Ref{}, Error{Code: "THEME_VERSION_CONFLICT", Message: "Personal theme version already exists with different content"} + } else if !errors.Is(err, os.ErrNotExist) { + return Ref{}, err + } + parent := filepath.Dir(target) + if err := os.MkdirAll(parent, 0o700); err != nil { + return Ref{}, err + } + staging, err := os.MkdirTemp(parent, ".theme-staging-") + if err != nil { + return Ref{}, err + } + committed := false + defer func() { + if !committed { + _ = os.RemoveAll(staging) + } + }() + if err := writeBundleDirectory(staging, normalized); err != nil { + return Ref{}, err + } + if err := os.Rename(staging, target); err != nil { + if errors.Is(err, os.ErrExist) { + return Ref{}, Error{Code: "THEME_VERSION_CONFLICT", Message: "Personal theme version already exists"} + } + return Ref{}, err + } + committed = true + return normalized.Ref, nil +} + +func (repository *Repository) DeletePersonal(id string, version *string) error { + if repository.personalRoot == "" { + return Error{Code: "STUDIO_DELETE_FAILED", Message: "Personal theme storage is unavailable"} + } + if len(id) < 3 || len(id) > 80 || !themeIDPattern.MatchString(id) { + return Error{Code: "STUDIO_DELETE_FAILED", Message: "Personal theme ID is invalid"} + } + if version != nil { + if !themeVersionPattern.MatchString(*version) { + return Error{Code: "STUDIO_DELETE_FAILED", Message: "Personal theme version is invalid"} + } + path, err := repository.personalPath(Ref{ID: id, Version: *version}) + if err != nil { + return err + } + if err := removeOwnedDirectory(path); err != nil { + return err + } + return removeEmptyDirectory(filepath.Dir(path)) + } + path, err := repository.personalIDPath(id) + if err != nil { + return err + } + return removeOwnedDirectory(path) +} + +func (repository *Repository) listPersonal() ([]ListItem, error) { + if repository.personalRoot == "" { + return nil, nil + } + root := filepath.Join(repository.personalRoot, "themes") + ids, err := os.ReadDir(root) + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + if err != nil { + return nil, err + } + items := make([]ListItem, 0) + for _, idEntry := range ids { + if !idEntry.IsDir() || !themeIDPattern.MatchString(idEntry.Name()) { + continue + } + versions, err := os.ReadDir(filepath.Join(root, idEntry.Name())) + if err != nil { + return nil, err + } + for _, versionEntry := range versions { + if !versionEntry.IsDir() || !themeVersionPattern.MatchString(versionEntry.Name()) { + continue + } + ref := Ref{ID: idEntry.Name(), Version: versionEntry.Name()} + bundle, err := repository.readPersonal(ref) + if err != nil { + return nil, err + } + header, err := headerFromDocument(bundle.Document) + if err != nil { + return nil, err + } + var previewURL *string + if _, exists := bundle.Files["preview.webp"]; exists { + value := "/api/theme-preview?source=personal&id=" + ref.ID + "&version=" + ref.Version + previewURL = &value + } + items = append(items, ListItem{ + Ref: ref, Name: header.Name, Description: header.Description, Author: header.Author, + Homepage: header.Metadata.Homepage, Localized: header.Metadata.Localized, + Source: "personal", Ready: true, LocalOnly: header.Rights.LocalOnly, PreviewURL: previewURL, + }) + } + } + sort.Slice(items, func(left, right int) bool { + return items[left].Ref.ID < items[right].Ref.ID || items[left].Ref.ID == items[right].Ref.ID && items[left].Ref.Version < items[right].Ref.Version + }) + return items, nil +} + +func (repository *Repository) readPersonal(ref Ref) (Bundle, error) { + path, err := repository.personalPath(ref) + if err != nil { + return Bundle{}, err + } + return readBundleDirectory(path, ref) +} + +func (repository *Repository) readPersonalPreview(ref Ref) (Asset, error) { + bundle, err := repository.readPersonal(ref) + if err != nil { + return Asset{}, err + } + preview, exists := bundle.Files["preview.webp"] + if !exists { + return Asset{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme preview is unavailable"} + } + return Asset{Bytes: preview, MIMEType: "image/webp"}, nil +} + +func (repository *Repository) personalPath(ref Ref) (string, error) { + if !validRef(ref) { + return "", Error{Code: "THEME_NOT_FOUND", Message: "Theme reference is invalid"} + } + return filepath.Join(repository.personalRoot, "themes", ref.ID, ref.Version), nil +} + +func (repository *Repository) personalIDPath(id string) (string, error) { + if len(id) < 3 || len(id) > 80 || !themeIDPattern.MatchString(id) { + return "", Error{Code: "THEME_NOT_FOUND", Message: "Theme ID is invalid"} + } + return filepath.Join(repository.personalRoot, "themes", id), nil +} + +func unpack(contents []byte) (Bundle, error) { + if len(contents) == 0 || len(contents) > maxArchiveBytes { + return Bundle{}, Error{Code: "PACKAGE_TOO_LARGE", Message: "Theme archive exceeds its size limit"} + } + reader, err := zip.NewReader(bytes.NewReader(contents), int64(len(contents))) + if err != nil { + return Bundle{}, Error{Code: "STUDIO_IMPORT_INVALID", Message: "Theme archive is invalid"} + } + files := make(map[string][]byte, len(reader.File)) + seen := make(map[string]struct{}, len(reader.File)) + expanded := int64(0) + for _, file := range reader.File { + if !safeArchiveName(file.Name) || file.Mode()&os.ModeSymlink != 0 { + return Bundle{}, Error{Code: "ARCHIVE_ENTRY_UNSAFE", Message: "Theme archive contains an unsafe entry"} + } + folded := strings.ToLower(file.Name) + if _, exists := seen[folded]; exists { + return Bundle{}, Error{Code: "ARCHIVE_ENTRY_DUPLICATE", Message: "Theme archive contains duplicate entries"} + } + seen[folded] = struct{}{} + if file.Name != "theme.json" && file.Name != "manifest.json" && file.Name != "preview.webp" && !safeThemePath(file.Name) { + return Bundle{}, Error{Code: "ARCHIVE_ENTRY_UNSUPPORTED", Message: "Theme archive contains an unsupported entry"} + } + if file.UncompressedSize64 == 0 || file.UncompressedSize64 > maxExpandedBytes || expanded+int64(file.UncompressedSize64) > maxExpandedBytes { + return Bundle{}, Error{Code: "PACKAGE_EXPANDED_TOO_LARGE", Message: "Theme archive expanded size exceeds its limit"} + } + expanded += int64(file.UncompressedSize64) + contents, err := readZipFile(file, int64(file.UncompressedSize64)) + if err != nil { + return Bundle{}, err + } + files[file.Name] = contents + } + themeBytes, hasTheme := files["theme.json"] + manifestBytes, hasManifest := files["manifest.json"] + if !hasTheme || !hasManifest { + return Bundle{}, Error{Code: "ARCHIVE_REQUIRED_FILE_MISSING", Message: "Theme archive requires theme.json and manifest.json"} + } + var manifest archiveManifest + if err := decodeStrict(manifestBytes, &manifest); err != nil || manifest.SchemaVersion != 1 || !validRef(Ref{ID: manifest.ThemeID, Version: manifest.ThemeVersion}) { + return Bundle{}, Error{Code: "ARCHIVE_MANIFEST_INVALID", Message: "Theme archive manifest is invalid"} + } + actual := make([]string, 0, len(files)-1) + for name := range files { + if name != "manifest.json" { + actual = append(actual, name) + } + } + sort.Strings(actual) + expected := make([]string, 0, len(manifest.Files)) + for name := range manifest.Files { + expected = append(expected, name) + } + sort.Strings(expected) + if strings.Join(actual, "\x00") != strings.Join(expected, "\x00") { + return Bundle{}, Error{Code: "ARCHIVE_MANIFEST_MISMATCH", Message: "Theme archive entries differ from manifest"} + } + for name, expectedFile := range manifest.Files { + contents := files[name] + if len(contents) != expectedFile.Bytes || expectedFile.SHA256 != hash(contents) { + return Bundle{}, Error{Code: "ARCHIVE_HASH_MISMATCH", Message: "Theme archive file hash differs from manifest"} + } + } + normalized, header, err := normalizeDocument(themeBytes) + if err != nil { + return Bundle{}, err + } + ref := Ref{ID: header.ID, Version: header.Version} + if ref.ID != manifest.ThemeID || ref.Version != manifest.ThemeVersion { + return Bundle{}, Error{Code: "ARCHIVE_IDENTITY_MISMATCH", Message: "Theme archive identity differs from manifest"} + } + bundle := Bundle{Ref: ref, Document: normalized, Files: withoutManifest(files)} + if err := validateBundleAssets(bundle); err != nil { + return Bundle{}, err + } + return bundle, nil +} + +func pack(bundle Bundle) ([]byte, error) { + normalized, err := normalizeBundle(bundle) + if err != nil { + return nil, err + } + files := make(map[string][]byte, len(normalized.Files)+1) + files["theme.json"] = normalized.Document + for name, contents := range normalized.Files { + files[name] = contents + } + manifest := archiveManifest{SchemaVersion: 1, ThemeID: normalized.Ref.ID, ThemeVersion: normalized.Ref.Version, Files: make(map[string]struct { + Bytes int `json:"bytes"` + SHA256 string `json:"sha256"` + }, len(files))} + for name, contents := range files { + manifest.Files[name] = struct { + Bytes int `json:"bytes"` + SHA256 string `json:"sha256"` + }{Bytes: len(contents), SHA256: hash(contents)} + } + manifestBytes, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + return nil, err + } + files["manifest.json"] = append(manifestBytes, '\n') + var output bytes.Buffer + writer := zip.NewWriter(&output) + names := make([]string, 0, len(files)) + for name := range files { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + header := &zip.FileHeader{Name: name, Method: zip.Deflate} + header.SetModTime(time.Unix(0, 0).UTC()) + entry, err := writer.CreateHeader(header) + if err != nil { + return nil, err + } + if _, err := entry.Write(files[name]); err != nil { + return nil, err + } + } + if err := writer.Close(); err != nil { + return nil, err + } + if output.Len() > maxArchiveBytes { + return nil, Error{Code: "PACKAGE_TOO_LARGE", Message: "Theme archive exceeds its size limit"} + } + return output.Bytes(), nil +} + +func normalizeBundle(bundle Bundle) (Bundle, error) { + document, header, err := normalizeDocument(bundle.Document) + if err != nil { + return Bundle{}, err + } + if bundle.Ref.ID != "" && bundle.Ref != (Ref{ID: header.ID, Version: header.Version}) { + return Bundle{}, Error{Code: "ARCHIVE_IDENTITY_MISMATCH", Message: "Theme bundle identity is inconsistent"} + } + normalized := Bundle{Ref: Ref{ID: header.ID, Version: header.Version}, Document: document, Files: cloneFiles(bundle.Files)} + if err := validateBundleAssets(normalized); err != nil { + return Bundle{}, err + } + return normalized, nil +} + +func readBundleDirectory(directory string, expected Ref) (Bundle, error) { + info, err := os.Stat(directory) + if errors.Is(err, os.ErrNotExist) { + return Bundle{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme does not exist"} + } + if err != nil || !info.IsDir() { + return Bundle{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme storage is invalid"} + } + document, err := os.ReadFile(filepath.Join(directory, "theme.json")) + if err != nil { + return Bundle{}, err + } + normalized, header, err := normalizeDocument(document) + if err != nil { + return Bundle{}, err + } + ref := Ref{ID: header.ID, Version: header.Version} + if expected != (Ref{}) && ref != expected { + return Bundle{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Stored theme identity is inconsistent"} + } + paths, err := assetPaths(normalized) + if err != nil { + return Bundle{}, err + } + files := make(map[string][]byte, len(paths)+1) + for _, path := range paths { + contents, err := readAsset(filepath.Join(directory, filepath.FromSlash(path)), path) + if err != nil { + return Bundle{}, err + } + files[path] = contents + } + if contents, err := readAsset(filepath.Join(directory, "preview.webp"), "preview.webp"); err == nil { + files["preview.webp"] = contents + } else if !errors.Is(err, os.ErrNotExist) { + return Bundle{}, err + } + return Bundle{Ref: ref, Document: normalized, Files: files}, nil +} + +func writeBundleDirectory(directory string, bundle Bundle) error { + if err := writeAtomicFile(filepath.Join(directory, "theme.json"), bundle.Document); err != nil { + return err + } + for name, contents := range bundle.Files { + if !safeAssetName(name) { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme file path is invalid"} + } + path := filepath.Join(directory, filepath.FromSlash(name)) + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return err + } + if err := writeAtomicFile(path, contents); err != nil { + return err + } + } + return nil +} + +func writeAtomicFile(path string, contents []byte) error { + temporary, err := os.CreateTemp(filepath.Dir(path), ".openchatgptskin-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + committed := false + defer func() { + if !committed { + _ = os.Remove(temporaryPath) + } + }() + if _, err := temporary.Write(contents); err != nil { + temporary.Close() + return err + } + if err := temporary.Sync(); err != nil { + temporary.Close() + return err + } + if err := temporary.Close(); err != nil { + return err + } + if err := os.Rename(temporaryPath, path); err != nil { + return err + } + committed = true + return nil +} + +func normalizeDocument(contents []byte) ([]byte, themeHeader, error) { + return normalizeDocumentWithOptions(contents, true) +} + +func normalizeDraftDocument(contents []byte) ([]byte, themeHeader, error) { + return normalizeDocumentWithOptions(contents, false) +} + +func normalizeDocumentWithOptions(contents []byte, requireBackground bool) ([]byte, themeHeader, error) { + decoder := json.NewDecoder(bytes.NewReader(contents)) + decoder.UseNumber() + var document map[string]any + if err := decoder.Decode(&document); err != nil { + return nil, themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme document is invalid"} + } + version, err := schemaVersion(document) + if err != nil || version < 1 || version > 4 { + return nil, themeHeader{}, Error{Code: "THEME_SCHEMA_VERSION_UNSUPPORTED", Message: "Theme schema version is unsupported"} + } + if version < 4 { + migrateToV4(document, version) + } else { + ensureV4Defaults(document) + } + document["schemaVersion"] = 4 + if err := validateDocumentShape(document, requireBackground); err != nil { + return nil, themeHeader{}, err + } + normalized, err := json.MarshalIndent(document, "", " ") + if err != nil { + return nil, themeHeader{}, err + } + normalized = append(normalized, '\n') + header, err := headerFromDocument(normalized) + if err != nil { + return nil, themeHeader{}, err + } + return normalized, header, nil +} + +func schemaVersion(document map[string]any) (int, error) { + value, ok := document["schemaVersion"].(json.Number) + if !ok { + return 0, errors.New("schema version is invalid") + } + version, err := value.Int64() + return int(version), err +} + +func validateDocumentShape(document map[string]any, requireBackground bool) error { + for name := range document { + switch name { + case "schemaVersion", "kind", "appearance", "id", "name", "description", "version", "author", "metadata", "assets", "colors", "typography", "background", "surfaces", "decorations", "layout", "rights", "interfaceImages", "home", "welcome", "composition": + default: + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme document has an unknown field"} + } + } + for _, name := range []string{"kind", "id", "name", "version", "author", "assets", "colors", "typography", "background", "decorations", "layout", "rights"} { + if _, exists := document[name]; !exists { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme document has a required field missing"} + } + } + kind, _ := document["kind"].(string) + if kind != "theme" && kind != "recipe" { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme kind is invalid"} + } + assets, ok := document["assets"].(map[string]any) + if !ok { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme assets are invalid"} + } + if kind == "theme" && requireBackground { + if background, exists := assets["background"].(string); !exists || !safeThemePath(background) { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme requires a valid background asset"} + } + } + return validateAssetRelationships(document, assets) +} + +// validateAssetRelationships is intentionally kept beside archive validation: +// these relationships must be identical for imported, saved and exported +// themes rather than reimplemented by each Studio route. +func validateAssetRelationships(document, assets map[string]any) error { + decorations, err := stringMap(assets["decorations"], "theme decoration assets") + if err != nil { + return err + } + fonts, err := stringMap(assets["fonts"], "theme font assets") + if err != nil { + return err + } + if icons, err := stringMap(assets["suggestionIcons"], "suggestion icons"); err != nil { + return err + } else { + for key := range icons { + if key != "card1" && key != "card2" && key != "card3" && key != "card4" { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Suggestion icon slot is invalid"} + } + } + } + if projects, exists := assets["projectIcons"]; exists { + entries, ok := projects.([]any) + if !ok || len(entries) < 1 || len(entries) > 12 { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Project icons are invalid"} + } + } + if typography, exists := document["typography"]; exists { + value, ok := typography.(map[string]any) + if !ok { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme typography is invalid"} + } + for _, field := range []string{"uiFontAssetKey", "codeFontAssetKey", "displayFontAssetKey"} { + if key, exists := value[field]; exists { + name, ok := key.(string) + if !ok || fonts[name] == "" { + return Error{Code: "THEME_DISPLAY_FONT_MISSING", Message: "Theme font key is not declared"} + } + } + } + } + entries, ok := document["decorations"].([]any) + if !ok || len(entries) > 16 { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme decorations are invalid"} + } + for _, entry := range entries { + item, ok := entry.(map[string]any) + if !ok { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme decoration is invalid"} + } + if item["type"] == "image" { + key, ok := item["assetKey"].(string) + if !ok || decorations[key] == "" { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Image decoration asset is not declared"} + } + } + } + composition, ok := document["composition"].(map[string]any) + if !ok { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition is invalid"} + } + layers, ok := composition["layers"].([]any) + if !ok || len(layers) > 24 { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition layer count is invalid"} + } + seen := map[string]struct{}{} + for _, entry := range layers { + layer, ok := entry.(map[string]any) + if !ok { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition layer is invalid"} + } + id, ok := layer["id"].(string) + if !ok || id == "" || len(id) > 40 || !themeIDPattern.MatchString(id) { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition layer ID is invalid"} + } + if _, exists := seen[id]; exists { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition layer ID is duplicated"} + } + seen[id] = struct{}{} + asset, ok := layer["asset"].(map[string]any) + if !ok { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition asset is invalid"} + } + kind, ok := asset["kind"].(string) + if !ok || kind != "portrait" && kind != "decoration" { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition asset kind is invalid"} + } + if kind == "portrait" { + if portrait, ok := assets["portrait"].(string); !ok || portrait == "" { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition portrait is not declared"} + } + } else { + key, ok := asset["assetKey"].(string) + if !ok || decorations[key] == "" { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition decoration is not declared"} + } + } + if surface, ok := layer["surface"].(string); !ok || !validCompositionSurface(surface) { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition surface is invalid"} + } + if _, ok := layer["required"].(bool); !ok { + return Error{Code: "THEME_COMPOSITION_INVALID", Message: "Theme composition required flag is invalid"} + } + } + return nil +} + +func stringMap(value any, label string) (map[string]string, error) { + if value == nil { + return map[string]string{}, nil + } + raw, ok := value.(map[string]any) + if !ok { + return nil, Error{Code: "THEME_SCHEMA_INVALID", Message: label + " are invalid"} + } + result := make(map[string]string, len(raw)) + for key, value := range raw { + path, ok := value.(string) + if !ok || !safeThemePath(path) { + return nil, Error{Code: "THEME_SCHEMA_INVALID", Message: label + " contain an invalid path"} + } + result[key] = path + } + return result, nil +} + +func validCompositionSurface(value string) bool { + for _, allowed := range []string{"viewport", "home-hero", "task-background", "content-layer", "sidebar", "composer"} { + if value == allowed { + return true + } + } + return false +} + +func migrateToV4(document map[string]any, version int) { + if _, exists := document["appearance"]; !exists { + document["appearance"] = "auto" + } + colors, _ := document["colors"].(map[string]any) + if colors != nil { + for name, fallback := range map[string]string{"textSecondary": "text", "link": "accent", "inputText": "text", "placeholder": "muted", "codeText": "text"} { + if _, exists := colors[name]; !exists { + colors[name] = colors[fallback] + } + } + } + ensureV4Defaults(document) + _ = version +} + +func ensureV4Defaults(document map[string]any) { + if _, exists := document["surfaces"]; !exists { + document["surfaces"] = map[string]any{"baseOpacity": 0.68, "elevatedOpacity": 0.92, "terminalOpacity": 0.82, "blur": 0} + } + background, _ := document["background"].(map[string]any) + if background != nil { + for name, fallback := range map[string]any{"safeArea": "auto", "taskMode": "full", "taskOpacity": 0.82} { + if _, exists := background[name]; !exists { + background[name] = fallback + } + } + } + typography, _ := document["typography"].(map[string]any) + if typography != nil { + if _, exists := typography["displayFamily"]; !exists { + typography["displayFamily"] = typography["uiFamily"] + } + if _, exists := typography["displayWeight"]; !exists { + typography["displayWeight"] = typography["uiWeight"] + } + if _, exists := typography["displayLineHeight"]; !exists { + typography["displayLineHeight"] = typography["lineHeight"] + } + if _, exists := typography["displayLetterSpacing"]; !exists { + typography["displayLetterSpacing"] = 0 + } + if _, exists := typography["displaySize"]; !exists { + typography["displaySize"] = 28 + } + } + if _, exists := document["interfaceImages"]; !exists { + document["interfaceImages"] = map[string]any{"profileAvatarSize": 24, "suggestionIconSize": 20, "projectIconSize": 16} + } + if _, exists := document["composition"]; !exists { + document["composition"] = map[string]any{"layers": []any{}} + } +} + +func headerFromDocument(contents []byte) (themeHeader, error) { + var header themeHeader + if err := json.Unmarshal(contents, &header); err != nil { + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme metadata is invalid"} + } + if header.SchemaVersion != 4 || !validRef(Ref{ID: header.ID, Version: header.Version}) || header.Name == "" || header.Author == "" || (header.Kind != "theme" && header.Kind != "recipe") { + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme metadata is invalid"} + } + return header, nil +} + +func assetPaths(document []byte) ([]string, error) { + var value struct { + Assets struct { + Background string `json:"background"` + Portrait string `json:"portrait"` + ProfileAvatar string `json:"profileAvatar"` + Decorations map[string]string `json:"decorations"` + Fonts map[string]string `json:"fonts"` + SuggestionIcons map[string]string `json:"suggestionIcons"` + ProjectIcons []string `json:"projectIcons"` + } `json:"assets"` + } + if err := json.Unmarshal(document, &value); err != nil { + return nil, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme assets are invalid"} + } + paths := []string{value.Assets.Background, value.Assets.Portrait, value.Assets.ProfileAvatar} + for _, entries := range []map[string]string{value.Assets.Decorations, value.Assets.Fonts, value.Assets.SuggestionIcons} { + for _, path := range entries { + paths = append(paths, path) + } + } + paths = append(paths, value.Assets.ProjectIcons...) + seen := make(map[string]struct{}, len(paths)) + output := make([]string, 0, len(paths)) + for _, path := range paths { + if path == "" { + continue + } + if !safeThemePath(path) { + return nil, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme asset path is invalid"} + } + if _, exists := seen[path]; !exists { + seen[path] = struct{}{} + output = append(output, path) + } + } + sort.Strings(output) + return output, nil +} + +func validateBundleAssets(bundle Bundle) error { + paths, err := assetPaths(bundle.Document) + if err != nil { + return err + } + declared := make(map[string]struct{}, len(paths)) + for _, path := range paths { + declared[path] = struct{}{} + contents, exists := bundle.Files[path] + if !exists || len(contents) == 0 || len(contents) > assetLimit(path) { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme declared asset is invalid"} + } + } + for path, contents := range bundle.Files { + if path == "preview.webp" { + if len(contents) == 0 || len(contents) > 2*1024*1024 { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme preview is invalid"} + } + continue + } + if _, exists := declared[path]; !exists || len(contents) == 0 || len(contents) > assetLimit(path) { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme has an undeclared or oversized asset"} + } + } + return nil +} + +func assetLimit(path string) int { + if strings.HasSuffix(strings.ToLower(path), ".woff2") { + return maxFontBytes + } + return maxAssetBytes +} + +func safeArchiveName(name string) bool { + return name != "" && name == strings.ToValidUTF8(name, "") && !strings.HasPrefix(name, "/") && !strings.Contains(name, "\\") && + !strings.Contains(name, ":") && !strings.Contains(name, "\x00") && !strings.HasSuffix(name, "/") && + !strings.Contains(name, "../") && !strings.HasPrefix(name, "..") +} + +func safeThemePath(path string) bool { + if path == "" || len(path) > 240 || strings.Contains(path, "\\") || strings.Contains(path, "\x00") || strings.HasPrefix(path, "/") || strings.Contains(path, "../") || strings.HasPrefix(path, "..") { + return false + } + lower := strings.ToLower(path) + return (strings.HasPrefix(path, "assets/") && (strings.HasSuffix(lower, ".png") || strings.HasSuffix(lower, ".jpg") || strings.HasSuffix(lower, ".jpeg") || strings.HasSuffix(lower, ".webp"))) || + (strings.HasPrefix(path, "fonts/") && strings.HasSuffix(lower, ".woff2")) +} + +func safeAssetName(path string) bool { return path == "preview.webp" || safeThemePath(path) } + +func readZipFile(file *zip.File, limit int64) ([]byte, error) { + reader, err := file.Open() + if err != nil { + return nil, err + } + defer reader.Close() + contents, err := io.ReadAll(io.LimitReader(reader, limit+1)) + if err != nil || int64(len(contents)) > limit { + return nil, Error{Code: "ARCHIVE_ENTRY_TOO_LARGE", Message: "Theme archive entry exceeds its limit"} + } + return contents, nil +} + +func readAsset(path, name string) ([]byte, error) { + info, err := os.Stat(path) + if err != nil { + return nil, err + } + if !info.Mode().IsRegular() || info.Size() < 1 || info.Size() > int64(assetLimit(name)) { + return nil, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme asset is invalid"} + } + return os.ReadFile(path) +} + +func cloneFiles(files map[string][]byte) map[string][]byte { + result := make(map[string][]byte, len(files)) + for name, contents := range files { + result[name] = append([]byte(nil), contents...) + } + return result +} + +func withoutManifest(files map[string][]byte) map[string][]byte { + result := cloneFiles(files) + delete(result, "manifest.json") + delete(result, "theme.json") + return result +} + +func bundlesEqual(left, right Bundle) bool { + if left.Ref != right.Ref || !bytes.Equal(left.Document, right.Document) || len(left.Files) != len(right.Files) { + return false + } + for name, contents := range left.Files { + if !bytes.Equal(contents, right.Files[name]) { + return false + } + } + return true +} + +func hash(contents []byte) string { + digest := sha256.Sum256(contents) + return hex.EncodeToString(digest[:]) +} + +func removeOwnedDirectory(path string) error { + info, err := os.Lstat(path) + if errors.Is(err, os.ErrNotExist) { + return Error{Code: "THEME_NOT_FOUND", Message: "Personal theme does not exist"} + } + if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { + return Error{Code: "STUDIO_DELETE_FAILED", Message: "Personal theme storage is invalid"} + } + return os.RemoveAll(path) +} + +func removeEmptyDirectory(path string) error { + err := os.Remove(path) + if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrExist) { + return nil + } + return err +} + +func headerFromBundle(bundle Bundle) (themeHeader, error) { return headerFromDocument(bundle.Document) } + +func formatRef(ref Ref) string { return fmt.Sprintf("%s@%s", ref.ID, ref.Version) } diff --git a/host/go/internal/themerepo/repository.go b/host/go/internal/themerepo/repository.go new file mode 100644 index 0000000..90419cd --- /dev/null +++ b/host/go/internal/themerepo/repository.go @@ -0,0 +1,359 @@ +package themerepo + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" +) + +const maxPreviewBytes = 16 * 1024 * 1024 + +var ( + themeIDPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) + themeVersionPattern = regexp.MustCompile(`^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$`) +) + +type Error struct { + Code string + Message string +} + +func (err Error) Error() string { return err.Message } + +type Ref struct { + ID string `json:"id"` + Version string `json:"version"` +} + +type Localized struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} + +type ListItem struct { + Ref Ref `json:"ref"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Author string `json:"author"` + Homepage *string `json:"homepage"` + Localized map[string]Localized `json:"localized,omitempty"` + Source string `json:"source"` + Ready bool `json:"ready"` + LocalOnly bool `json:"localOnly"` + PreviewURL *string `json:"previewUrl"` +} + +type Library struct { + Themes []ListItem `json:"themes"` +} + +type Asset struct { + Bytes []byte + MIMEType string +} + +type Repository struct { + root string + personalRoot string +} + +type catalog struct { + SchemaVersion int `json:"schemaVersion"` + Builtins []catalogEntry `json:"builtins"` + Recipes []catalogEntry `json:"recipes"` +} + +type catalogEntry struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Kind string `json:"kind"` + Path string `json:"path"` + Ready bool `json:"ready"` + LocalOnly bool `json:"localOnly"` + LicenseID string `json:"licenseId"` + Preview string `json:"preview,omitempty"` +} + +type themeHeader struct { + SchemaVersion int `json:"schemaVersion"` + Kind string `json:"kind"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Version string `json:"version"` + Author string `json:"author"` + Metadata struct { + Homepage *string `json:"homepage"` + Localized map[string]Localized `json:"localized,omitempty"` + } `json:"metadata,omitempty"` + Rights struct { + LocalOnly bool `json:"localOnly"` + } `json:"rights"` +} + +func Open(root string) (*Repository, error) { + absolute, err := filepath.Abs(root) + if err != nil { + return nil, err + } + return &Repository{root: absolute}, nil +} + +func OpenWithPersonal(root, personalRoot string) (*Repository, error) { + repository, err := Open(root) + if err != nil { + return nil, err + } + if personalRoot == "" { + return repository, nil + } + absolute, err := filepath.Abs(personalRoot) + if err != nil { + return nil, err + } + repository.personalRoot = absolute + return repository, nil +} + +func (repository *Repository) List() (Library, error) { + catalog, err := repository.loadCatalog() + if err != nil { + return Library{}, err + } + items := make([]ListItem, 0, len(catalog.Builtins)+len(catalog.Recipes)) + for _, entry := range append(catalog.Builtins, catalog.Recipes...) { + item, err := repository.libraryItem(entry) + if err != nil { + return Library{}, err + } + items = append(items, item) + } + personal, err := repository.listPersonal() + if err != nil { + return Library{}, err + } + items = append(items, personal...) + return Library{Themes: items}, nil +} + +// BuiltinRefs reads only the signed-in-package catalog. Studio startup uses it +// to establish reserved IDs without touching a possibly stale user store. +func (repository *Repository) BuiltinRefs() ([]Ref, error) { + catalog, err := repository.loadCatalog() + if err != nil { + return nil, err + } + refs := make([]Ref, 0, len(catalog.Builtins)) + for _, entry := range catalog.Builtins { + refs = append(refs, Ref{ID: entry.ID, Version: entry.Version}) + } + return refs, nil +} + +func (repository *Repository) Preview(source string, ref Ref) (Asset, error) { + if source == "personal" { + return repository.readPersonalPreview(ref) + } + if source != "builtin" { + return Asset{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme preview source is unavailable"} + } + entry, err := repository.findBuiltin(ref) + if err != nil { + return Asset{}, err + } + if entry.Preview == "" { + return Asset{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme preview is unavailable"} + } + path, err := repository.safePath(entry.Preview) + if err != nil { + return Asset{}, err + } + info, err := os.Stat(path) + if err != nil { + return Asset{}, err + } + if !info.Mode().IsRegular() || info.Size() < 1 || info.Size() > maxPreviewBytes { + return Asset{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme preview is invalid"} + } + contents, err := os.ReadFile(path) + if err != nil { + return Asset{}, err + } + return Asset{Bytes: contents, MIMEType: "image/webp"}, nil +} + +func (repository *Repository) loadCatalog() (catalog, error) { + contents, err := os.ReadFile(filepath.Join(repository.root, "catalog.json")) + if err != nil { + return catalog{}, err + } + var value catalog + if err := decodeStrict(contents, &value); err != nil { + return catalog{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme catalog is invalid"} + } + if value.SchemaVersion != 1 { + return catalog{}, Error{Code: "THEME_SCHEMA_VERSION_UNSUPPORTED", Message: "Theme catalog schema is unsupported"} + } + seen := make(map[string]struct{}, len(value.Builtins)+len(value.Recipes)) + for _, entry := range value.Builtins { + if entry.Kind != "theme" { + return catalog{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Builtin catalog contains a non-theme entry"} + } + if err := validateCatalogEntry(entry); err != nil { + return catalog{}, err + } + if _, exists := seen[entry.ID]; exists { + return catalog{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme catalog has duplicate IDs"} + } + seen[entry.ID] = struct{}{} + } + for _, entry := range value.Recipes { + if entry.Kind != "recipe" { + return catalog{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Recipe catalog contains a non-recipe entry"} + } + if err := validateCatalogEntry(entry); err != nil { + return catalog{}, err + } + if _, exists := seen[entry.ID]; exists { + return catalog{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme catalog has duplicate IDs"} + } + seen[entry.ID] = struct{}{} + } + return value, nil +} + +func validateCatalogEntry(entry catalogEntry) error { + if !validRef(Ref{ID: entry.ID, Version: entry.Version}) || entry.Name == "" || entry.LicenseID == "" { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme catalog entry is invalid"} + } + if entry.Kind == "theme" { + expectedPath := filepath.ToSlash(filepath.Join("builtin", entry.ID)) + expectedPreview := filepath.ToSlash(filepath.Join(expectedPath, "preview.webp")) + if entry.Path != expectedPath || entry.Preview != expectedPreview || !entry.Ready || entry.LocalOnly { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Builtin theme catalog entry is inconsistent"} + } + return nil + } + if entry.Kind == "recipe" { + expectedPath := filepath.ToSlash(filepath.Join("recipes", entry.ID)) + if entry.Path != expectedPath || entry.Ready || !entry.LocalOnly || entry.Preview != "" { + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme recipe catalog entry is inconsistent"} + } + return nil + } + return Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme catalog kind is invalid"} +} + +func (repository *Repository) libraryItem(entry catalogEntry) (ListItem, error) { + header, err := repository.readHeader(entry) + if err != nil { + return ListItem{}, err + } + if header.ID != entry.ID || header.Version != entry.Version || header.Name != entry.Name || header.Kind != entry.Kind { + return ListItem{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme metadata differs from its catalog entry"} + } + if entry.Kind == "theme" && header.Rights.LocalOnly { + return ListItem{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Builtin theme cannot be local only"} + } + var previewURL *string + if entry.Preview != "" { + value := "/api/theme-preview?source=builtin&id=" + entry.ID + "&version=" + entry.Version + previewURL = &value + } + return ListItem{ + Ref: Ref{ID: entry.ID, Version: entry.Version}, + Name: header.Name, + Description: header.Description, + Author: header.Author, + Homepage: header.Metadata.Homepage, + Localized: header.Metadata.Localized, + Source: map[bool]string{true: "builtin", false: "recipe"}[entry.Kind == "theme"], + Ready: entry.Ready, + LocalOnly: entry.LocalOnly, + PreviewURL: previewURL, + }, nil +} + +func (repository *Repository) findBuiltin(ref Ref) (catalogEntry, error) { + if !validRef(ref) { + return catalogEntry{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme reference is invalid"} + } + catalog, err := repository.loadCatalog() + if err != nil { + return catalogEntry{}, err + } + for _, entry := range catalog.Builtins { + if entry.ID == ref.ID && entry.Version == ref.Version { + return entry, nil + } + } + return catalogEntry{}, Error{Code: "THEME_NOT_FOUND", Message: "Theme does not exist"} +} + +func (repository *Repository) readHeader(entry catalogEntry) (themeHeader, error) { + path, err := repository.safePath(filepath.ToSlash(filepath.Join(entry.Path, "theme.json"))) + if err != nil { + return themeHeader{}, err + } + contents, err := os.ReadFile(path) + if err != nil { + return themeHeader{}, err + } + var document map[string]json.RawMessage + if err := decodeStrict(contents, &document); err != nil { + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme document is invalid"} + } + for name := range document { + switch name { + case "schemaVersion", "kind", "appearance", "id", "name", "description", "version", "author", "metadata", "assets", "colors", "typography", "background", "surfaces", "decorations", "layout", "rights", "interfaceImages", "home", "welcome", "composition": + default: + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme document has an unknown field"} + } + } + var header themeHeader + if err := json.Unmarshal(contents, &header); err != nil { + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme metadata is invalid"} + } + if header.SchemaVersion != 4 || !validRef(Ref{ID: header.ID, Version: header.Version}) || header.Name == "" || header.Author == "" { + return themeHeader{}, Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme metadata is invalid"} + } + return header, nil +} + +func (repository *Repository) safePath(relative string) (string, error) { + if relative == "" || filepath.IsAbs(relative) { + return "", Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme path is invalid"} + } + clean := filepath.Clean(filepath.FromSlash(relative)) + if clean == "." || clean == ".." || len(clean) >= 3 && clean[:3] == ".."+string(filepath.Separator) { + return "", Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme path escapes its root"} + } + path := filepath.Join(repository.root, clean) + relativeToRoot, err := filepath.Rel(repository.root, path) + if err != nil || relativeToRoot == ".." || len(relativeToRoot) >= 3 && relativeToRoot[:3] == ".."+string(filepath.Separator) { + return "", Error{Code: "THEME_SCHEMA_INVALID", Message: "Theme path escapes its root"} + } + return path, nil +} + +func validRef(ref Ref) bool { + return len(ref.ID) >= 3 && len(ref.ID) <= 80 && themeIDPattern.MatchString(ref.ID) && themeVersionPattern.MatchString(ref.Version) +} + +func decodeStrict(contents []byte, output any) error { + decoder := json.NewDecoder(bytes.NewReader(contents)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(output); err != nil { + return err + } + if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { + return fmt.Errorf("JSON has trailing content") + } + return nil +} diff --git a/host/go/internal/themerepo/repository_test.go b/host/go/internal/themerepo/repository_test.go new file mode 100644 index 0000000..dba2000 --- /dev/null +++ b/host/go/internal/themerepo/repository_test.go @@ -0,0 +1,149 @@ +package themerepo + +import ( + "archive/zip" + "bytes" + "os" + "path/filepath" + "testing" +) + +func writeBuiltin(t *testing.T, root string) { + t.Helper() + directory := filepath.Join(root, "builtin", "mountain-mist") + if err := os.MkdirAll(directory, 0o700); err != nil { + t.Fatal(err) + } + catalog := `{"schemaVersion":1,"builtins":[{"id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","kind":"theme","path":"builtin/mountain-mist","ready":true,"localOnly":false,"licenseId":"LicenseRef-Test","preview":"builtin/mountain-mist/preview.webp"}],"recipes":[]}` + theme := `{"schemaVersion":4,"kind":"theme","appearance":"light","id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","author":"OpenChatGPTSkin","assets":{"background":"assets/background.webp"},"colors":{},"typography":{},"background":{},"decorations":[],"layout":{},"rights":{"localOnly":false}}` + if err := os.WriteFile(filepath.Join(root, "catalog.json"), []byte(catalog), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "theme.json"), []byte(theme), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "preview.webp"), []byte("RIFF0000WEBP"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(directory, "assets"), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "assets", "background.webp"), []byte("RIFF0000WEBP"), 0o600); err != nil { + t.Fatal(err) + } +} + +func TestRepositoryListsBuiltinsAndReadsDeclaredPreview(t *testing.T) { + root := t.TempDir() + writeBuiltin(t, root) + repository, err := Open(root) + if err != nil { + t.Fatal(err) + } + library, err := repository.List() + if err != nil { + t.Fatal(err) + } + if len(library.Themes) != 1 || library.Themes[0].Source != "builtin" { + t.Fatalf("library = %+v", library) + } + asset, err := repository.Preview("builtin", Ref{ID: "mountain-mist", Version: "1.3.0"}) + if err != nil || asset.MIMEType != "image/webp" { + t.Fatalf("preview = %+v error=%v", asset, err) + } +} + +func TestRepositoryRejectsCatalogAndDocumentPathEscapes(t *testing.T) { + root := t.TempDir() + writeBuiltin(t, root) + if err := os.WriteFile(filepath.Join(root, "catalog.json"), []byte(`{"schemaVersion":1,"builtins":[{"id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","kind":"theme","path":"../escape","ready":true,"localOnly":false,"licenseId":"LicenseRef-Test","preview":"builtin/mountain-mist/preview.webp"}],"recipes":[]}`), 0o600); err != nil { + t.Fatal(err) + } + repository, err := Open(root) + if err != nil { + t.Fatal(err) + } + if _, err := repository.List(); err == nil { + t.Fatal("path escaping catalog entry was accepted") + } +} + +func TestRepositoryImportsExportsAndDeletesPersonalThemeAtomically(t *testing.T) { + builtinRoot := t.TempDir() + writeBuiltin(t, builtinRoot) + personalRoot := t.TempDir() + repository, err := OpenWithPersonal(builtinRoot, personalRoot) + if err != nil { + t.Fatal(err) + } + archive, err := repository.Export("builtin", Ref{ID: "mountain-mist", Version: "1.3.0"}) + if err != nil { + t.Fatal(err) + } + ref, err := repository.ImportArchive(archive) + if err != nil { + t.Fatal(err) + } + if ref != (Ref{ID: "mountain-mist", Version: "1.3.0"}) { + t.Fatalf("imported ref = %+v", ref) + } + library, err := repository.List() + if err != nil { + t.Fatal(err) + } + if len(library.Themes) != 2 || library.Themes[1].Source != "personal" { + t.Fatalf("library = %+v", library) + } + reexported, err := repository.Export("personal", ref) + if err != nil || !bytes.Equal(archive, reexported) { + t.Fatalf("personal export error=%v same=%v", err, bytes.Equal(archive, reexported)) + } + if err := repository.DeletePersonal(ref.ID, &ref.Version); err != nil { + t.Fatal(err) + } + if _, err := repository.Read("personal", ref); err == nil { + t.Fatal("deleted personal theme was still readable") + } +} + +func TestRepositoryRejectsUnsafeArchiveEntry(t *testing.T) { + var output bytes.Buffer + writer := zip.NewWriter(&output) + entry, err := writer.Create("../secret.txt") + if err != nil { + t.Fatal(err) + } + if _, err := entry.Write([]byte("secret")); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + if _, err := unpack(output.Bytes()); err == nil { + t.Fatal("unsafe archive entry was accepted") + } +} + +func TestRepositoryMigratesLegacyThemeAndRejectsUnknownFields(t *testing.T) { + legacy := []byte(`{"schemaVersion":1,"kind":"theme","id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","author":"OpenChatGPTSkin","assets":{"background":"assets/background.webp"},"colors":{"accent":"#000000","secondary":"#000000","text":"#000000","muted":"#000000","panel":"#000000","border":"#000000","success":"#000000","warning":"#000000","danger":"#000000","info":"#000000"},"typography":{"uiFamily":"Sans","codeFamily":"Mono","scale":1,"uiSize":14,"codeSize":13,"uiWeight":400,"codeWeight":400,"lineHeight":1.5},"background":{"positionX":0.5,"positionY":0.5,"scale":1,"blur":0,"brightness":1,"overlay":0},"decorations":[],"layout":{},"rights":{"licenseId":"LicenseRef-Test","localOnly":false}}`) + normalized, header, err := normalizeDocument(legacy) + if err != nil || header.SchemaVersion != 4 || !bytes.Contains(normalized, []byte(`"composition"`)) { + t.Fatalf("legacy normalization error=%v document=%s", err, normalized) + } + unknown := append([]byte(nil), normalized[:len(normalized)-2]...) + unknown = append(unknown, []byte(`,"unknown":true}`)...) + if _, _, err := normalizeDocument(unknown); err == nil { + t.Fatal("unknown theme field was accepted") + } +} + +func FuzzUnpackRejectsMalformedArchives(f *testing.F) { + f.Add([]byte("not a zip")) + f.Add([]byte("PK\x03\x04")) + f.Fuzz(func(t *testing.T, contents []byte) { + if len(contents) > maxArchiveBytes { + return + } + _, _ = unpack(contents) + }) +} diff --git a/host/go/internal/workspace/workspace.go b/host/go/internal/workspace/workspace.go new file mode 100644 index 0000000..30ca941 --- /dev/null +++ b/host/go/internal/workspace/workspace.go @@ -0,0 +1,1245 @@ +// Package workspace owns the mutable Theme Studio draft state. It deliberately +// stores the same draft.json shape and asset layout as the v0.2 Node host so a +// user can safely roll a dataRoot back while the Go cutover remains provisional. +package workspace + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "sync" + "time" + + imagepipeline "github.com/u2bo/OpenChatGPTSkin/host/go/internal/image" + "github.com/u2bo/OpenChatGPTSkin/host/go/internal/themerepo" +) + +const historyLimit = 50 + +var uuidPattern = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`) +var assetKeyPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) + +type Error struct { + Code string + Message string + NextAction string +} + +func (err Error) Error() string { return err.Message } + +func ErrorCode(err error) string { + var value Error + if errors.As(err, &value) { + return value.Code + } + return "INTERNAL" +} + +type Issue struct { + Code string `json:"code"` + Path string `json:"path"` + Message string `json:"message"` + Severity string `json:"severity"` +} + +// Draft is the public Studio contract. Theme deliberately remains raw JSON: +// the schema author source is TypeScript, while Go verifies and normalises it +// without inventing a second, lossy object model. +type Draft struct { + DraftID string `json:"draftId"` + Theme json.RawMessage `json:"theme"` + Revision int `json:"revision"` + UpdatedAt string `json:"updatedAt"` + SavedRef *themerepo.Ref `json:"savedRef"` + Dirty bool `json:"dirty"` + UndoAvailable bool `json:"undoAvailable"` + RedoAvailable bool `json:"redoAvailable"` + Issues []Issue `json:"issues"` + AssetURLs map[string]string `json:"assetUrls"` +} + +type CreateInput struct { + Source string + Ref themerepo.Ref + ThemeID string + Name string + ConflictResolution string +} + +type UploadInput struct { + DraftID string + ExpectedRevision int + Slot string + AssetKey string + FileName string + MIMEType string + Bytes []byte +} + +type ClearAssetInput struct { + DraftID string + ExpectedRevision int + Slot string + AssetKey string +} + +type record struct { + SchemaVersion int `json:"schemaVersion"` + DraftID string `json:"draftId"` + Theme json.RawMessage `json:"theme"` + Revision int `json:"revision"` + UpdatedAt string `json:"updatedAt"` + SavedRef *themerepo.Ref `json:"savedRef"` + Dirty bool `json:"dirty"` + Past []json.RawMessage `json:"past"` + Future []json.RawMessage `json:"future"` +} + +type Workspace struct { + repository *themerepo.Repository + root string + builtinIDs []string + mu sync.Mutex +} + +func New(repository *themerepo.Repository, root string) (*Workspace, error) { + if repository == nil || root == "" { + return nil, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Draft storage is unavailable"} + } + absolute, err := filepath.Abs(root) + if err != nil { + return nil, err + } + builtins, err := repository.BuiltinRefs() + if err != nil { + return nil, err + } + builtinIDs := make([]string, 0, len(builtins)) + for _, builtin := range builtins { + builtinIDs = append(builtinIDs, builtin.ID) + } + sort.Strings(builtinIDs) + workspace := &Workspace{repository: repository, root: absolute, builtinIDs: builtinIDs} + if err := os.MkdirAll(absolute, 0o700); err != nil { + return nil, err + } + if err := workspace.removeDuplicateDrafts(); err != nil { + return nil, err + } + return workspace, nil +} + +func (workspace *Workspace) Create(input CreateInput) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + if input.Source != "builtin" && input.Source != "personal" { + return Draft{}, Error{Code: "STUDIO_REQUEST_INVALID", Message: "Theme source is unavailable"} + } + bundle, err := workspace.repository.Read(input.Source, input.Ref) + if err != nil { + return Draft{}, err + } + return workspace.createFromBundleLocked(input, bundle) +} + +func (workspace *Workspace) Import(contents []byte) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + ref, err := workspace.repository.ImportArchive(contents) + if err != nil { + return Draft{}, Error{Code: "STUDIO_IMPORT_INVALID", Message: err.Error()} + } + bundle, err := workspace.repository.Read("personal", ref) + if err != nil { + return Draft{}, Error{Code: "STUDIO_IMPORT_INVALID", Message: err.Error()} + } + return workspace.createFromBundleLocked(CreateInput{ + Source: "personal", Ref: ref, ThemeID: ref.ID, ConflictResolution: "overwrite-existing", + }, bundle) +} + +func (workspace *Workspace) createFromBundleLocked(input CreateInput, bundle themerepo.Bundle) (Draft, error) { + theme, ref, err := workspace.prepareSourceTheme(bundle.Document, input) + if err != nil { + return Draft{}, err + } + group := workspace.groupKey(ref.ID) + existing, err := workspace.findByGroup(group) + if err != nil { + return Draft{}, err + } + if existing != nil && input.ConflictResolution == "" { + return Draft{}, Error{Code: "STUDIO_DRAFT_CONFLICT", Message: "该主题已有草稿", NextAction: "请选择加载已有草稿或覆盖现有草稿。"} + } + if existing != nil && input.ConflictResolution == "load-existing" { + return workspace.view(*existing) + } + if input.ConflictResolution != "" && input.ConflictResolution != "overwrite-existing" && input.ConflictResolution != "load-existing" { + return Draft{}, Error{Code: "STUDIO_REQUEST_INVALID", Message: "Draft conflict resolution is invalid"} + } + draftID := newUUID() + revision := 0 + if existing != nil { + draftID, revision = existing.DraftID, existing.Revision+1 + } + savedRef := (*themerepo.Ref)(nil) + if input.Source == "personal" { + value := input.Ref + savedRef = &value + } + now := time.Now().UTC().Format(time.RFC3339Nano) + next := record{SchemaVersion: 1, DraftID: draftID, Theme: theme, Revision: revision, UpdatedAt: now, SavedRef: savedRef, Dirty: savedRef == nil, Past: []json.RawMessage{}, Future: []json.RawMessage{}} + if err := workspace.writeBundleAssets(draftID, bundle, theme); err != nil { + return Draft{}, err + } + if err := workspace.writeRecord(next); err != nil { + return Draft{}, err + } + return workspace.view(next) +} + +func (workspace *Workspace) Open(draftID string) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return Draft{}, err + } + return workspace.view(record) +} + +func (workspace *Workspace) Latest() (*Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + records, err := workspace.listRecords() + if err != nil { + return nil, err + } + if len(records) == 0 { + return nil, nil + } + sort.Slice(records, func(left, right int) bool { + return records[left].UpdatedAt > records[right].UpdatedAt || records[left].UpdatedAt == records[right].UpdatedAt && records[left].DraftID > records[right].DraftID + }) + view, err := workspace.view(records[0]) + return &view, err +} + +func (workspace *Workspace) Update(draftID string, expectedRevision int, theme json.RawMessage) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return Draft{}, err + } + if err := assertRevision(record, expectedRevision); err != nil { + return Draft{}, err + } + normalized, ref, err := themerepo.NormalizeDraftDocument(theme) + if err != nil { + return Draft{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: err.Error()} + } + if workspace.isReservedID(ref.ID) { + return Draft{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme ID is reserved by the built-in catalog"} + } + if record.Theme != nil && bytes.Equal(normalized, record.Theme) { + return workspace.view(record) + } + next := mutateHistory(record, normalized) + if err := workspace.writeRecord(next); err != nil { + return Draft{}, err + } + return workspace.view(next) +} + +// Upload processes an asset completely before the draft is mutated. This +// preserves the one-upload/one-revision invariant and leaves the old draft +// untouched if decoding, limits, or a filesystem write fails. +func (workspace *Workspace) Upload(input UploadInput) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(input.DraftID) + if err != nil { + return Draft{}, err + } + if err := assertRevision(record, input.ExpectedRevision); err != nil { + return Draft{}, err + } + path, contents, err := normalizeAsset(input) + if err != nil { + return Draft{}, err + } + nextTheme, err := attachAsset(record.Theme, input, path) + if err != nil { + return Draft{}, err + } + normalized, _, err := themerepo.NormalizeDraftDocument(nextTheme) + if err != nil { + return Draft{}, Error{Code: "STUDIO_ASSET_INVALID", Message: err.Error()} + } + target := workspace.assetPath(input.DraftID, path) + _, existed := os.Stat(target) + if err := workspace.writeAsset(input.DraftID, path, contents); err != nil { + return Draft{}, err + } + next := mutateHistory(record, normalized) + if err := workspace.writeRecord(next); err != nil { + if errors.Is(existed, os.ErrNotExist) { + _ = os.Remove(target) + } + return Draft{}, err + } + return workspace.view(next) +} + +func (workspace *Workspace) ClearAsset(input ClearAssetInput) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(input.DraftID) + if err != nil { + return Draft{}, err + } + if err := assertRevision(record, input.ExpectedRevision); err != nil { + return Draft{}, err + } + nextTheme, err := detachAsset(record.Theme, input) + if err != nil { + return Draft{}, err + } + normalized, _, err := themerepo.NormalizeDraftDocument(nextTheme) + if err != nil { + return Draft{}, Error{Code: "STUDIO_ASSET_INVALID", Message: err.Error()} + } + next := mutateHistory(record, normalized) + if err := workspace.writeRecord(next); err != nil { + return Draft{}, err + } + return workspace.view(next) +} + +func (workspace *Workspace) Undo(draftID string, expectedRevision int) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return Draft{}, err + } + if err := assertRevision(record, expectedRevision); err != nil { + return Draft{}, err + } + if len(record.Past) == 0 { + return workspace.view(record) + } + previous := record.Past[len(record.Past)-1] + next := record + next.Theme = cloneJSON(previous) + next.Past = append([]json.RawMessage(nil), record.Past[:len(record.Past)-1]...) + next.Future = append([]json.RawMessage{cloneJSON(record.Theme)}, record.Future...) + if len(next.Future) > historyLimit { + next.Future = next.Future[:historyLimit] + } + next.Revision++ + next.UpdatedAt = now() + next.Dirty = true + if err := workspace.writeRecord(next); err != nil { + return Draft{}, err + } + return workspace.view(next) +} + +func (workspace *Workspace) Redo(draftID string, expectedRevision int) (Draft, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return Draft{}, err + } + if err := assertRevision(record, expectedRevision); err != nil { + return Draft{}, err + } + if len(record.Future) == 0 { + return workspace.view(record) + } + next := record + next.Theme = cloneJSON(record.Future[0]) + next.Past = append(next.Past, cloneJSON(record.Theme)) + if len(next.Past) > historyLimit { + next.Past = next.Past[len(next.Past)-historyLimit:] + } + next.Future = append([]json.RawMessage(nil), record.Future[1:]...) + next.Revision++ + next.UpdatedAt = now() + next.Dirty = true + if err := workspace.writeRecord(next); err != nil { + return Draft{}, err + } + return workspace.view(next) +} + +// Validate intentionally does not write. The UI calls it before an explicit +// save, and a failed validation must never create another draft revision. +func (workspace *Workspace) Validate(draftID string) (Draft, error) { + return workspace.Open(draftID) +} + +func (workspace *Workspace) Save(draftID string, expectedRevision int) (Draft, themerepo.Ref, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + if err := assertRevision(record, expectedRevision); err != nil { + return Draft{}, themerepo.Ref{}, err + } + if !record.Dirty && record.SavedRef != nil { + view, viewErr := workspace.view(record) + return view, *record.SavedRef, viewErr + } + _, ref, err := themerepo.NormalizeDocument(record.Theme) + if err != nil { + return Draft{}, themerepo.Ref{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: err.Error()} + } + if workspace.isReservedID(ref.ID) { + return Draft{}, themerepo.Ref{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme ID is reserved by the built-in catalog"} + } + version, err := workspace.nextVersion(ref.ID) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + versioned, _, err := setThemeFields(record.Theme, ref.ID, "", version) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + paths, err := themerepo.AssetPaths(versioned) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + files, err := workspace.readAssets(draftID, paths) + if err != nil { + return Draft{}, themerepo.Ref{}, Error{Code: "STUDIO_SAVE_FAILED", Message: err.Error()} + } + background, err := backgroundAsset(versioned) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + preview, err := imagepipeline.Process(files[background], imagepipeline.Options{ + Width: 640, Height: 400, Quality: 84, Fit: imagepipeline.FitCover, MaxInputBytes: 16 * 1024 * 1024, + }) + if err != nil { + return Draft{}, themerepo.Ref{}, Error{Code: "STUDIO_SAVE_FAILED", Message: err.Error()} + } + files["preview.webp"] = preview + saved, err := workspace.repository.InstallPersonal(themerepo.Bundle{Ref: themerepo.Ref{ID: ref.ID, Version: version}, Document: versioned, Files: files}) + if err != nil { + return Draft{}, themerepo.Ref{}, err + } + next := record + next.Theme = versioned + next.Revision++ + next.UpdatedAt = now() + next.SavedRef = &saved + next.Dirty = false + if err := workspace.writeRecord(next); err != nil { + return Draft{}, themerepo.Ref{}, err + } + view, err := workspace.view(next) + return view, saved, err +} + +func (workspace *Workspace) DeletePersonal(id string, version *string) error { + workspace.mu.Lock() + defer workspace.mu.Unlock() + return workspace.repository.DeletePersonal(id, version) +} + +func (workspace *Workspace) Export(ref themerepo.Ref) ([]byte, error) { + return workspace.repository.Export("personal", ref) +} + +func (workspace *Workspace) ReadAsset(draftID, path string) (themerepo.Asset, error) { + workspace.mu.Lock() + defer workspace.mu.Unlock() + record, err := workspace.readRecord(draftID) + if err != nil { + return themerepo.Asset{}, err + } + paths, err := themerepo.AssetPaths(record.Theme) + if err != nil { + return themerepo.Asset{}, Error{Code: "STUDIO_ASSET_INVALID", Message: err.Error()} + } + found := false + for _, candidate := range paths { + if candidate == path { + found = true + break + } + } + if !found { + return themerepo.Asset{}, Error{Code: "STUDIO_ASSET_INVALID", Message: "Draft asset is not declared"} + } + contents, err := os.ReadFile(workspace.assetPath(draftID, path)) + if err != nil { + return themerepo.Asset{}, err + } + mimeType := "image/webp" + if strings.HasSuffix(strings.ToLower(path), ".woff2") { + mimeType = "font/woff2" + } + return themerepo.Asset{Bytes: contents, MIMEType: mimeType}, nil +} + +func (workspace *Workspace) prepareSourceTheme(contents []byte, input CreateInput) (json.RawMessage, themerepo.Ref, error) { + normalized, ref, err := themerepo.NormalizeDocument(contents) + if err != nil { + return nil, themerepo.Ref{}, err + } + id := input.ThemeID + if id == "" { + if input.Source == "personal" { + id = ref.ID + } else { + id = strings.TrimSuffix(ref.ID+"-custom", "-") + } + } + if workspace.isReservedID(id) { + return nil, themerepo.Ref{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme ID is reserved by the built-in catalog"} + } + version := "0.0.0" + if input.Source == "personal" { + version = ref.Version + } + updated, updatedRef, err := setThemeFields(normalized, id, input.Name, version) + if err != nil { + return nil, themerepo.Ref{}, err + } + return updated, updatedRef, nil +} + +func setThemeFields(document []byte, id, name, version string) (json.RawMessage, themerepo.Ref, error) { + var object map[string]any + decoder := json.NewDecoder(bytes.NewReader(document)) + decoder.UseNumber() + if err := decoder.Decode(&object); err != nil { + return nil, themerepo.Ref{}, err + } + if id != "" { + object["id"] = id + } + if name != "" { + object["name"] = name + } + if version != "" { + object["version"] = version + } + encoded, err := json.Marshal(object) + if err != nil { + return nil, themerepo.Ref{}, err + } + normalized, ref, err := themerepo.NormalizeDraftDocument(encoded) + return json.RawMessage(normalized), ref, err +} + +func backgroundAsset(document []byte) (string, error) { + value, _, err := themeObject(document) + if err != nil { + return "", err + } + assets, _ := value["assets"].(map[string]any) + background, ok := assets["background"].(string) + if !ok || background == "" { + return "", Error{Code: "STUDIO_DRAFT_INVALID", Message: "Background image is required"} + } + return background, nil +} + +func normalizeAsset(input UploadInput) (string, []byte, error) { + if !validSlot(input.Slot) || strings.TrimSpace(input.FileName) == "" || len(input.FileName) > 160 { + return "", nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Asset upload metadata is invalid"} + } + if isFontSlot(input.Slot) { + if len(input.Bytes) == 0 || len(input.Bytes) > 5*1024*1024 || strings.ToLower(filepath.Ext(input.FileName)) != ".woff2" || !validWOFF2(input.Bytes) { + return "", nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Font must be a valid WOFF2 file up to 5 MB"} + } + key, err := requiredAssetKey(input.Slot, input.AssetKey) + if err != nil { + return "", nil, err + } + return "fonts/" + key + "-" + digest(input.Bytes) + ".woff2", append([]byte(nil), input.Bytes...), nil + } + if len(input.Bytes) == 0 || len(input.Bytes) > 50*1024*1024 || !validImageType(input.FileName, input.MIMEType) { + return "", nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Image must be a PNG, JPEG, or WebP file up to 50 MB"} + } + options := imagepipeline.Options{Quality: 80, Fit: imagepipeline.FitInside, NoUpscale: true, MaxInputBytes: 50 * 1024 * 1024} + switch input.Slot { + case "background": + options.Width, options.Height = 2400, 1350 + case "profile-avatar": + options.Width, options.Height, options.Fit, options.NoUpscale = 256, 256, imagepipeline.FitCover, false + case "suggestion-card1", "suggestion-card2", "suggestion-card3", "suggestion-card4", "project-icon1", "project-icon2", "project-icon3", "project-icon4": + options.Width, options.Height, options.Fit, options.NoUpscale = 192, 192, imagepipeline.FitCover, false + default: + options.Width, options.Height = 1400, 1400 + } + processed, err := imagepipeline.Process(input.Bytes, options) + if err != nil { + return "", nil, Error{Code: "STUDIO_ASSET_INVALID", Message: err.Error()} + } + name := "decoration" + switch input.Slot { + case "background": + name = "background" + case "portrait": + name = "portrait" + case "profile-avatar": + name = "profile-avatar" + case "suggestion-card1", "suggestion-card2", "suggestion-card3", "suggestion-card4", "project-icon1", "project-icon2", "project-icon3", "project-icon4": + name = input.Slot + case "decoration", "composition-layer": + key, keyErr := requiredAssetKey(input.Slot, input.AssetKey) + if keyErr != nil { + return "", nil, keyErr + } + name += "-" + key + } + return "assets/" + name + "-" + digest(processed) + ".webp", processed, nil +} + +func attachAsset(document []byte, input UploadInput, assetPath string) ([]byte, error) { + value, assets, err := themeObject(document) + if err != nil { + return nil, err + } + switch input.Slot { + case "background", "portrait", "profile-avatar": + field := map[string]string{"background": "background", "portrait": "portrait", "profile-avatar": "profileAvatar"}[input.Slot] + assets[field] = assetPath + case "suggestion-card1", "suggestion-card2", "suggestion-card3", "suggestion-card4": + icons := objectField(assets, "suggestionIcons") + icons[strings.TrimPrefix(input.Slot, "suggestion-")] = assetPath + case "project-icon1", "project-icon2", "project-icon3", "project-icon4": + index := int(input.Slot[len(input.Slot)-1] - '1') + icons, err := stringSliceField(assets, "projectIcons") + if err != nil { + return nil, err + } + for len(icons) <= index { + icons = append(icons, assetPath) + } + icons[index] = assetPath + assets["projectIcons"] = stringsToAny(icons) + case "decoration", "composition-layer": + key, err := requiredAssetKey(input.Slot, input.AssetKey) + if err != nil { + return nil, err + } + decorations := objectField(assets, "decorations") + decorations[key] = assetPath + if input.Slot == "decoration" { + entries, err := anySliceField(value, "decorations") + if err != nil { + return nil, err + } + entry := map[string]any{"type": "image", "enabled": true, "intensity": 0.6, "assetKey": key, "placement": "corners", "opacity": 0.75, "scale": 1} + replaced := false + for index, candidate := range entries { + if item, ok := candidate.(map[string]any); ok && item["assetKey"] == key { + entries[index], replaced = entry, true + break + } + } + if !replaced { + if len(entries) >= 16 { + return nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "A theme can contain at most 16 decorations"} + } + entries = append(entries, entry) + } + value["decorations"] = entries + } else if err := upsertCompositionLayer(value, key); err != nil { + return nil, err + } + case "ui-font", "code-font", "display-font": + key, err := requiredAssetKey(input.Slot, input.AssetKey) + if err != nil { + return nil, err + } + fonts := objectField(assets, "fonts") + fonts[key] = assetPath + typography := objectField(value, "typography") + field := map[string]string{"ui-font": "uiFontAssetKey", "code-font": "codeFontAssetKey", "display-font": "displayFontAssetKey"}[input.Slot] + typography[field] = key + family := map[string]string{"ui-font": "uiFamily", "code-font": "codeFamily", "display-font": "displayFamily"}[input.Slot] + typography[family] = "ocs-" + key + default: + return nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Asset slot is unavailable"} + } + return json.Marshal(value) +} + +func detachAsset(document []byte, input ClearAssetInput) ([]byte, error) { + if !validSlot(input.Slot) { + return nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Asset slot is unavailable"} + } + value, assets, err := themeObject(document) + if err != nil { + return nil, err + } + switch input.Slot { + case "background", "portrait", "profile-avatar": + delete(assets, map[string]string{"background": "background", "portrait": "portrait", "profile-avatar": "profileAvatar"}[input.Slot]) + case "suggestion-card1", "suggestion-card2", "suggestion-card3", "suggestion-card4": + delete(objectField(assets, "suggestionIcons"), strings.TrimPrefix(input.Slot, "suggestion-")) + case "project-icon1", "project-icon2", "project-icon3", "project-icon4": + icons, err := stringSliceField(assets, "projectIcons") + if err != nil { + return nil, err + } + index := int(input.Slot[len(input.Slot)-1] - '1') + if index < len(icons) { + icons = append(icons[:index], icons[index+1:]...) + } + if len(icons) == 0 { + delete(assets, "projectIcons") + } else { + assets["projectIcons"] = stringsToAny(icons) + } + case "decoration", "composition-layer": + key, err := requiredAssetKey(input.Slot, input.AssetKey) + if err != nil { + return nil, err + } + delete(objectField(assets, "decorations"), key) + entries, sliceErr := anySliceField(value, "decorations") + if sliceErr != nil { + return nil, sliceErr + } + value["decorations"] = filterAssetKey(entries, key) + if composition, ok := value["composition"].(map[string]any); ok { + if layers, ok := composition["layers"].([]any); ok { + composition["layers"] = filterCompositionKey(layers, key) + } + } + case "ui-font", "code-font", "display-font": + key, err := requiredAssetKey(input.Slot, input.AssetKey) + if err != nil { + return nil, err + } + delete(objectField(assets, "fonts"), key) + typography := objectField(value, "typography") + delete(typography, map[string]string{"ui-font": "uiFontAssetKey", "code-font": "codeFontAssetKey", "display-font": "displayFontAssetKey"}[input.Slot]) + } + return json.Marshal(value) +} + +func themeObject(document []byte) (map[string]any, map[string]any, error) { + decoder := json.NewDecoder(bytes.NewReader(document)) + decoder.UseNumber() + value := map[string]any{} + if err := decoder.Decode(&value); err != nil { + return nil, nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Theme draft is invalid"} + } + assets, ok := value["assets"].(map[string]any) + if !ok { + return nil, nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Theme assets are invalid"} + } + return value, assets, nil +} + +func objectField(parent map[string]any, key string) map[string]any { + if value, ok := parent[key].(map[string]any); ok { + return value + } + value := map[string]any{} + parent[key] = value + return value +} + +func anySliceField(parent map[string]any, key string) ([]any, error) { + if value, exists := parent[key]; exists { + entries, ok := value.([]any) + if !ok { + return nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Theme asset collection is invalid"} + } + return append([]any(nil), entries...), nil + } + return []any{}, nil +} + +func stringSliceField(parent map[string]any, key string) ([]string, error) { + entries, err := anySliceField(parent, key) + if err != nil { + return nil, err + } + result := make([]string, 0, len(entries)) + for _, entry := range entries { + value, ok := entry.(string) + if !ok { + return nil, Error{Code: "STUDIO_ASSET_INVALID", Message: "Theme icon collection is invalid"} + } + result = append(result, value) + } + return result, nil +} + +func upsertCompositionLayer(theme map[string]any, key string) error { + composition := objectField(theme, "composition") + layers, err := anySliceField(composition, "layers") + if err != nil { + return err + } + layer := map[string]any{ + "id": key, "asset": map[string]any{"kind": "decoration", "assetKey": key}, "surface": "home-hero", + "anchor": "top-left", "positionX": 0.1, "positionY": 0.1, "width": 0.2, "opacity": 1, "rotation": 0, "required": false, + } + for index, candidate := range layers { + if entry, ok := candidate.(map[string]any); ok && entry["id"] == key { + layers[index] = layer + composition["layers"] = layers + return nil + } + } + if len(layers) >= 24 { + return Error{Code: "STUDIO_ASSET_INVALID", Message: "A theme can contain at most 24 composition layers"} + } + composition["layers"] = append(layers, layer) + return nil +} + +func filterAssetKey(entries []any, key string) []any { + result := make([]any, 0, len(entries)) + for _, entry := range entries { + if value, ok := entry.(map[string]any); ok && value["assetKey"] == key { + continue + } + result = append(result, entry) + } + return result +} + +func filterCompositionKey(entries []any, key string) []any { + result := make([]any, 0, len(entries)) + for _, entry := range entries { + value, ok := entry.(map[string]any) + if !ok { + result = append(result, entry) + continue + } + asset, _ := value["asset"].(map[string]any) + if asset["assetKey"] == key || value["id"] == key { + continue + } + result = append(result, entry) + } + return result +} + +func validSlot(slot string) bool { + switch slot { + case "background", "portrait", "decoration", "ui-font", "code-font", "display-font", "composition-layer", "profile-avatar", "suggestion-card1", "suggestion-card2", "suggestion-card3", "suggestion-card4", "project-icon1", "project-icon2", "project-icon3", "project-icon4": + return true + default: + return false + } +} + +func isFontSlot(slot string) bool { + return slot == "ui-font" || slot == "code-font" || slot == "display-font" +} + +func requiredAssetKey(slot, value string) (string, error) { + if value == "" { + switch slot { + case "ui-font", "code-font", "display-font": + value = slot + default: + return "", Error{Code: "STUDIO_ASSET_INVALID", Message: "Asset key is required"} + } + } + if len(value) > 40 || !assetKeyPattern.MatchString(value) { + return "", Error{Code: "STUDIO_ASSET_INVALID", Message: "Asset key is invalid"} + } + return value, nil +} + +func validImageType(fileName, mimeType string) bool { + extension := strings.ToLower(filepath.Ext(fileName)) + switch extension { + case ".png": + return mimeType == "image/png" + case ".jpg", ".jpeg": + return mimeType == "image/jpeg" + case ".webp": + return mimeType == "image/webp" + default: + return false + } +} + +func validWOFF2(contents []byte) bool { + if len(contents) < 48 || string(contents[:4]) != "wOF2" || int(binary.BigEndian.Uint32(contents[8:12])) != len(contents) || binary.BigEndian.Uint16(contents[12:14]) == 0 || binary.BigEndian.Uint32(contents[16:20]) == 0 { + return false + } + return true +} + +func digest(contents []byte) string { + sum := sha256.Sum256(contents) + return hex.EncodeToString(sum[:])[:12] +} + +func stringsToAny(values []string) []any { + result := make([]any, len(values)) + for index, value := range values { + result[index] = value + } + return result +} + +func (workspace *Workspace) view(record record) (Draft, error) { + paths, err := themerepo.AssetPaths(record.Theme) + if err != nil { + return Draft{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: err.Error()} + } + urls := make(map[string]string, len(paths)) + for _, path := range paths { + urls[path] = "/api/draft-asset?draftId=" + record.DraftID + "&path=" + path + } + return Draft{ + DraftID: record.DraftID, Theme: cloneJSON(record.Theme), Revision: record.Revision, + UpdatedAt: record.UpdatedAt, SavedRef: cloneRef(record.SavedRef), Dirty: record.Dirty, + UndoAvailable: len(record.Past) > 0, RedoAvailable: len(record.Future) > 0, + Issues: validationIssues(record.Theme), AssetURLs: urls, + }, nil +} + +func validationIssues(theme json.RawMessage) []Issue { + // A draft may be edited while incomplete, but the inspector must still + // surface anything that would block its explicit immutable save. + if err := themerepo.ValidateDocument(theme); err != nil { + return []Issue{{Code: themerepo.ErrorCodeFrom(err), Path: "theme", Message: err.Error(), Severity: "error"}} + } + return []Issue{} +} + +func (workspace *Workspace) writeBundleAssets(draftID string, bundle themerepo.Bundle, document []byte) error { + paths, err := themerepo.AssetPaths(document) + if err != nil { + return err + } + for _, path := range paths { + contents, exists := bundle.Files[path] + if !exists { + return Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme source asset is missing"} + } + if err := workspace.writeAsset(draftID, path, contents); err != nil { + return err + } + } + return nil +} + +func (workspace *Workspace) readAssets(draftID string, paths []string) (map[string][]byte, error) { + files := make(map[string][]byte, len(paths)) + for _, path := range paths { + contents, err := os.ReadFile(workspace.assetPath(draftID, path)) + if err != nil { + return nil, err + } + files[path] = contents + } + return files, nil +} + +func (workspace *Workspace) writeAsset(draftID, path string, contents []byte) error { + if !safeDraftID(draftID) { + return Error{Code: "STUDIO_DRAFT_INVALID", Message: "Draft ID is invalid"} + } + target := workspace.assetPath(draftID, path) + if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil { + return err + } + return atomicWrite(target, contents) +} + +func (workspace *Workspace) assetPath(draftID, path string) string { + return filepath.Join(workspace.draftDirectory(draftID), filepath.FromSlash(path)) +} + +func (workspace *Workspace) draftDirectory(draftID string) string { + return filepath.Join(workspace.root, draftID) +} + +func (workspace *Workspace) recordPath(draftID string) string { + return filepath.Join(workspace.draftDirectory(draftID), "draft.json") +} + +func (workspace *Workspace) writeRecord(record record) error { + if err := validateRecord(record); err != nil { + return err + } + contents, err := json.MarshalIndent(record, "", " ") + if err != nil { + return err + } + contents = append(contents, '\n') + if err := os.MkdirAll(workspace.draftDirectory(record.DraftID), 0o700); err != nil { + return err + } + return atomicWrite(workspace.recordPath(record.DraftID), contents) +} + +func (workspace *Workspace) readRecord(draftID string) (record, error) { + if !safeDraftID(draftID) { + return record{}, Error{Code: "STUDIO_DRAFT_NOT_FOUND", Message: "Theme draft does not exist"} + } + contents, err := os.ReadFile(workspace.recordPath(draftID)) + if errors.Is(err, os.ErrNotExist) { + return record{}, Error{Code: "STUDIO_DRAFT_NOT_FOUND", Message: "Theme draft does not exist"} + } + if err != nil { + return record{}, err + } + var value record + if err := strictJSON(contents, &value); err != nil || validateRecord(value) != nil { + workspace.preserveInvalidEvidence(draftID, contents) + if err != nil { + return record{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme draft is invalid"} + } + return record{}, Error{Code: "STUDIO_DRAFT_INVALID", Message: "Theme draft is invalid"} + } + return value, nil +} + +func (workspace *Workspace) preserveInvalidEvidence(draftID string, contents []byte) { + directory := filepath.Join(workspace.root, "invalid-evidence") + _ = os.MkdirAll(directory, 0o700) + _ = atomicWrite(filepath.Join(directory, draftID+"-"+fmt.Sprint(time.Now().UTC().UnixNano())+".json"), contents) +} + +func (workspace *Workspace) listRecords() ([]record, error) { + entries, err := os.ReadDir(workspace.root) + if errors.Is(err, os.ErrNotExist) { + return []record{}, nil + } + if err != nil { + return nil, err + } + records := make([]record, 0, len(entries)) + for _, entry := range entries { + if !entry.IsDir() || !safeDraftID(entry.Name()) { + continue + } + value, err := workspace.readRecord(entry.Name()) + if err != nil { + if ErrorCode(err) == "STUDIO_DRAFT_INVALID" { + // Preserve invalid records as evidence, but do not let a single + // future-format/corrupt draft prevent access to every valid draft. + continue + } + return nil, err + } + records = append(records, value) + } + return records, nil +} + +func (workspace *Workspace) findByGroup(group string) (*record, error) { + records, err := workspace.listRecords() + if err != nil { + return nil, err + } + var latest *record + for index := range records { + _, ref, err := themerepo.NormalizeDraftDocument(records[index].Theme) + if err != nil || workspace.groupKey(ref.ID) != group { + continue + } + if latest == nil || records[index].UpdatedAt > latest.UpdatedAt || records[index].UpdatedAt == latest.UpdatedAt && records[index].DraftID > latest.DraftID { + value := records[index] + latest = &value + } + } + return latest, nil +} + +func (workspace *Workspace) removeDuplicateDrafts() error { + records, err := workspace.listRecords() + if err != nil { + return err + } + sort.Slice(records, func(left, right int) bool { + return records[left].UpdatedAt > records[right].UpdatedAt || records[left].UpdatedAt == records[right].UpdatedAt && records[left].DraftID > records[right].DraftID + }) + seen := map[string]bool{} + for _, record := range records { + _, ref, err := themerepo.NormalizeDraftDocument(record.Theme) + if err != nil { + return err + } + group := workspace.groupKey(ref.ID) + if !seen[group] { + seen[group] = true + continue + } + // Removing a duplicate directory is safe: it was identified from a + // fully valid record and a newer record for the same identity exists. + if err := os.RemoveAll(workspace.draftDirectory(record.DraftID)); err != nil { + return err + } + } + return nil +} + +func (workspace *Workspace) nextVersion(id string) (string, error) { + library, err := workspace.repository.List() + if err != nil { + return "", err + } + major, minor, patch := 1, 0, -1 + for _, item := range library.Themes { + if item.Source != "personal" || item.Ref.ID != id { + continue + } + var candidateMajor, candidateMinor, candidatePatch int + if _, err := fmt.Sscanf(item.Ref.Version, "%d.%d.%d", &candidateMajor, &candidateMinor, &candidatePatch); err != nil { + return "", Error{Code: "STUDIO_SAVE_FAILED", Message: "Stored theme version is invalid"} + } + if candidateMajor > major || candidateMajor == major && candidateMinor > minor || candidateMajor == major && candidateMinor == minor && candidatePatch > patch { + major, minor, patch = candidateMajor, candidateMinor, candidatePatch + } + } + if patch < 0 { + return "1.0.0", nil + } + return fmt.Sprintf("%d.%d.%d", major, minor, patch+1), nil +} + +func (workspace *Workspace) groupKey(id string) string { + for _, builtin := range workspace.builtinIDs { + stable := builtin + "-custom" + if id == stable || strings.HasPrefix(id, stable+"-") { + return "builtin:" + builtin + } + } + return "personal:" + id +} + +func (workspace *Workspace) isReservedID(id string) bool { + for _, builtin := range workspace.builtinIDs { + if id == builtin { + return true + } + } + return false +} + +func mutateHistory(current record, theme json.RawMessage) record { + next := current + next.Theme = cloneJSON(theme) + next.Revision++ + next.UpdatedAt = now() + next.Dirty = true + next.Past = append(append([]json.RawMessage(nil), current.Past...), cloneJSON(current.Theme)) + if len(next.Past) > historyLimit { + next.Past = next.Past[len(next.Past)-historyLimit:] + } + next.Future = []json.RawMessage{} + return next +} + +func assertRevision(record record, expected int) error { + if record.Revision != expected { + return Error{Code: "STUDIO_DRAFT_CONFLICT", Message: fmt.Sprintf("Draft revision changed from %d to %d", expected, record.Revision)} + } + return nil +} + +func validateRecord(record record) error { + if record.SchemaVersion != 1 || !safeDraftID(record.DraftID) || record.Revision < 0 || record.UpdatedAt == "" || len(record.Past) > historyLimit || len(record.Future) > historyLimit { + return errors.New("draft record envelope is invalid") + } + if _, _, err := themerepo.NormalizeDraftDocument(record.Theme); err != nil { + return err + } + for _, snapshot := range append(append([]json.RawMessage{}, record.Past...), record.Future...) { + if _, _, err := themerepo.NormalizeDraftDocument(snapshot); err != nil { + return err + } + } + return nil +} + +func strictJSON(contents []byte, target any) error { + decoder := json.NewDecoder(bytes.NewReader(contents)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(target); err != nil { + return err + } + if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { + return errors.New("JSON has trailing content") + } + return nil +} + +func atomicWrite(path string, contents []byte) error { + temporary, err := os.CreateTemp(filepath.Dir(path), ".openchatgptskin-*") + if err != nil { + return err + } + temporaryPath := temporary.Name() + committed := false + defer func() { + if !committed { + _ = os.Remove(temporaryPath) + } + }() + if _, err := temporary.Write(contents); err != nil { + _ = temporary.Close() + return err + } + if err := temporary.Sync(); err != nil { + _ = temporary.Close() + return err + } + if err := temporary.Close(); err != nil { + return err + } + if err := os.Rename(temporaryPath, path); err != nil { + return err + } + committed = true + return nil +} + +func newUUID() string { + value := make([]byte, 16) + if _, err := rand.Read(value); err != nil { + panic("crypto/rand failed: " + err.Error()) + } + value[6] = value[6]&0x0f | 0x40 + value[8] = value[8]&0x3f | 0x80 + encoded := hex.EncodeToString(value) + return encoded[:8] + "-" + encoded[8:12] + "-" + encoded[12:16] + "-" + encoded[16:20] + "-" + encoded[20:] +} + +func safeDraftID(value string) bool { return uuidPattern.MatchString(value) } +func cloneJSON(value json.RawMessage) json.RawMessage { return append(json.RawMessage(nil), value...) } +func cloneRef(value *themerepo.Ref) *themerepo.Ref { + if value == nil { + return nil + } + copy := *value + return © +} +func now() string { return time.Now().UTC().Format(time.RFC3339Nano) } diff --git a/host/go/internal/workspace/workspace_test.go b/host/go/internal/workspace/workspace_test.go new file mode 100644 index 0000000..c4a7304 --- /dev/null +++ b/host/go/internal/workspace/workspace_test.go @@ -0,0 +1,256 @@ +package workspace + +import ( + "bytes" + "encoding/binary" + "encoding/json" + stdimage "image" + "image/color" + "image/png" + "os" + "path/filepath" + "testing" + + "github.com/u2bo/OpenChatGPTSkin/host/go/internal/themerepo" +) + +func writeBuiltin(t *testing.T, root string) { + t.Helper() + directory := filepath.Join(root, "builtin", "mountain-mist") + if err := os.MkdirAll(filepath.Join(directory, "assets"), 0o700); err != nil { + t.Fatal(err) + } + catalog := `{"schemaVersion":1,"builtins":[{"id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","kind":"theme","path":"builtin/mountain-mist","ready":true,"localOnly":false,"licenseId":"LicenseRef-Test","preview":"builtin/mountain-mist/preview.webp"}],"recipes":[]}` + theme := `{"schemaVersion":4,"kind":"theme","appearance":"light","id":"mountain-mist","name":"Mountain Mist","version":"1.3.0","author":"OpenChatGPTSkin","assets":{"background":"assets/background.webp"},"colors":{"accent":"#113355","secondary":"#224466","text":"#ffffff","muted":"#cccccc","panel":"#112233","border":"#223344","success":"#228855","warning":"#cc8822","danger":"#bb3344","info":"#4477cc","textSecondary":"#eeeeee","link":"#aaccee","inputText":"#ffffff","placeholder":"#cccccc","codeText":"#ffffff"},"typography":{"uiFamily":"Sans","codeFamily":"Mono","scale":1,"uiSize":14,"codeSize":13,"uiWeight":400,"codeWeight":400,"lineHeight":1.5,"displayFamily":"Sans","displaySize":28,"displayWeight":400,"displayLineHeight":1.5,"displayLetterSpacing":0},"background":{"positionX":0.5,"positionY":0.5,"scale":1,"blur":0,"brightness":1,"overlay":0},"decorations":[],"layout":{},"rights":{"licenseId":"LicenseRef-Test","attribution":"OpenChatGPTSkin","localOnly":false}}` + if err := os.WriteFile(filepath.Join(root, "catalog.json"), []byte(catalog), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "theme.json"), []byte(theme), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "assets", "background.webp"), uploadPNG(t), 0o600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "preview.webp"), []byte("RIFF0000WEBP"), 0o600); err != nil { + t.Fatal(err) + } +} + +func newWorkspace(t *testing.T) (*Workspace, *themerepo.Repository, string) { + t.Helper() + builtinRoot := t.TempDir() + writeBuiltin(t, builtinRoot) + personalRoot := t.TempDir() + repository, err := themerepo.OpenWithPersonal(builtinRoot, personalRoot) + if err != nil { + t.Fatal(err) + } + draftRoot := filepath.Join(t.TempDir(), "theme-studio-drafts") + workspace, err := New(repository, draftRoot) + if err != nil { + t.Fatal(err) + } + return workspace, repository, draftRoot +} + +func renamedTheme(t *testing.T, theme json.RawMessage, name string) json.RawMessage { + t.Helper() + var value map[string]any + if err := json.Unmarshal(theme, &value); err != nil { + t.Fatal(err) + } + value["name"] = name + result, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + return result +} + +func TestWorkspaceCreatesOneDraftTracksRevisionAndPersistsHistory(t *testing.T) { + workspace, _, draftRoot := newWorkspace(t) + created, err := workspace.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}}) + if err != nil { + t.Fatal(err) + } + if created.Theme == nil || created.Revision != 0 || !created.Dirty { + t.Fatalf("created draft = %+v", created) + } + if _, err := workspace.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}}); ErrorCode(err) != "STUDIO_DRAFT_CONFLICT" { + t.Fatalf("duplicate draft error = %v", err) + } + loaded, err := workspace.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}, ConflictResolution: "load-existing"}) + if err != nil || loaded.DraftID != created.DraftID { + t.Fatalf("loaded draft=%+v error=%v", loaded, err) + } + updated, err := workspace.Update(created.DraftID, created.Revision, renamedTheme(t, created.Theme, "Renamed")) + if err != nil { + t.Fatal(err) + } + if updated.Revision != 1 || !updated.UndoAvailable || updated.RedoAvailable { + t.Fatalf("updated draft = %+v", updated) + } + if _, err := workspace.Update(created.DraftID, created.Revision, created.Theme); ErrorCode(err) != "STUDIO_DRAFT_CONFLICT" { + t.Fatalf("stale update error = %v", err) + } + undone, err := workspace.Undo(created.DraftID, updated.Revision) + if err != nil || undone.Revision != 2 || undone.UndoAvailable || !undone.RedoAvailable { + t.Fatalf("undone=%+v error=%v", undone, err) + } + redone, err := workspace.Redo(created.DraftID, undone.Revision) + if err != nil || redone.Revision != 3 || !redone.UndoAvailable || redone.RedoAvailable { + t.Fatalf("redone=%+v error=%v", redone, err) + } + // A new instance reads the Node-compatible draft.json shape unchanged. + reopened, err := New(workspace.repository, draftRoot) + if err != nil { + t.Fatal(err) + } + latest, err := reopened.Latest() + if err != nil || latest == nil || latest.DraftID != created.DraftID || latest.Revision != redone.Revision { + t.Fatalf("latest=%+v error=%v", latest, err) + } +} + +func TestWorkspaceSaveCreatesImmutablePersonalVersionAndExports(t *testing.T) { + workspace, repository, _ := newWorkspace(t) + draft, err := workspace.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}}) + if err != nil { + t.Fatal(err) + } + saved, ref, err := workspace.Save(draft.DraftID, draft.Revision) + if err != nil { + t.Fatal(err) + } + if ref != (themerepo.Ref{ID: "mountain-mist-custom", Version: "1.0.0"}) || saved.SavedRef == nil || saved.Dirty { + t.Fatalf("saved draft=%+v ref=%+v", saved, ref) + } + archive, err := workspace.Export(ref) + if err != nil || len(archive) == 0 { + t.Fatalf("export bytes=%d error=%v", len(archive), err) + } + bundle, err := repository.Read("personal", ref) + if err != nil { + t.Fatal(err) + } + if preview := bundle.Files["preview.webp"]; len(preview) < 12 || string(preview[:4]) != "RIFF" || string(preview[8:12]) != "WEBP" { + t.Fatal("saved personal theme has no generated WebP preview") + } + idempotent, sameRef, err := workspace.Save(saved.DraftID, saved.Revision) + if err != nil || sameRef != ref || idempotent.Revision != saved.Revision { + t.Fatalf("idempotent save draft=%+v ref=%+v error=%v", idempotent, sameRef, err) + } +} + +func TestWorkspaceRejectsMalformedDraftAndPreservesEvidence(t *testing.T) { + workspace, _, root := newWorkspace(t) + draftID := "00000000-0000-4000-8000-000000000001" + directory := filepath.Join(root, draftID) + if err := os.MkdirAll(directory, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(directory, "draft.json"), []byte(`{"schemaVersion":99}`), 0o600); err != nil { + t.Fatal(err) + } + if _, err := workspace.Open(draftID); ErrorCode(err) != "STUDIO_DRAFT_INVALID" { + t.Fatalf("malformed draft error = %v", err) + } + evidence, err := os.ReadDir(filepath.Join(root, "invalid-evidence")) + if err != nil || len(evidence) != 1 { + t.Fatalf("invalid evidence = %v error=%v", evidence, err) + } +} + +func uploadPNG(t *testing.T) []byte { + t.Helper() + image := stdimage.NewNRGBA(stdimage.Rect(0, 0, 8, 4)) + image.SetNRGBA(1, 1, color.NRGBA{R: 255, G: 80, B: 160, A: 150}) + var output bytes.Buffer + if err := png.Encode(&output, image); err != nil { + t.Fatal(err) + } + return output.Bytes() +} + +func TestWorkspaceProcessesSlotAssetsOnceAndClearKeepsDraftHistory(t *testing.T) { + workspace, _, root := newWorkspace(t) + draft, err := workspace.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}}) + if err != nil { + t.Fatal(err) + } + updated, err := workspace.Upload(UploadInput{ + DraftID: draft.DraftID, ExpectedRevision: draft.Revision, Slot: "profile-avatar", + FileName: "avatar.png", MIMEType: "image/png", Bytes: uploadPNG(t), + }) + if err != nil || updated.Revision != draft.Revision+1 || len(updated.AssetURLs) != 2 { + t.Fatalf("upload draft=%+v error=%v", updated, err) + } + var theme struct { + Assets struct { + ProfileAvatar string `json:"profileAvatar"` + } `json:"assets"` + } + if err := json.Unmarshal(updated.Theme, &theme); err != nil { + t.Fatal(err) + } + contents, err := os.ReadFile(filepath.Join(root, draft.DraftID, filepath.FromSlash(theme.Assets.ProfileAvatar))) + if err != nil || len(contents) < 12 || string(contents[:4]) != "RIFF" || string(contents[8:12]) != "WEBP" { + t.Fatalf("processed avatar bytes=%d error=%v", len(contents), err) + } + if _, err := workspace.Upload(UploadInput{ + DraftID: draft.DraftID, ExpectedRevision: updated.Revision, Slot: "profile-avatar", + FileName: "bad.txt", MIMEType: "text/plain", Bytes: []byte("bad"), + }); ErrorCode(err) != "STUDIO_ASSET_INVALID" { + t.Fatalf("invalid upload error = %v", err) + } + afterInvalid, err := workspace.Open(draft.DraftID) + if err != nil || afterInvalid.Revision != updated.Revision { + t.Fatalf("invalid upload changed draft=%+v error=%v", afterInvalid, err) + } + cleared, err := workspace.ClearAsset(ClearAssetInput{DraftID: draft.DraftID, ExpectedRevision: updated.Revision, Slot: "background"}) + if err != nil || cleared.Revision != updated.Revision+1 || len(cleared.Issues) != 1 { + t.Fatalf("cleared draft=%+v error=%v", cleared, err) + } + if _, _, err := workspace.Save(cleared.DraftID, cleared.Revision); ErrorCode(err) != "STUDIO_DRAFT_INVALID" { + t.Fatalf("saved incomplete draft error = %v", err) + } +} + +func TestWorkspaceAcceptsBoundedWOFF2Slots(t *testing.T) { + font := make([]byte, 48) + copy(font, "wOF2") + binary.BigEndian.PutUint32(font[8:12], uint32(len(font))) + binary.BigEndian.PutUint16(font[12:14], 1) + binary.BigEndian.PutUint32(font[16:20], 1) + if !validWOFF2(font) { + t.Fatal("well-formed bounded WOFF2 header was rejected") + } + if validWOFF2(font[:20]) { + t.Fatal("truncated WOFF2 header was accepted") + } +} + +func TestWorkspaceImportsArchiveIntoOnePersonalDraft(t *testing.T) { + first, _, _ := newWorkspace(t) + draft, err := first.Create(CreateInput{Source: "builtin", Ref: themerepo.Ref{ID: "mountain-mist", Version: "1.3.0"}}) + if err != nil { + t.Fatal(err) + } + _, ref, err := first.Save(draft.DraftID, draft.Revision) + if err != nil { + t.Fatal(err) + } + archive, err := first.Export(ref) + if err != nil { + t.Fatal(err) + } + second, _, _ := newWorkspace(t) + imported, err := second.Import(archive) + if err != nil || imported.SavedRef == nil || *imported.SavedRef != ref || imported.Dirty { + t.Fatalf("imported draft=%+v error=%v", imported, err) + } + loaded, err := second.Create(CreateInput{Source: "personal", Ref: ref, ThemeID: ref.ID, ConflictResolution: "load-existing"}) + if err != nil || loaded.DraftID != imported.DraftID { + t.Fatalf("loaded draft=%+v error=%v", loaded, err) + } +} diff --git a/host/go/testdata/images/cases.json b/host/go/testdata/images/cases.json new file mode 100644 index 0000000..25af887 --- /dev/null +++ b/host/go/testdata/images/cases.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "cases": [ + { "name": "jpeg-exif-orientation-6", "expectedWidth": 1, "expectedHeight": 2 }, + { "name": "png-alpha-crop-resize", "expectedWidth": 2, "expectedHeight": 2 }, + { "name": "webp-roundtrip", "expectedFormat": "webp" }, + { "name": "corrupt-input", "expectedErrorCode": "IMAGE_DECODE_INVALID" }, + { "name": "input-limit", "expectedErrorCode": "IMAGE_INPUT_TOO_LARGE" }, + { "name": "atomic-failure", "expectedOutput": "unchanged" } + ] +} diff --git a/host/go/testdata/v0.2.0/data-root/corpus.json b/host/go/testdata/v0.2.0/data-root/corpus.json new file mode 100644 index 0000000..e36bf2d --- /dev/null +++ b/host/go/testdata/v0.2.0/data-root/corpus.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "draft": { + "draftId": "00000000-0000-4000-8000-000000000301", + "revision": 6, + "dirty": true, + "past": 3, + "future": 0, + "savedRef": { + "id": "baseline-draft", + "version": "1.0.0" + } + }, + "personalTheme": { + "id": "baseline-personal", + "versions": ["1.0.0", "1.1.0"] + }, + "runtime": { + "sessionId": "00000000-0000-4000-8000-000000000302", + "status": "restored-awaiting-exit", + "theme": { + "id": "mountain-mist", + "version": "1.3.0" + } + }, + "trustedCacheSchemaVersion": 1, + "controllerLockSchemaVersion": 1, + "corruptRecordCount": 4, + "corruptStoreCount": 4 +} diff --git a/host/go/testdata/v0.2.0/golden/node.json b/host/go/testdata/v0.2.0/golden/node.json new file mode 100644 index 0000000..6839202 --- /dev/null +++ b/host/go/testdata/v0.2.0/golden/node.json @@ -0,0 +1,93 @@ +{ + "implementation": "node", + "suites": [ + { + "implementation": "node", + "suite": "studio", + "result": { + "protocolVersion": 2, + "sessionStatus": 204, + "bootstrapStatus": 200, + "unauthenticatedStatus": 401, + "securityHeaderCount": 5, + "eventKind": "runtime-status" + } + }, + { + "implementation": "node", + "suite": "runtime", + "result": { + "protocolVersion": 1, + "frameLimitBytes": 65536, + "transportSecurityVerified": true, + "status": "stopped", + "launchStatus": "active", + "replayed": true, + "conflictingRequestCode": "RUNTIME_CONTROL_UNAVAILABLE" + } + }, + { + "implementation": "node", + "suite": "theme", + "result": { + "builtins": [ + "future-idol-cyan", + "glacier-aurora", + "mountain-mist", + "rose-carpet-star", + "yua-mikami-starlight" + ], + "migratedVersions": [ + 1, + 2, + 3, + 4 + ], + "archiveRoundTrips": 5, + "archiveNegativeCode": "ARCHIVE_ENTRY_UNSAFE", + "futureVersionCode": "THEME_SCHEMA_VERSION_UNSUPPORTED", + "v4Capabilities": { + "welcomeLocales": [ + "en", + "zh-CN" + ], + "displayFont": true, + "profileAvatar": true, + "suggestionIcons": 4, + "compositionLayers": 4 + } + } + }, + { + "implementation": "node", + "suite": "data", + "result": { + "draft": { + "schemaVersion": 1, + "dirty": true, + "revision": 6, + "past": 3, + "future": 0, + "savedRef": { + "id": "baseline-draft", + "version": "1.0.0" + }, + "savedDraftWasClean": true, + "undoExposedRedo": true, + "redoConsumedFuture": true + }, + "personalVersions": 2, + "runtimeTerminalStatus": "restored-awaiting-exit", + "trustedCacheSchemaVersion": 1, + "controllerLockSchemaVersion": 1, + "corruptRecordsRejected": 4, + "corruptStoresRejected": 4, + "sensitiveValuesFound": false + } + } + ], + "review": { + "status": "reviewed", + "knownNodeBugs": [] + } +} diff --git a/package-lock.json b/package-lock.json index 9c80bdf..09516d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,29 @@ { "name": "open-chatgpt-skin", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-chatgpt-skin", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "workspaces": [ "apps/*", - "packages/*", - "runtime/*" + "packages/*" ], "devDependencies": { "@types/node": "^22.20.1", "@types/ws": "^8.18.1", + "esbuild": "^0.28.1", + "fflate": "^0.8.3", "linkedom": "^0.18.12", "sharp": "^0.34.5", "tsx": "^4.23.1", "typescript": "^5.9.3", "vitest": "^3.2.7", - "ws": "^8.18.3" + "ws": "^8.18.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.2" }, "engines": { "node": ">=22.0.0" @@ -28,10 +31,10 @@ }, "apps/theme-studio": { "name": "@open-chatgpt-skin/theme-studio", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "dependencies": { - "@open-chatgpt-skin/theme-schema": "0.2.0", - "@open-chatgpt-skin/theme-studio-core": "0.2.0", + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1", + "@open-chatgpt-skin/theme-studio-core": "0.3.0-alpha.1", "@phosphor-icons/react": "^2.1.10", "react": "19.0.0", "react-dom": "19.0.0" @@ -514,6 +517,7 @@ "version": "1.11.2", "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.11.2.tgz", "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -966,6 +970,7 @@ "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -978,6 +983,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1000,6 +1006,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1022,6 +1029,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1038,6 +1046,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1054,6 +1063,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1070,6 +1080,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1086,6 +1097,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1102,6 +1114,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1118,6 +1131,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1134,6 +1148,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1150,6 +1165,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1166,6 +1182,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1182,6 +1199,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1204,6 +1222,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1226,6 +1245,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1248,6 +1268,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1270,6 +1291,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1292,6 +1314,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1314,6 +1337,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1336,6 +1360,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1358,6 +1383,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -1377,6 +1403,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1396,6 +1423,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1415,6 +1443,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1481,6 +1510,10 @@ "resolved": "packages/cdp-adapter", "link": true }, + "node_modules/@open-chatgpt-skin/runtime-contract": { + "resolved": "packages/runtime-contract", + "link": true + }, "node_modules/@open-chatgpt-skin/theme-core": { "resolved": "packages/theme-core", "link": true @@ -1497,14 +1530,6 @@ "resolved": "packages/theme-studio-core", "link": true }, - "node_modules/@open-chatgpt-skin/theme-studio-service": { - "resolved": "runtime/theme-studio-service", - "link": true - }, - "node_modules/@open-chatgpt-skin/windows-runtime": { - "resolved": "runtime/windows", - "link": true - }, "node_modules/@phosphor-icons/react": { "version": "2.1.10", "resolved": "https://registry.npmmirror.com/@phosphor-icons/react/-/react-2.1.10.tgz", @@ -2619,6 +2644,7 @@ "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -3793,6 +3819,7 @@ "version": "7.8.5", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3805,6 +3832,7 @@ "version": "0.34.5", "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4046,6 +4074,7 @@ "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD", "optional": true }, @@ -4911,19 +4940,37 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, "packages/cdp-adapter": { "name": "@open-chatgpt-skin/cdp-adapter", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "dependencies": { - "@open-chatgpt-skin/theme-core": "0.2.0", - "@open-chatgpt-skin/theme-schema": "0.2.0" + "@open-chatgpt-skin/theme-core": "0.3.0-alpha.1", + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1" + } + }, + "packages/runtime-contract": { + "name": "@open-chatgpt-skin/runtime-contract", + "version": "0.3.0-alpha.1", + "dependencies": { + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1", + "zod": "^3.25.76" } }, "packages/theme-core": { "name": "@open-chatgpt-skin/theme-core", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "dependencies": { - "@open-chatgpt-skin/theme-schema": "0.2.0", + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1", "fflate": "^0.8.3", "zod": "^3.25.76" }, @@ -4933,51 +4980,18 @@ }, "packages/theme-schema": { "name": "@open-chatgpt-skin/theme-schema", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "dependencies": { "zod": "^3.25.76" } }, "packages/theme-studio-core": { "name": "@open-chatgpt-skin/theme-studio-core", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "dependencies": { - "@open-chatgpt-skin/theme-schema": "0.2.0", + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1", "zod": "^3.25.76" } - }, - "runtime/theme-studio-service": { - "name": "@open-chatgpt-skin/theme-studio-service", - "version": "0.2.0", - "dependencies": { - "@open-chatgpt-skin/cdp-adapter": "0.2.0", - "@open-chatgpt-skin/theme-core": "0.2.0", - "@open-chatgpt-skin/theme-schema": "0.2.0", - "@open-chatgpt-skin/theme-studio-core": "0.2.0", - "@open-chatgpt-skin/windows-runtime": "0.2.0", - "sharp": "^0.34.5", - "zod": "^3.25.76" - }, - "bin": { - "open-chatgpt-skin-studio": "dist/cli.js" - }, - "devDependencies": { - "tsx": "^4.23.1", - "vite": "6.1.0" - } - }, - "runtime/windows": { - "name": "@open-chatgpt-skin/windows-runtime", - "version": "0.2.0", - "dependencies": { - "@open-chatgpt-skin/cdp-adapter": "0.2.0", - "@open-chatgpt-skin/theme-core": "0.2.0", - "@open-chatgpt-skin/theme-schema": "0.2.0", - "zod": "^3.25.76" - }, - "bin": { - "open-chatgpt-skin-runtime": "dist/cli.js" - } } } } diff --git a/package.json b/package.json index 6e4725e..86357c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-chatgpt-skin", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "private": true, "type": "module", "repository": { @@ -13,32 +13,32 @@ }, "workspaces": [ "apps/*", - "packages/*", - "runtime/*" + "packages/*" ], "scripts": { "build": "tsc -b", "themes:build": "tsx --tsconfig tsconfig.scripts.json scripts/build-theme-catalog.ts", + "go:baseline:verify": "tsx --tsconfig tsconfig.scripts.json scripts/contracts/verify-baseline.ts", + "contracts:build": "tsx --tsconfig tsconfig.scripts.json scripts/contracts/build.ts", + "contracts:verify": "tsx --tsconfig tsconfig.scripts.json scripts/contracts/verify.ts", + "contracts:baseline": "tsx --tsconfig tsconfig.scripts.json scripts/contracts/baseline-runner.ts", + "go:test": "go -C host/go test -buildvcs=false -tags nodynamic ./... -count=1 -timeout 60s", + "go:verify": "npm run go:test && go -C host/go vet -buildvcs=false -tags nodynamic ./...", + "go:cdp-adapter:build": "tsx --tsconfig tsconfig.scripts.json scripts/build-go-cdp-adapter.ts", + "release:build": "tsx --tsconfig tsconfig.scripts.json scripts/release/build-go-release.ts", + "release:acceptance": "tsx --tsconfig tsconfig.scripts.json scripts/release/accept-go-release.ts", + "release:merge": "tsx --tsconfig tsconfig.scripts.json scripts/release/merge-go-release.ts", "typecheck": "tsc -b --pretty false", "test": "vitest run", "test:watch": "vitest", - "runtime": "npm run build && node runtime/windows/dist/cli.js", - "runtime:probe": "npm run build && node runtime/windows/dist/probe-cli.js", - "runtime:acceptance": "npm run build && node runtime/windows/dist/acceptance-cli.js", + "runtime": "go -C host/go run -buildvcs=false -tags nodynamic ./cmd/openchatgptskin runtime", "studio:build": "npm run build -w @open-chatgpt-skin/theme-studio", - "release:stage": "tsx scripts/release/stage-release.ts", - "release:acceptance": "tsx scripts/release/accept-release.ts", - "release:package": "tsx scripts/release/package-release.ts", - "release:acceptance:archive": "tsx scripts/release/accept-portable.ts", "release:checksums": "tsx scripts/release/write-checksums.ts", "release:version": "tsx scripts/release/verify-version.ts", - "release:node": "tsx scripts/release/fetch-node-runtime.ts", - "release:windows": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/release/build-windows-local.ps1", - "release:macos": "tsx scripts/release/build-macos-distribution.ts", - "release:acceptance:macos": "tsx scripts/release/accept-macos-distribution.ts", - "studio:dev": "npm run build && npm run dev -w @open-chatgpt-skin/theme-studio-service", - "verify": "npm run build && npm run test && npm run typecheck", - "verify:runtime": "npm run build && npm run test && npm run typecheck", + "release:windows": "npm run release:build -- --native-only --output artifacts/windows-x64 && npm run release:checksums -- --output artifacts/windows-x64", + "release:macos": "npm run release:build -- --native-only --output artifacts/macos-native && npm run release:checksums -- --output artifacts/macos-native", + "studio:dev": "tsx scripts/dev/start-go-studio.ts", + "verify": "npm run contracts:verify && npm run build && npm run test && npm run typecheck && npm run go:verify", "verify:foundation": "npm run themes:build && npm run build && npm run test && npm run typecheck && node packages/theme-core/dist/cli.js catalog --root themes" }, "engines": { @@ -47,11 +47,15 @@ "devDependencies": { "@types/node": "^22.20.1", "@types/ws": "^8.18.1", + "esbuild": "^0.28.1", + "fflate": "^0.8.3", "linkedom": "^0.18.12", "sharp": "^0.34.5", "tsx": "^4.23.1", "typescript": "^5.9.3", "vitest": "^3.2.7", - "ws": "^8.18.3" + "ws": "^8.18.3", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.2" } } diff --git a/packages/cdp-adapter/package.json b/packages/cdp-adapter/package.json index 2258ee2..73edae2 100644 --- a/packages/cdp-adapter/package.json +++ b/packages/cdp-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@open-chatgpt-skin/cdp-adapter", - "version": "0.2.0", + "version": "0.3.0-alpha.1", "type": "module", "exports": "./dist/index.js", "types": "./dist/index.d.ts", @@ -11,7 +11,7 @@ "build": "tsc -b" }, "dependencies": { - "@open-chatgpt-skin/theme-core": "0.2.0", - "@open-chatgpt-skin/theme-schema": "0.2.0" + "@open-chatgpt-skin/theme-core": "0.3.0-alpha.1", + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1" } } diff --git a/packages/cdp-adapter/src/scripts.ts b/packages/cdp-adapter/src/scripts.ts index ea8e164..4c6d91d 100644 --- a/packages/cdp-adapter/src/scripts.ts +++ b/packages/cdp-adapter/src/scripts.ts @@ -209,8 +209,8 @@ export const PROBE_EXPRESSION = `(() => { }; })()`; -export function preflightExpression(theme: CompiledTheme): string { - const payload = JSON.stringify(theme); +export function preflightExpression(theme: CompiledTheme, themeReference?: string): string { + const payload = themeReference ?? JSON.stringify(theme); const nativeSelectors = JSON.stringify(NATIVE_CODEX_SURFACE_SELECTORS); return `(() => { const theme = ${payload}; @@ -319,8 +319,8 @@ export function preflightExpression(theme: CompiledTheme): string { })()`; } -export function applyExpression(theme: CompiledTheme): string { - const payload = JSON.stringify(theme); +export function applyExpression(theme: CompiledTheme, themeReference?: string): string { + const payload = themeReference ?? JSON.stringify(theme); const nativeSelectors = JSON.stringify(NATIVE_CODEX_SURFACE_SELECTORS); return `(async () => { const theme = ${payload}; diff --git a/packages/runtime-contract/package.json b/packages/runtime-contract/package.json new file mode 100644 index 0000000..682cf46 --- /dev/null +++ b/packages/runtime-contract/package.json @@ -0,0 +1,17 @@ +{ + "name": "@open-chatgpt-skin/runtime-contract", + "version": "0.3.0-alpha.1", + "type": "module", + "exports": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -b" + }, + "dependencies": { + "@open-chatgpt-skin/theme-schema": "0.3.0-alpha.1", + "zod": "^3.25.76" + } +} diff --git a/packages/runtime-contract/src/index.ts b/packages/runtime-contract/src/index.ts new file mode 100644 index 0000000..e188b44 --- /dev/null +++ b/packages/runtime-contract/src/index.ts @@ -0,0 +1,214 @@ +import { z } from "zod"; +import { ThemeIdSchema, ThemeVersionSchema } from "@open-chatgpt-skin/theme-schema"; + +export const CONTROL_PROTOCOL_VERSION = 1 as const; +export const CONTROL_MAX_FRAME_BYTES = 64 * 1024; +export const CONTROL_COMMANDS = ["launch", "status", "switch", "pause", "resume", "restore"] as const; + +export const RUNTIME_ERROR_CODES = [ + "CODEX_NOT_INSTALLED", "CODEX_DISCOVERY_REQUIRES_BOOTSTRAP", "CODEX_IDENTITY_INVALID", + "CODEX_ALREADY_RUNNING_UNMANAGED", "CODEX_LAUNCH_FAILED", "CODEX_WINDOW_ACTIVATION_FAILED", + "PROCESS_INSPECTION_DENIED", "CDP_NOT_READY", "CDP_ENDPOINT_UNSAFE", "CDP_PROCESS_MISMATCH", + "CDP_TARGET_NOT_FOUND", "CDP_TARGET_AMBIGUOUS", "ADAPTER_INCOMPATIBLE", "THEME_APPLY_FAILED", + "THEME_VERIFY_FAILED", "THEME_CLEANUP_FAILED", "THEME_SWITCH_FAILED", "THEME_ROLLBACK_FAILED", + "THEME_RUNTIME_TOO_LARGE", "THEME_SCHEMA_VERSION_UNSUPPORTED", "THEME_WELCOME_INVALID", + "THEME_DISPLAY_FONT_MISSING", "THEME_COMPOSITION_INVALID", "THEME_HOME_WELCOME_UNSUPPORTED", + "THEME_REQUIRED_LAYER_UNRESOLVED", "THEME_NOT_FOUND", "THEME_NOT_READY", "RUNTIME_SESSION_STALE", + "RUNTIME_INVALID_STATE", "RUNTIME_BUSY", "RUNTIME_ENVIRONMENT_INVALID", "RUNTIME_CONTROL_UNAVAILABLE", + "RESTORE_AWAITING_EXIT", "PROBE_EXIT_PENDING", "PROBE_FINALIZE_REQUIRED", + "PROBE_PENDING_SESSION_INVALID", "INTERNAL", +] as const; + +export const RuntimeStatusSchema = z.enum([ + "launching", "active", "paused", "paused-incompatible", "recovery-required", "restoring", + "restored-awaiting-exit", "restored-cleanup-required", +]); +export type RuntimeStatus = z.infer