Skip to content

Commit 75c7760

Browse files
committed
Commit local changes (menu refactor, home updates, built-in catalog spec)
1 parent 766094a commit 75c7760

File tree

41 files changed

+1048
-481
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1048
-481
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ nuke publish-libs
279279
| `Agibuild.Modulus.Cli` | CLI tool |
280280
| `Agibuild.Modulus.Templates` | Project templates |
281281

282+
> Note: `modulus new` generates a `Directory.Build.props` with `ModulusCliLibDir` so newly generated modules can compile against the same Modulus assemblies shipped with the CLI.
283+
282284
## Contributing
283285

284286
Pull requests and issues are welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.

README.zh-CN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ nuke publish-libs
279279
| `Agibuild.Modulus.Cli` | CLI 工具 |
280280
| `Agibuild.Modulus.Templates` | 项目模板 |
281281

282+
> 说明:`modulus new` 会生成 `Directory.Build.props`,通过 `ModulusCliLibDir` 从 CLI 安装目录解析 `Modulus.*.dll`,确保新生成的模块可直接编译通过。
283+
282284
## 贡献
283285

284286
欢迎提交 Issue 和 PR!请参阅 [CONTRIBUTING.zh-CN.md](./CONTRIBUTING.zh-CN.md)

docs/getting-started.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,14 @@ The manifest file is automatically generated by the template and contains:
175175
<Assets>
176176
<!-- Module assemblies -->
177177
<Asset Type="Modulus.Package" Path="MyFirstModule.Core.dll" />
178+
179+
<!-- Host-specific UI packages -->
180+
<Asset Type="Modulus.Package" Path="MyFirstModule.UI.Avalonia.dll" TargetHost="Modulus.Host.Avalonia" />
181+
<Asset Type="Modulus.Package" Path="MyFirstModule.UI.Blazor.dll" TargetHost="Modulus.Host.Blazor" />
178182

179-
<!-- Menu registration -->
180-
<Asset Type="Modulus.Menu"
181-
Id="myfirstmodule-main"
182-
DisplayName="My First Module"
183-
Icon="Folder"
184-
Route="MyFirstModule.ViewModels.MainViewModel" />
183+
<!-- Menu declarations are NOT in the manifest anymore.
184+
Menus are declared via [AvaloniaMenu]/[BlazorMenu] on the host-specific module entry type
185+
and projected to the database at install/update time. -->
185186
</Assets>
186187
</PackageManifest>
187188
```
@@ -271,7 +272,12 @@ dotnet restore
271272

272273
### Module not appearing in menu
273274

274-
Check that your manifest has the correct `Modulus.Menu` asset with a valid `Route` pointing to your ViewModel.
275+
Check that your host-specific module entry type declares a menu attribute:
276+
277+
- Avalonia: `[AvaloniaMenu("key", "Display", typeof(MainViewModel), ...)]`
278+
- Blazor: `[BlazorMenu("key", "Display", "/route", ...)]`
279+
280+
Menus are read from the database at render time. If you upgraded from an older build, delete the host database file and restart.
275281

276282
## Getting Help
277283

docs/getting-started.zh-CN.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,14 @@ dotnet run --project path/to/Modulus/src/Hosts/Modulus.Host.Avalonia
175175
<Assets>
176176
<!-- 模块程序集 -->
177177
<Asset Type="Modulus.Package" Path="MyFirstModule.Core.dll" />
178+
179+
<!-- Host-specific UI packages -->
180+
<Asset Type="Modulus.Package" Path="MyFirstModule.UI.Avalonia.dll" TargetHost="Modulus.Host.Avalonia" />
181+
<Asset Type="Modulus.Package" Path="MyFirstModule.UI.Blazor.dll" TargetHost="Modulus.Host.Blazor" />
178182

179-
<!-- 菜单注册 -->
180-
<Asset Type="Modulus.Menu"
181-
Id="myfirstmodule-main"
182-
DisplayName="我的第一个模块"
183-
Icon="Folder"
184-
Route="MyFirstModule.ViewModels.MainViewModel" />
183+
<!-- 菜单不再在清单中声明。
184+
菜单通过 host-specific 模块入口类型上的 [AvaloniaMenu]/[BlazorMenu] 声明,
185+
并在安装/启动同步时投影到数据库。 -->
185186
</Assets>
186187
</PackageManifest>
187188
```
@@ -271,7 +272,12 @@ dotnet restore
271272

