diff --git a/community-server/community/package.py b/community-server/community/package.py index c2dfe05..3930218 100644 --- a/community-server/community/package.py +++ b/community-server/community/package.py @@ -89,7 +89,9 @@ def inspect(release: Release, blob: bytes): import yaml value = yaml.safe_load(files[matches[0]]) identity = {"theme": "theme_id", "skill": "skill_id", "plugin": "plugin_id"}[release.type] - if not isinstance(value, dict) or value.get(identity) != release.package_id or value.get("version") != release.version: + if (not isinstance(value, dict) or value.get(identity, value.get("id") if release.type in {"plugin", "skill"} else None) != release.package_id + or (identity in value and "id" in value and value[identity] != value["id"]) + or value.get("version") != release.version): raise ValueError("发行身份与类型清单不一致") if set(value.get("permissions", [])) != set(release.permissions): raise ValueError("发行权限与类型清单不一致") diff --git a/community-server/tests/test_catalog.py b/community-server/tests/test_catalog.py index 1c4ca0c..84f4a42 100644 --- a/community-server/tests/test_catalog.py +++ b/community-server/tests/test_catalog.py @@ -87,3 +87,24 @@ def test_cross_namespace_and_missing_auth(env): assert client.post("/catalog/v1/publish/submissions", json=payload).status_code == 401 payload["release"]["namespace"] = "other" assert client.post("/catalog/v1/publish/submissions", headers=author, json=payload).status_code == 403 + + +@pytest.mark.parametrize("kind,identifier", [("plugin", "markdown-workbench"), ("skill", "note-reviewer")]) +def test_repository_manifest_identity_aliases_and_conflicts(kind, identifier): + from pathlib import Path + import yaml + from community.package import inspect + source = Path(__file__).resolve().parents[2] / "backend" / "extensions" / "community" / (kind + "s") / identifier / (kind + ".yaml") + body = source.read_text(encoding="utf-8") + manifest = yaml.safe_load(body) + private = Ed25519PrivateKey.generate() + payload = package(private, kind, files={kind + ".yaml": body}) + release = Release(**payload["release"]) + release.package_id = identifier + release.permissions = manifest["permissions"] + inspect(release, base64.b64decode(payload["archive_base64"])) + bad = package(private, kind, files={kind + ".yaml": body + "\n" + kind + "_id: other-id\n"}) + release.sha256 = bad["release"]["sha256"] + release.size = bad["release"]["size"] + with pytest.raises(ValueError): + inspect(release, base64.b64decode(bad["archive_base64"])) diff --git a/docs/contracts/Community-v1契约.md b/docs/contracts/Community-v1契约.md index 90f7bcc..096fa87 100644 --- a/docs/contracts/Community-v1契约.md +++ b/docs/contracts/Community-v1契约.md @@ -43,4 +43,9 @@ Host 的 `extension_package` 已实现 Release v1 字段校验、递归排序 ca ZIP 在解压前独立校验中央目录计数、重复原始名称、本地头一致性和数据区不重叠,避免解压库的名称映射隐藏重复项。逐条检查 NFC 路径、Windows 保留名、完整大小写折叠冲突、文件/目录前缀冲突、链接/特殊文件、加密与压缩方法;按类别检查压缩/展开/条目限额,并用有界缓冲验证全部条目及 CRC。当前只返回文件摘要清单,不写出包文件;不支持多卷或需 ZIP64 中央目录的包。 -类型清单身份/权限/schema 验证、在线来源复核、持久安装库、迁移与事务运行生命周期仍待接入。因此当前不能据此开启 extensions capability 或标记完整 D-01 通过。 +类型清单的身份、版本和权限校验已接入 `verify_package`;完整 Runtime 配置 schema、在线来源复核、持久安装库、迁移与事务运行生命周期仍待接入。因此当前不能据此开启 extensions capability 或标记完整 D-01 通过。 + + +清单解析最多 1 MiB、32 层、10000 个节点、一个文档,限制别名及记录的锚点大小;重复键、包含指令、循环别名、非 UTF-8 和未知 schema_version 拒绝。关闭文件包含与环境插值特性。JSON 类别先验证 JSON 语法再进行重复键检查,不接受伪装为 JSON 的 YAML。 + +Theme 必须声明 theme_id;Skill/Plugin 支持 Runtime 现有的 id 或专用 skill_id/plugin_id,两者同时出现须相等。YAML 类型版本及全部类型权限集合须与发行一致,重复权限拒绝。人设、模板、MCP 和模型方案检查各自基本结构及递归秘密字段;声明式包通过检查不代表已具备运行权限。项目自带 markdown-workbench/note-reviewer 清单已在 Rust 和 Community 校验中验证。 diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index 3ed5ee1..1701f03 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -148,3 +148,13 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 新增锁定依赖 ed25519-dalek 2.2.0 与 unicode-casefold 0.2.0;复用已存在的 zip 8.6.0、Unicode normalization。声明 Rust 下限调整到 ZIP 库要求的 1.88,实际验证仍使用本机 GNU 1.98.1,尚无最低工具链或 MSVC 发布证据。 - Rust desktop 全目标 58 项通过(另两个既有显式/辅助入口 ignored),随后补充较新 Unicode 大小写映射,4 项包验证测试复跑通过,最终 Clippy `-D warnings` 通过。日志 `.build/extension-verification-rust-tests.log`。期间一次链接被仍在运行的测试 EXE 占用;确认原进程完成后,针对性测试重跑通过。 - 此批只验证包,不提取、不安装、不执行。清单身份/权限和 schema、在线撤回复核、单一安装库、迁移及事务尚未完成,extensions capability 维持 false,完整 D-01–04 仍未通过。 + + +## 增量:有界类型清单与发行一致性校验 + +- `verify_package` 串联签名、ZIP 全字节检查和清单解析,返回经过检查的摘要清单及声明内容。清单最多 1 MiB / 32 层 / 10000 节点 / 单文档;重复键、循环别名、文件包含、未知 schema_version 和非法 JSON 拒绝。YAML 解析关闭环境插值与文件包含特性,错误不回显正文。 +- Theme 保持专用 theme_id;Skill/Plugin 兼容 Runtime 实际包使用的 id 与专用类型 ID,双声明冲突拒绝。发现并修复 Community 服务此前会拒绝项目自带 id 清单的兼容问题;两端均使用 markdown-workbench/note-reviewer 实际文件验证。 +- 版本与权限集合须匹配发行;人设/模板/MCP/模型基本结构和递归秘密字段校验已加入。此处尚不替代各 Runtime 完整配置 schema、运行许可与 OS 沙箱。 +- 锁定 serde-saphyr 1.2.0 的 deserialize 特性与传递依赖,按其要求将声明 Rust 下限调整至 1.89;实际仍为 GNU 1.98.1,不作为最低工具链测试证据。 +- Community 全量 15 项通过(两个既有弃用警告);Rust desktop 全目标 61 项通过(两个既有特殊入口 ignored)。随后核对 Theme 专用 ID,7 项扩展验证复跑与最终 Clippy `-D warnings` 通过。日志 `.build/extension-manifest-rust-tests.log`。 +- 安全解包写盘、在线来源/撤回复核、安装库迁移、依赖与配置升级事务、授权和运行生命周期仍须实现,完整 D-01–04 和整体生产化继续进行。 diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index ec2792f..69e04d7 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -100,6 +100,23 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "anyhow" version = "1.0.104" @@ -118,6 +135,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayvec" version = "0.7.8" @@ -805,6 +828,12 @@ dependencies = [ "libc", ] +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1365,6 +1394,30 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encoding_rs" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7a45518d2863d18aa47f4a0cf9faec2aa4304cc09df5e41299f276b3ad135e" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + [[package]] name = "endi" version = "1.1.1" @@ -1933,6 +1986,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "granit-parser" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ec0d45986cd51c847c75c5b69a00852c4fc84d0e5e79f041173f73437d0cdf" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "group" version = "0.13.0" @@ -2872,6 +2935,34 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multiversion" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201" +dependencies = [ + "multiversion-macros", + "target-features", +] + +[[package]] +name = "multiversion-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "target-features", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "ndk" version = "0.9.0" @@ -2952,6 +3043,7 @@ dependencies = [ "rfd", "rusqlite", "serde", + "serde-saphyr", "serde_json", "sha2", "tauri", @@ -4226,6 +4318,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afb591f9cdb6223c88ba39269aff895620c7f0716dc42b705b5733d5c7c0823" +dependencies = [ + "annotate-snippets", + "encoding_rs_io", + "granit-parser", + "num-traits", + "serde_core", + "smallvec", +] + [[package]] name = "serde-untagged" version = "0.1.9" @@ -4430,6 +4536,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.3" @@ -4755,6 +4867,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-features" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5470,6 +5588,12 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 399a280..525a399 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -2,7 +2,7 @@ name = "notesagent-desktop" version = "0.3.0-alpha.1" edition = "2021" -rust-version = "1.88" +rust-version = "1.89" [lib] name = "notesagent_host" @@ -41,6 +41,7 @@ ed25519-dalek = { version = "2.2", default-features = false, features = ["std"] zip = { version = "8.6", default-features = false, features = ["deflate"] } unicode-normalization = "0.1" unicode-casefold = "0.2" +serde-saphyr = { version = "1.2", default-features = false, features = ["deserialize"] } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_RemoteDesktop", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader"] } diff --git a/frontend/src-tauri/src/extension_manifest.rs b/frontend/src-tauri/src/extension_manifest.rs new file mode 100644 index 0000000..9f2b199 --- /dev/null +++ b/frontend/src-tauri/src/extension_manifest.rs @@ -0,0 +1,241 @@ +//! Bounded declarative manifest inspection. No includes, environment interpolation or code execution. +use crate::{ + extension_package::{Inventory, Release}, + workspace::{HostError, Result}, +}; +use serde_json::Value; +use std::{ + collections::BTreeSet, + io::{Cursor, Read}, +}; + +fn invalid() -> HostError { + HostError::new("EXTENSION_MANIFEST_INVALID") +} +pub fn read(release: &Release, archive: &[u8], inventory: &Inventory) -> Result { + let mut zip = zip::ZipArchive::new(Cursor::new(archive)).map_err(|_| invalid())?; + let file = zip.by_name(&inventory.manifest).map_err(|_| invalid())?; + if file.size() > 1024 * 1024 { + return Err(HostError::new("EXTENSION_MANIFEST_LIMIT")); + } + let mut bytes = Vec::new(); + file.take(1024 * 1024 + 1) + .read_to_end(&mut bytes) + .map_err(|_| invalid())?; + if bytes.len() > 1024 * 1024 { + return Err(HostError::new("EXTENSION_MANIFEST_LIMIT")); + } + validate(release, &bytes) +} +pub fn validate(release: &Release, bytes: &[u8]) -> Result { + if bytes.len() > 1024 * 1024 { + return Err(HostError::new("EXTENSION_MANIFEST_LIMIT")); + } + let input = std::str::from_utf8(bytes).map_err(|_| invalid())?; + let yaml = matches!(release.kind.as_str(), "theme" | "plugin" | "skill"); + if !yaml { + serde_json::from_str::(input).map_err(|_| invalid())?; + } + let options = serde_saphyr::options! { + duplicate_keys: serde_saphyr::options::DuplicateKeyPolicy::Error, + with_snippet: false, + budget: serde_saphyr::budget! { max_documents:1, max_depth:32, max_nodes:10000, max_events:20000, max_total_scalar_bytes:1024*1024, max_aliases:128, max_anchors:128, max_recorded_anchor_events:10000, max_recorded_anchor_bytes:1024*1024 }, + }; + let value: Value = + serde_saphyr::from_str_with_options(input, options).map_err(|_| invalid())?; + let map = value.as_object().ok_or_else(invalid)?; + if map + .get("schema_version") + .is_some_and(|v| v.as_u64() != Some(1)) + { + return Err(HostError::new("EXTENSION_MANIFEST_SCHEMA")); + } + if yaml { + let identity = match release.kind.as_str() { + "theme" => "theme_id", + "plugin" => "plugin_id", + _ => "skill_id", + }; + let typed = map.get(identity); + let common = map.get("id"); + if (release.kind == "theme" && typed.is_none()) + || typed.or(common).and_then(Value::as_str) != Some(release.package_id.as_str()) + || typed.zip(common).is_some_and(|(a, b)| a != b) + || map.get("version").and_then(Value::as_str) != Some(release.version.as_str()) + { + return Err(HostError::new("EXTENSION_MANIFEST_IDENTITY")); + } + } + let permissions: Vec = map + .get("permissions") + .map(|v| serde_json::from_value(v.clone()).map_err(|_| invalid())) + .transpose()? + .unwrap_or_default(); + let actual: BTreeSet<_> = permissions.iter().collect(); + let expected: BTreeSet<_> = release.permissions.iter().collect(); + if actual != expected || actual.len() != permissions.len() { + return Err(HostError::new("EXTENSION_MANIFEST_PERMISSIONS")); + } + if !yaml { + fn safe(v: &Value) -> bool { + match v { + Value::Object(map) => map.iter().all(|(k, v)| { + !matches!( + k.to_lowercase().as_str(), + "api_key" | "password" | "token" | "secret" | "chat_history" | "messages" + ) && safe(v) + }), + Value::Array(items) => items.iter().all(safe), + _ => true, + } + } + if !safe(&value) { + return Err(HostError::new("EXTENSION_MANIFEST_SECRET")); + } + let valid = match release.kind.as_str() { + "persona" => value["system_prompt"].is_string(), + "template" => { + value["markdown"].is_string() + && value + .get("executable") + .is_none_or(|v| v == false || v.is_null()) + } + "model" => { + ["source", "revision", "license"] + .iter() + .all(|k| value[*k].as_str().is_some_and(|s| !s.is_empty())) + && value["resources"] + .as_object() + .is_some_and(|v| !v.is_empty()) + && value["verified_platforms"] + .as_array() + .is_some_and(|v| !v.is_empty() && v.iter().all(Value::is_string)) + } + "mcp" => match value["transport"].as_str() { + Some("stdio") => { + value["args"] + .as_array() + .is_some_and(|v| v.iter().all(Value::is_string)) + && value + .get("command") + .is_none_or(|v| v.as_str().is_some_and(|s| !s.is_empty())) + } + Some("streamable_http" | "sse") => true, + _ => false, + }, + _ => false, + }; + if !valid { + return Err(invalid()); + } + } + Ok(value) +} + +#[cfg(test)] +mod tests { + use super::*; + fn release(kind: &str) -> Release { + let mut v: Value = serde_json::from_str(include_str!( + "../../src/services/fixtures/community-python-vector.json" + )) + .unwrap(); + let value = v["release"].as_object_mut().unwrap(); + for key in ["release_id", "withdrawn", "download_path"] { + value.remove(key); + } + value.insert("type".into(), Value::String(kind.into())); + serde_json::from_value(v["release"].clone()).unwrap() + } + #[test] + fn real_repository_manifests_match_identity_version_and_signed_permissions() { + for (kind, id, bytes, permissions) in [ + ( + "plugin", + "markdown-workbench", + include_bytes!( + "../../../backend/extensions/community/plugins/markdown-workbench/plugin.yaml" + ) + .as_slice(), + vec![], + ), + ( + "skill", + "note-reviewer", + include_bytes!( + "../../../backend/extensions/community/skills/note-reviewer/skill.yaml" + ) + .as_slice(), + vec!["notes.search".into(), "notes.read".into()], + ), + ] { + let mut r = release(kind); + r.package_id = id.into(); + r.permissions = permissions; + validate(&r, bytes).unwrap(); + r.version = "9.0.0".into(); + assert!(validate(&r, bytes).is_err()); + } + assert!(validate(&release("theme"), b"id: test-package\nversion: 1.0.0").is_err()); + validate(&release("theme"), b"theme_id: test-package\nversion: 1.0.0").unwrap(); + let r = release("plugin"); + for bytes in [ + b"id: test-package\nplugin_id: other\nversion: 1.0.0".as_slice(), + b"id: test-package\nversion: 1.0.0\npermissions: [notes.read]", + b"id: test-package\nversion: 1.0.0\npermissions: false", + ] { + assert!(validate(&r, bytes).is_err()); + } + } + #[test] + fn duplicate_keys_documents_depth_and_includes_are_rejected() { + let r = release("plugin"); + for bytes in [ + b"id: test-package\nversion: 1.0.0\nversion: 2.0.0".as_slice(), + b"id: test-package\nversion: 1.0.0\n---\nid: other", + b"id: !include /etc/passwd\nversion: 1.0.0", + b"id: test-package\nversion: 1.0.0\nx: &a [*a]", + ] { + assert!(validate(&r, bytes).is_err()); + } + let deep = format!( + "id: test-package\nversion: 1.0.0\nx: {}0{}", + "[".repeat(40), + "]".repeat(40) + ); + assert!(validate(&r, deep.as_bytes()).is_err()); + assert_eq!( + validate(&r, &vec![b' '; 1024 * 1024 + 1]).unwrap_err().code, + "EXTENSION_MANIFEST_LIMIT" + ); + assert_eq!( + validate(&r, b"id: test-package\nversion: 1.0.0\nschema_version: 2") + .unwrap_err() + .code, + "EXTENSION_MANIFEST_SCHEMA" + ); + let persona = release("persona"); + assert!(validate( + &persona, + br#"{"system_prompt":"one","system_prompt":"two"}"# + ) + .is_err()); + assert!(validate(&persona, b"system_prompt: YAML is not JSON").is_err()); + } + #[test] + fn declarative_types_enforce_contract_and_nested_secret_exclusion() { + for (kind,bytes) in [("persona",br#"{"system_prompt":"hello"}"#.as_slice()),("template",br##"{"markdown":"# title","executable":false}"##),("mcp",br#"{"transport":"stdio","command":"python","args":["server.py"]}"#),("model",br#"{"source":"repository","revision":"fixed","license":"MIT","resources":{"ram_gb":8},"verified_platforms":["windows"]}"#)] { + let r=release(kind); let valid=validate(&r,bytes).unwrap(); + for field in ["api_key","token","password","secret","messages","chat_history"] { + let mut bad=valid.clone(); bad["nested"]=serde_json::json!([{field:"planted"}]); + assert_eq!(validate(&r,&serde_json::to_vec(&bad).unwrap()).unwrap_err().code,"EXTENSION_MANIFEST_SECRET"); + } + } + assert!(validate( + &release("template"), + br#"{"markdown":"x","executable":true}"# + ) + .is_err()); + assert!(validate(&release("mcp"), br#"{"transport":"stdio","args":[42]}"#).is_err()); + } +} diff --git a/frontend/src-tauri/src/extension_package.rs b/frontend/src-tauri/src/extension_package.rs index 41dc10a..8d47f63 100644 --- a/frontend/src-tauri/src/extension_package.rs +++ b/frontend/src-tauri/src/extension_package.rs @@ -66,6 +66,21 @@ fn version(s: &str) -> bool { }) } impl Release { + /// Full offline package check. Online revocation freshness and runtime permissions remain Host responsibilities. + pub fn verify_package( + &self, + pinned: &[u8; 32], + key_id: &str, + namespace: &str, + revoked: bool, + withdrawn: bool, + archive: &[u8], + ) -> Result<(Inventory, Value)> { + self.verify(pinned, key_id, namespace, revoked, withdrawn, archive)?; + let inventory = inspect(self, archive)?; + let manifest = crate::extension_manifest::read(self, archive, &inventory)?; + Ok((inventory, manifest)) + } pub fn validate(&self) -> Result<()> { let valid = self.schema_version == 1 && identity(&self.namespace) @@ -470,6 +485,10 @@ mod tests { .unwrap(); let inventory = inspect(&release, &bytes).unwrap(); assert_eq!(inventory.manifest, "persona.json"); + let (_, manifest) = release + .verify_package(&key, "test-key", "examples", false, false, &bytes) + .unwrap(); + assert!(manifest["system_prompt"].is_string()); let value = serde_json::to_value(&release).unwrap(); for field in value .as_object() diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 3ea75ea..be6f207 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -27,3 +27,6 @@ pub mod sync_retry; #[cfg(feature = "desktop")] pub mod extension_package; + +#[cfg(feature = "desktop")] +pub mod extension_manifest;