272273
### 模块未出现在菜单中
273274

274-
检查您的清单是否具有正确的 `Modulus.Menu` 资产,其 `Route` 指向您的 ViewModel。
275+
检查 host-specific 模块入口类型是否声明了菜单属性:
276+
277+
- Avalonia:`[AvaloniaMenu("key", "Display", typeof(MainViewModel), ...)]`
278+
- Blazor:`[BlazorMenu("key", "Display", "/route", ...)]`
279+
280+
导航菜单渲染时只读取数据库。如从旧版本升级,请删除 Host 的数据库文件后重启。
275281

276282
## 获取帮助
277283

docs/module-development.md

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -257,15 +257,9 @@ public class MyModuleAvaloniaModule : ModulusPackage
257257
TargetHost="Modulus.Host.Avalonia" />
258258
<Asset Type="Modulus.Package" Path="MyModule.UI.Blazor.dll"
259259
TargetHost="Modulus.Host.Blazor" />
260-
261-
<!-- Menu items -->
262-
<Asset Type="Modulus.Menu"
263-
Id="mymodule-main"
264-
DisplayName="My Module"
265-
Icon="Folder"
266-
Route="MyModule.ViewModels.MainViewModel"
267-
Location="Main"
268-
Order="100" />
260+
261+
<!-- Menu items are NOT declared in the manifest anymore.
262+
They are declared via attributes on the host-specific module entry type and projected to DB at install/update time. -->
269263
</Assets>
270264
</PackageManifest>
271265
```
@@ -280,18 +274,16 @@ public class MyModuleAvaloniaModule : ModulusPackage
280274

281275
### Multiple Menus
282276

283-
```xml
284-
<Assets>
285-
<Asset Type="Modulus.Menu" Id="mymodule-main"
286-
DisplayName="Dashboard" Icon="Home"
287-
Route="MyModule.ViewModels.DashboardViewModel"
288-
Location="Main" Order="10" />
289-
290-
<Asset Type="Modulus.Menu" Id="mymodule-settings"
291-
DisplayName="Settings" Icon="Settings"
292-
Route="MyModule.ViewModels.SettingsViewModel"
293-
Location="Bottom" Order="100" />
294-
</Assets>
277+
```csharp
278+
// Avalonia UI module entry
279+
[AvaloniaMenu("dashboard", "Dashboard", typeof(DashboardViewModel), Icon = IconKind.Home, Location = MenuLocation.Main, Order = 10)]
280+
[AvaloniaMenu("settings", "Settings", typeof(SettingsViewModel), Icon = IconKind.Settings, Location = MenuLocation.Bottom, Order = 100)]
281+
public sealed class MyModuleAvaloniaModule : ModulusPackage { }
282+
283+
// Blazor UI module entry
284+
[BlazorMenu("dashboard", "Dashboard", "/dashboard", Icon = IconKind.Home, Location = MenuLocation.Main, Order = 10)]
285+
[BlazorMenu("settings", "Settings", "/settings", Icon = IconKind.Settings, Location = MenuLocation.Bottom, Order = 100)]
286+
public sealed class MyModuleBlazorModule : ModulusPackage { }
295287
```
296288

297289
## Testing
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Design: Built-in module catalog security and dev-mode integrity policy
2+
3+
## Scope
4+
本设计覆盖:
5+
- 自带模块(Built-in / System modules)如何由 Host 项目引用确定、随应用发布、启动时入库、并进行完整性校验以防替换。
6+
- Development 环境下的完整性策略(仅校验程序集名/强名称,仍使用 `{AppBaseDir}/Modules/`)。
7+
- 系统模块不可卸载/不可禁用的强制规则(多层防线 + 启动自修复)。
8+
- 用户模块防回滚(仅用户模块),数据存 DB。
9+
10+
不在本设计范围:
11+
- 自带模块独立更新、签名分发、模块市场、供应链签名验证。
12+
- 兼容旧模块/旧数据库(本版本不兼容)。
13+
14+
## Definitions
15+
- **Built-in module / System module**:随 Host 应用一起发布的模块集合,来源是 Host `.csproj``ProjectReference` 列表(build-time)。
16+
- **User module**:安装在用户目录的模块集合(可卸载/可禁用)。
17+
- **Catalog**:不可篡改的自带模块清单,编译进 Host 程序集,作为 allowlist。
18+
- **Integrity check**:对模块包(manifest + 目标程序集)的校验,用于防止“替换模块文件”。
19+
- **HostType**:例如 `Modulus.Host.Avalonia` / `Modulus.Host.Blazor`
20+
- **AppBaseDir**`AppContext.BaseDirectory`
21+
22+
## Goals
23+
- **G1**:自带模块集合必须由 Host 项目引用确定,不允许通过扫描 `{AppBaseDir}/Modules/` 注入。
24+
- **G2**:Production 环境下,自带模块必须通过强完整性校验(SHA256)才能入库并加载。
25+
- **G3**:Development 环境下(`DOTNET_ENVIRONMENT=Development`),允许频繁编译导致 hash 变化,但仍要限制注入面(仅允许 allowlist + 程序集名/强名称校验 + 必须在 `{AppBaseDir}/Modules/`)。
26+
- **G4**:系统模块不允许卸载、不允许禁用;即使 DB 被篡改也能自修复。
27+
- **G5**:菜单渲染只来自 DB;模块属性只用于入库/安装/启动同步阶段投影。
28+
- **G6**:防回滚仅针对用户模块,状态存 DB,install/update 时拒绝降级。
29+
30+
## Non-Goals
31+
- **NG1**:不处理攻击者具备写入/替换 Host 程序集的场景(那属于 OS 权限与应用签名问题)。
32+
- **NG2**:不支持“系统模块在用户目录 overlay 更新”的机制。
33+
34+
## Architecture
35+
36+
### A) Built-in Module Catalog
37+
Catalog 由构建阶段生成并编译进 Host(建议在 Host 工程下生成 `BuiltInModuleCatalog.g.cs`):
38+
39+
- **Input**:Host `.csproj` 中标记为 built-in 的 `ProjectReference`(推荐通过 item metadata,例如 `ModulusBuiltIn="true"`)。
40+
- **Output**`BuiltInModuleCatalog`(强类型)包含 entries:
41+
- `ModuleId`(GUID string)
42+
- `ModuleName`
43+
- `RelativePackageDir`:固定 `Modules/{ModuleName}/`
44+
- `ManifestSha256`(Production 用)
45+
- `Files`:每个文件 `RelativePath + Sha256 + AssemblyIdentity(可选)`
46+
- `Hosts`:该模块支持的 host(用于选择 UI 目标程序集)
47+
48+
**关键约束**
49+
- Runtime 绝不通过扫描 `{AppBaseDir}/Modules` 发现系统模块。
50+
- 系统模块加载路径不信任 DB,只信 Catalog 的 `{AppBaseDir}/{RelativePackageDir}`
51+
52+
### B) Integrity policy (Production vs Development)
53+
依据 `DOTNET_ENVIRONMENT` 判定:
54+
- `Development`:Dev policy
55+
- 其它:Production policy
56+
57+
#### Production policy
58+
- **必须校验**
59+
- `extension.vsixmanifest` SHA256 == `Catalog.ManifestSha256`
60+
- 当前 HostType 需要加载的目标程序集(例如 `*.UI.Blazor.dll``*.UI.Avalonia.dll`)SHA256 == Catalog 记录
61+
- `*.Core.dll`(如果属于系统模块包的一部分且会被加载)SHA256 == Catalog 记录
62+
- **失败处理**
63+
- 不入库(或将模块标记为 `Tampered`
64+
- 不加载
65+
- 记录可诊断日志
66+
67+
#### Development policy
68+
为了不阻断开发迭代:
69+
- **路径约束**:仍必须位于 `{AppBaseDir}/Modules/{ModuleName}/`(禁止从用户目录覆盖系统模块)。
70+
- **程序集校验**:不做 SHA256;仅校验目标程序集的:
71+
- `AssemblyName.Name` 匹配 Catalog 期望值
72+
- StrongName 公钥(或 PublicKeyToken)匹配 Catalog 期望值(若程序集是强签名)
73+
- 若程序集未强签名,则仅做 `AssemblyName.Name` 校验并产生 warning(显式提示风险仅限开发环境)
74+
- **manifest 校验**
75+
- 校验 manifest 中的 `ModuleId` 必须等于 Catalog 的 `ModuleId`
76+
- 校验 `InstallationTarget` 包含当前 HostType(并校验 Version range 满足当前 HostVersion)
77+
- 不校验 manifest SHA256(避免改 manifest 时阻断开发)
78+
79+
> 以上满足你要求:Development 模式仅校验程序集名/强名称,且仍使用 `{AppBaseDir}/Modules/`
80+
81+
### C) Startup sync (2B) and menu projection
82+
启动链路(DB migrate 之后、模块加载之前)执行:
83+
84+
1. `SyncBuiltInModulesAsync(hostType)`
85+
- 遍历 Catalog entries(仅 allowlist)
86+
- 计算模块 package dir:`{AppBaseDir}/{RelativePackageDir}`
87+
- 依据环境执行完整性校验(Prod/Dev policy)
88+
- 通过校验后执行 2B 入库:
89+
- 触发条件:
90+
- DB 无该模块
91+
- `ManifestHash` !=(Production:Catalog manifest sha;Development:可跳过此项,仅在缺失时写)
92+
- 当前 hostType 的菜单记录缺失
93+
- 动作:
94+
- Upsert `ModuleEntity`:强制 `IsSystem=true``IsEnabled=true``Path` 写为相对路径(如 `Modules/{ModuleName}/` 或 manifest 相对路径)
95+
- `ReplaceModuleMenusAsync`:使用 metadata-only 读取菜单属性写库
96+
2. `Integrity check`(若系统已有 checker):
97+
- 对系统模块:额外验证 `DB.Path` 不参与路径决策;仅用于展示/诊断
98+
3. 加载阶段:
99+
- 从 DB 查询 `IsEnabled=true && State=Ready` 的模块
100+
-`IsSystem=true`:加载路径来自 Catalog
101+
102+
### D) System module enforcement
103+
系统模块强制规则:
104+
- 禁止卸载(uninstall)
105+
- 禁止禁用(disable)
106+
- 启动自修复:若 DB 中系统模块 `IsEnabled=false``IsSystem=false` → 纠正并记录 warning
107+
108+
强制点(多层):
109+
- `IModuleInstallerService` / uninstall service:拒绝
110+
- `IModuleRepository` 写入路径:系统模块写入时强制字段
111+
- CLI 命令层:拒绝并返回明确错误
112+
- UI 操作入口:隐藏或置灰(但不能只靠 UI)
113+
114+
### E) User module anti-rollback (DB-backed)
115+
仅针对用户模块(用户目录可写):
116+
- DB 存储 per-module 的最大已接受版本(推荐 SemVer 比较):
117+
- `UserModulePolicyEntity``ModuleId`, `MaxAcceptedVersion`, `UpdatedAt`
118+
- 安装/更新时:
119+
-`incomingVersion < MaxAcceptedVersion` → 拒绝(默认)
120+
- 若安装成功且版本更高 → 更新 `MaxAcceptedVersion`
121+
- 可选:提供 `--force` 仅用于开发/测试(默认生产环境关闭,或需要显式配置开启)
122+
123+
## Operational notes
124+
- 对系统模块,“防替换”依赖应用安装目录写权限与 Production 校验共同生效。
125+
- Development 政策必须显式由 `DOTNET_ENVIRONMENT=Development` 开启,避免误入生产。
126+
127+
## Open Questions
128+
- 系统模块 DLL 是否全部强签名?若不是,Development 下强名称校验只能部分生效,需要定义 warning 策略。
129+
- User module 的版本比较使用何种 SemVer 实现与规范(NuGet.Versioning 推荐)。
130+
131+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Change: Add built-in module catalog security and dev-mode integrity policy
2+
3+
## Why
4+
- 自带模块与应用绑定,必须防止“投放/替换”模块文件带来的安全风险。
5+
- 菜单渲染只读数据库,自带模块需要在启动时可靠入库、可诊断、可控。
6+
- 开发阶段频繁编译会导致 hash 变化,需要一个明确的 Development 策略避免开发体验崩坏。
7+
- 防回滚主要针对用户模块(用户目录可写),自带模块随应用更新不需要单独防回滚。
8+
9+
## What Changes
10+
- **Built-in Module Catalog**:Host build-time 依据 `ProjectReference` 生成不可篡改的自带模块清单(编译进 Host)。
11+
- **Integrity policy**
12+
- Production:自带模块的 `extension.vsixmanifest` 与目标程序集文件 SHA256 必须与 Catalog 匹配,否则拒绝入库/加载。
13+
- Development(`DOTNET_ENVIRONMENT=Development`):自带模块不做 SHA256 校验,仅校验程序集名/强名称(且仍要求位于 `{AppBaseDir}/Modules/`)。
14+
- **System module enforcement**:自带模块 `IsSystem=true`**不允许卸载/禁用**,任何操作请求直接拒绝并在启动时自修复。
15+
- **Menu projection**:自带模块按方案 2B(缺失/变更/菜单缺失才投影)在启动时入库;导航渲染只读 DB。
16+
- **User module anti-rollback**:仅对用户模块启用防回滚策略(保存到 DB 并在 install/update 时拒绝降级)。
17+
- **No backward compatibility**:不兼容旧数据与旧逻辑;对旧数据库结构/旧菜单来源失败快并提示删除数据库。
18+
19+
## Impact
20+
- Affected specs:
21+
- `openspec/specs/runtime/spec.md`
22+
- `openspec/specs/module-packaging/spec.md`
23+
- Affected code (implementation stage):
24+
- Host build pipeline (MSBuild/Nuke) for catalog generation
25+
- `Modulus.Core.Runtime` startup sync & integrity gates
26+
- `Modulus.Core.Installation` for projection triggers
27+
- DB schema for user module anti-rollback
28+
29+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# module-packaging Specification (delta)
2+
3+
## ADDED Requirements
4+
5+
### Requirement: User module anti-rollback stored in database
6+
用户模块安装/更新 MUST 实施防回滚策略:系统 SHALL 在数据库中记录每个用户模块的最大已接受版本,并在安装时拒绝降级。
7+
8+
#### Scenario: Install higher version updates max accepted version
9+
- **WHEN** 用户安装用户模块版本 `1.2.0`
10+
- **AND** 数据库中该模块的 `MaxAcceptedVersion` 不存在或小于 `1.2.0`
11+
- **THEN** 安装成功
12+
- **AND** 数据库将 `MaxAcceptedVersion` 更新为 `1.2.0`
13+
14+
#### Scenario: Install lower version is rejected as rollback
15+
- **WHEN** 数据库中用户模块 `MaxAcceptedVersion``1.2.0`
16+
- **AND** 用户尝试安装版本 `1.1.0`
17+
- **THEN** 安装失败
18+
- **AND** 输出错误:检测到回滚安装(downgrade)被拒绝。
19+
20+
## MODIFIED Requirements
21+
22+
### Requirement: Built-in module selection via host project references
23+
构建系统 SHALL 允许 Host 通过项目引用选择“随应用发布”的内置模块集合,并生成不可篡改的 Built-in Module Catalog;运行时不将模块程序集加载进默认 ALC,且系统模块不依赖目录扫描发现。
24+
25+
#### Scenario: Host includes built-in modules without referencing output assembly
26+
- **WHEN** Host 项目引用一个内置模块项目
27+
- **THEN** 该引用使用 `ProjectReference` 并设置 `ReferenceOutputAssembly="false"`
28+
- **AND** 构建输出将模块产物输出到 `artifacts/bin/Modules/{ModuleName}/`
29+
- **AND** 发布时将该模块复制到 `{AppBaseDir}/Modules/{ModuleName}/`
30+
- **AND** 构建阶段生成 Built-in Module Catalog(包含 ModuleId、相对路径与完整性信息)并编译进 Host
31+
- **AND** Host 运行时仍由 `ModuleLoader` 在独立 ALC 中加载该模块(不经默认 ALC)。
32+
33+

0 commit comments

Comments
 (0)