create-windy 0.3.19 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -13
- package/dist/cli.js +788 -280
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +34 -10
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +18 -8
- package/template/README.md +32 -22
- package/template/apps/server/Dockerfile +1 -0
- package/template/apps/server/package.json +1 -0
- package/template/apps/server/scripts/validate-module-composition-once.ts +4 -0
- package/template/apps/server/scripts/validate-module-composition.ts +28 -0
- package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -5
- package/template/apps/server/src/agent/routes.test.ts +20 -7
- package/template/apps/server/src/agent/routes.ts +14 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +22 -6
- package/template/apps/server/src/agent/sandbox-worker.ts +2 -2
- package/template/apps/server/src/agent/unavailable-routes.ts +33 -0
- package/template/apps/server/src/app/server-composition.ts +56 -0
- package/template/apps/server/src/application-services.ts +2 -0
- package/template/apps/server/src/bulk-data/service.ts +14 -0
- package/template/apps/server/src/data-governance/export/authorization-port.ts +1 -1
- package/template/apps/server/src/data-governance/export/postgres-api-fixture.ts +18 -2
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +10 -9
- package/template/apps/server/src/data-governance/export/routes-test-fixture.ts +6 -8
- package/template/apps/server/src/data-governance/user-plaintext-routes.test.ts +4 -2
- package/template/apps/server/src/feature/routes.authorization.test.ts +26 -1
- package/template/apps/server/src/feature/routes.ts +33 -15
- package/template/apps/server/src/foundation-activation.test.ts +60 -0
- package/template/apps/server/src/foundation.ts +64 -5
- package/template/apps/server/src/guards.test.ts +23 -0
- package/template/apps/server/src/guards.ts +12 -0
- package/template/apps/server/src/index.ts +28 -34
- package/template/apps/server/src/installed-business-modules.ts +13 -24
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +106 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +40 -0
- package/template/apps/server/src/license/activation.test.ts +30 -0
- package/template/apps/server/src/license/activation.ts +26 -0
- package/template/apps/server/src/license/restriction.test.ts +16 -0
- package/template/apps/server/src/license/routes-offline-code.test.ts +6 -2
- package/template/apps/server/src/license/routes.ts +20 -4
- package/template/apps/server/src/license/runtime-service.test.ts +10 -2
- package/template/apps/server/src/module-bootstrap.ts +14 -23
- package/template/apps/server/src/module-composition.test.ts +43 -2
- package/template/apps/server/src/module-composition.ts +29 -0
- package/template/apps/server/src/module-host/durable-context-guard.test.ts +172 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +50 -1
- package/template/apps/server/src/module-host/durable-context.test.ts +42 -25
- package/template/apps/server/src/module-host/durable-context.ts +42 -2
- package/template/apps/server/src/module-host/durable-handler-registration.ts +96 -5
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +185 -0
- package/template/apps/server/src/module-host/implementation-completeness.ts +132 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +78 -2
- package/template/apps/server/src/module-host/initialize.ts +15 -4
- package/template/apps/server/src/module-host/project-attachments.test.ts +92 -0
- package/template/apps/server/src/module-host/project-attachments.ts +58 -0
- package/template/apps/server/src/module-host/project-composition-package.test.ts +186 -0
- package/template/apps/server/src/module-host/project-composition.test.ts +260 -0
- package/template/apps/server/src/module-host/project-composition.ts +311 -0
- package/template/apps/server/src/module-host/request-context.ts +3 -0
- package/template/apps/server/src/module-host/route-declaration.ts +15 -6
- package/template/apps/server/src/module-host/route-installer.test.ts +15 -2
- package/template/apps/server/src/module-host/route-installer.ts +12 -2
- package/template/apps/server/src/module-host/route-registration.test.ts +13 -4
- package/template/apps/server/src/module-host/runtime-composition-compatibility.test.ts +63 -0
- package/template/apps/server/src/module-host/runtime-composition-compatibility.ts +46 -0
- package/template/apps/server/src/module-host/runtime-composition.ts +34 -0
- package/template/apps/server/src/module-host/search-provider-registration.test.ts +64 -0
- package/template/apps/server/src/module-host/search-provider-registration.ts +35 -0
- package/template/apps/server/src/module-host/static-route-authorization.test.ts +99 -0
- package/template/apps/server/src/module-host/static-route-authorization.ts +90 -0
- package/template/apps/server/src/module-host/task-definitions.ts +1 -0
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +4 -1
- package/template/apps/server/src/module-host.test.ts +2 -0
- package/template/apps/server/src/module-preflight.test.ts +289 -0
- package/template/apps/server/src/module-preflight.ts +96 -0
- package/template/apps/server/src/module-runtime-validation.ts +1 -0
- package/template/apps/server/src/platform-app-bootstrap.ts +50 -0
- package/template/apps/server/src/platform-http-runtime.ts +32 -7
- package/template/apps/server/src/route-guards.test.ts +22 -0
- package/template/apps/server/src/route-guards.ts +10 -3
- package/template/apps/server/src/runtime-audit-query.ts +50 -0
- package/template/apps/server/src/runtime-feature.test.ts +46 -0
- package/template/apps/server/src/runtime-feature.ts +5 -0
- package/template/apps/server/src/runtime.ts +10 -49
- package/template/apps/server/src/scheduler/audit-action.test.ts +49 -0
- package/template/apps/server/src/scheduler/audit.ts +1 -1
- package/template/apps/server/src/scheduler/execution-recorder.ts +10 -5
- package/template/apps/server/src/scheduler/recovery-service.test.ts +4 -3
- package/template/apps/server/src/scheduler/service-idle.test.ts +4 -3
- package/template/apps/server/src/scheduler/service.test.ts +1 -0
- package/template/apps/server/src/scheduler/service.ts +18 -21
- package/template/apps/server/src/scheduler/types.ts +5 -1
- package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
- package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
- package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
- package/template/apps/server/src/settings/watermark-routes.ts +29 -1
- package/template/apps/server/src/startup-composition-validation.test.ts +35 -0
- package/template/apps/server/src/startup-composition-validation.ts +54 -0
- package/template/apps/server/src/startup-preflight.test.ts +36 -0
- package/template/apps/server/src/startup-preflight.ts +37 -0
- package/template/apps/server/src/system/audit-routes.test.ts +9 -3
- package/template/apps/server/src/system/department-routes.test.ts +5 -3
- package/template/apps/server/src/system/feature-routes.test.ts +4 -3
- package/template/apps/server/src/system/interface-catalog.test.ts +5 -3
- package/template/apps/server/src/system/interface-catalog.ts +3 -0
- package/template/apps/server/src/system/menu-routes.test.ts +5 -3
- package/template/apps/server/src/system/route-test-runtime.ts +24 -0
- package/template/apps/server/src/system/routes-validation.test.ts +7 -3
- package/template/apps/server/src/system/routes.test.ts +21 -11
- package/template/apps/server/src/system/routes.ts +11 -2
- package/template/apps/server/src/work-order/foundation-modules.test.ts +6 -4
- package/template/apps/server/src/work-order/routes.test.ts +8 -0
- package/template/apps/server/src/work-order/task.test.ts +6 -1
- package/template/apps/web/Dockerfile +5 -0
- package/template/apps/web/package.json +1 -0
- package/template/apps/web/scripts/validate-admin-ui-composition.ts +106 -0
- package/template/apps/web/src/app/web-composition.ts +24 -0
- package/template/apps/web/src/app/web-composition.webtest.ts +15 -0
- package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
- package/template/apps/web/src/composables/watermark-settings.ts +40 -11
- package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
- package/template/apps/web/src/layout/AppSidebar.vue +8 -1
- package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
- package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
- package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +8 -1
- package/template/apps/web/src/layout/navigation.ts +29 -4
- package/template/apps/web/src/layout/navigation.webtest.ts +51 -0
- package/template/apps/web/src/pages/dashboard/dashboard-presenter.ts +6 -1
- package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
- package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
- package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.vue +202 -0
- package/template/apps/web/src/pages/work-orders/WorkOrderListPage.webtest.ts +51 -0
- package/template/apps/web/src/router/admin-extension-loader.ts +44 -0
- package/template/apps/web/src/router/admin-extension-loader.webtest.ts +49 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.ts +33 -0
- package/template/apps/web/src/router/admin-ui-composition-plugin.webtest.ts +20 -0
- package/template/apps/web/src/router/admin-ui-composition.ts +313 -0
- package/template/apps/web/src/router/admin-ui-composition.webtest.ts +225 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.ts +55 -0
- package/template/apps/web/src/router/admin-ui-module-registry-plugin.webtest.ts +46 -0
- package/template/apps/web/src/router/business-routes.ts +32 -45
- package/template/apps/web/src/router/business-routes.webtest.ts +40 -44
- package/template/apps/web/src/router/index.ts +64 -54
- package/template/apps/web/src/router/manifest-routes.ts +27 -7
- package/template/apps/web/src/router/manifest-routes.webtest.ts +10 -2
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +58 -0
- package/template/apps/web/src/router/reserved-route-paths.ts +14 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +10 -4
- package/template/apps/web/src/router/web-composition-host.ts +20 -0
- package/template/apps/web/src/services/work-order-api.ts +16 -0
- package/template/apps/web/src/services/work-order-api.webtest.ts +21 -0
- package/template/apps/web/src/virtual-admin-ui-modules.d.ts +5 -0
- package/template/apps/web/vite.config.ts +22 -1
- package/template/docs/architecture/migration-bundle.md +10 -7
- package/template/docs/architecture/module-manifest-composition.md +85 -3
- package/template/docs/architecture/search-provider-protocol.md +6 -1
- package/template/docs/architecture/server-module-interface.md +70 -7
- package/template/docs/development/business-web-routes.md +53 -50
- package/template/docs/development/module-lifecycle.md +48 -54
- package/template/docs/platform/navigation-registry.md +5 -4
- package/template/docs/platform/platform-shell-settings.md +15 -10
- package/template/docs/platform/web-system-crud.md +1 -1
- package/template/package.json +1 -1
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
- package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/platform-settings.ts +3 -0
- package/template/packages/example-work-order/package.json +6 -0
- package/template/packages/example-work-order/src/admin-page.ts +78 -0
- package/template/packages/example-work-order/src/admin.ts +35 -0
- package/template/packages/example-work-order/src/manifest.test.ts +62 -5
- package/template/packages/example-work-order/src/manifest.ts +54 -7
- package/template/packages/example-work-order/src/migration-bundle.ts +8 -0
- package/template/packages/example-work-order/src/server-module.ts +2 -2
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +4 -0
- package/template/packages/modules/package.json +3 -2
- package/template/packages/modules/src/admin-ui-entry.ts +8 -0
- package/template/packages/modules/src/admin-ui-manifest.test.ts +52 -0
- package/template/packages/modules/src/admin-ui-manifest.ts +53 -0
- package/template/packages/modules/src/capability-audit-validation.test.ts +127 -0
- package/template/packages/modules/src/capability-audit-validation.ts +38 -0
- package/template/packages/modules/src/capability-guard-validation.ts +148 -0
- package/template/packages/modules/src/capability-key-validation.test.ts +150 -0
- package/template/packages/modules/src/composition-fixtures.test-support.ts +7 -5
- package/template/packages/modules/src/composition.test.ts +74 -9
- package/template/packages/modules/src/composition.ts +8 -0
- package/template/packages/modules/src/manifest.ts +1 -0
- package/template/packages/modules/src/migration-bundle-validation.test.ts +23 -0
- package/template/packages/modules/src/module-host-range.test.ts +57 -0
- package/template/packages/modules/src/module-host-range.ts +125 -0
- package/template/packages/modules/src/project-module-assembly.test.ts +133 -0
- package/template/packages/modules/src/project-module-assembly.ts +126 -0
- package/template/packages/modules/src/system-admin-routes.ts +6 -5
- package/template/packages/modules/src/system-admin-ui-catalog.ts +72 -0
- package/template/packages/modules/src/system-agent-tools.ts +2 -2
- package/template/packages/modules/src/system-api-binding.ts +43 -9
- package/template/packages/modules/src/system-api-control-plane.ts +202 -0
- package/template/packages/modules/src/system-api-data-governance.ts +4 -2
- package/template/packages/modules/src/system-api-operational.ts +57 -8
- package/template/packages/modules/src/system-api-permissions.ts +115 -21
- package/template/packages/modules/src/system-api-watermark.ts +3 -0
- package/template/packages/modules/src/system-audit-actions.ts +7 -0
- package/template/packages/modules/src/system-data-governance.ts +5 -2
- package/template/packages/modules/src/system-features.ts +31 -0
- package/template/packages/modules/src/system-governed-export-bindings.ts +8 -4
- package/template/packages/modules/src/system-module-catalog.test.ts +39 -4
- package/template/packages/modules/src/system-module-catalog.ts +68 -7
- package/template/packages/modules/src/system-module-tasks.ts +8 -1
- package/template/packages/modules/src/system-modules-access.test.ts +4 -0
- package/template/packages/modules/src/system-modules.test.ts +10 -2
- package/template/packages/modules/src/system-modules.ts +6 -5
- package/template/packages/modules/src/system-permissions.ts +4 -0
- package/template/packages/modules/user/module.ts +19 -2
- package/template/packages/server-sdk/index.ts +8 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/durable-handler.ts +16 -1
- package/template/packages/server-sdk/src/module-adapter.test.ts +44 -0
- package/template/packages/server-sdk/src/module-adapter.ts +59 -0
- package/template/packages/server-sdk/src/route-definition.ts +3 -2
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.test.ts +7 -0
- package/template/packages/shared/src/audit.ts +16 -0
- package/template/packages/shared/src/migration-bundle-validation.ts +9 -11
- package/template/packages/shared/src/platform-settings.ts +17 -0
- package/template/packages/shared/src/rbac.ts +2 -0
- package/template/apps/web/src/app/business-routes.ts +0 -22
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { fixture } from "./composition-fixtures.test-support.js";
|
|
3
|
+
import { validateModuleComposition } from "./composition.js";
|
|
4
|
+
import { systemFoundationModule } from "./system-modules.js";
|
|
5
|
+
|
|
6
|
+
describe("API 与定时任务 Audit Action 组合门禁", () => {
|
|
7
|
+
test("非匿名 API 缺少 Audit Action 时 fail closed", () => {
|
|
8
|
+
const module = fixture("api-audit");
|
|
9
|
+
Reflect.deleteProperty(module.apiPermissions[0]!, "auditAction");
|
|
10
|
+
|
|
11
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
12
|
+
"API Route GET /api/api-audit 必须声明 Audit Action",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("定时任务缺少 Audit Action 时 fail closed", () => {
|
|
17
|
+
const module = fixture("task-audit");
|
|
18
|
+
Reflect.deleteProperty(module.tasks[0]!, "auditAction");
|
|
19
|
+
|
|
20
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
21
|
+
"定时任务 task-audit.task 必须声明 Audit Action",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test.each([
|
|
26
|
+
[
|
|
27
|
+
"API Route GET /api/api-audit",
|
|
28
|
+
(module: ReturnType<typeof fixture>) => {
|
|
29
|
+
module.apiPermissions[0]!.auditAction = "audit.unknown";
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"定时任务 task-audit.task",
|
|
34
|
+
(module: ReturnType<typeof fixture>) => {
|
|
35
|
+
module.tasks[0]!.auditAction = "audit.unknown";
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
] as const)("%s 引用未知 Audit Action 时 fail closed", (label, mutate) => {
|
|
39
|
+
const module = fixture(
|
|
40
|
+
label.startsWith("API") ? "api-audit" : "task-audit",
|
|
41
|
+
);
|
|
42
|
+
mutate(module);
|
|
43
|
+
|
|
44
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
45
|
+
`${label} 引用了未注册审计动作:audit.unknown`,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("显式匿名 API 可以不声明 Audit Action", () => {
|
|
50
|
+
const module = fixture("public");
|
|
51
|
+
module.apiPermissions[0]!.anonymous = true;
|
|
52
|
+
Reflect.deleteProperty(module.apiPermissions[0]!, "auditAction");
|
|
53
|
+
|
|
54
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Provider 不虚构执行 Audit Action", () => {
|
|
58
|
+
const module = fixture("provider");
|
|
59
|
+
|
|
60
|
+
expect("auditAction" in module.providers[0]!).toBe(false);
|
|
61
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("系统 API 与任务引用的 Audit Action 均由 Manifest 注册", () => {
|
|
65
|
+
const manifest = systemFoundationModule();
|
|
66
|
+
const registered = new Set(
|
|
67
|
+
manifest.auditActions.map((action) =>
|
|
68
|
+
typeof action === "string" ? action : action.key,
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
const used = [
|
|
72
|
+
...manifest.apiPermissions
|
|
73
|
+
.filter(({ anonymous }) => anonymous !== true)
|
|
74
|
+
.map(({ auditAction }) => auditAction),
|
|
75
|
+
...manifest.tasks.map(({ auditAction }) => auditAction),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
expect(used.every((action) => action && registered.has(action))).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("系统资源 API 按读取、导入、导出和 CRUD 语义声明 Audit Action", () => {
|
|
82
|
+
const bindings = systemFoundationModule().apiPermissions.filter(
|
|
83
|
+
({ path }) => path.startsWith("/api/system/roles"),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(bindings.map(({ auditAction }) => auditAction)).toEqual([
|
|
87
|
+
"data.read",
|
|
88
|
+
"data.export",
|
|
89
|
+
"data.read",
|
|
90
|
+
"data.import",
|
|
91
|
+
"crud.create",
|
|
92
|
+
"crud.update",
|
|
93
|
+
"crud.delete",
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test.each([
|
|
98
|
+
// @windy-module system.search begin
|
|
99
|
+
["POST", "/api/search", "search.query"],
|
|
100
|
+
// @windy-module system.search end
|
|
101
|
+
["PUT", "/api/system/menus/placements", "menu.structure.update"],
|
|
102
|
+
["PUT", "/api/system/features/:id", "feature.update"],
|
|
103
|
+
// @windy-module system.scheduler begin
|
|
104
|
+
["POST", "/api/system/scheduled-tasks/:id/trigger", "task.trigger"],
|
|
105
|
+
// @windy-module system.scheduler end
|
|
106
|
+
// @windy-module system.bulk-data begin
|
|
107
|
+
[
|
|
108
|
+
"POST",
|
|
109
|
+
"/api/system/bulk-data/dictionaries/import",
|
|
110
|
+
"data.bulk-import.create",
|
|
111
|
+
],
|
|
112
|
+
// @windy-module system.bulk-data end
|
|
113
|
+
// @windy-module system.storage begin
|
|
114
|
+
[
|
|
115
|
+
"PUT",
|
|
116
|
+
"/api/system/storage/uploads/:id/parts/:partNumber",
|
|
117
|
+
"file.upload.part.write",
|
|
118
|
+
],
|
|
119
|
+
// @windy-module system.storage end
|
|
120
|
+
] as const)("%s %s 声明真实动作 %s", (method, path, auditAction) => {
|
|
121
|
+
expect(
|
|
122
|
+
systemFoundationModule().apiPermissions.find(
|
|
123
|
+
(binding) => binding.method === method && binding.path === path,
|
|
124
|
+
),
|
|
125
|
+
).toMatchObject({ auditAction });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AuditActionType } from "@southwind-ai/shared";
|
|
2
|
+
import type { ModuleManifest } from "./manifest.js";
|
|
3
|
+
|
|
4
|
+
export function validateCapabilityAuditActions(
|
|
5
|
+
modules: readonly ModuleManifest[],
|
|
6
|
+
auditActions: ReadonlySet<string>,
|
|
7
|
+
): void {
|
|
8
|
+
for (const module of modules) {
|
|
9
|
+
for (const route of module.apiPermissions) {
|
|
10
|
+
if (route.anonymous === true) continue;
|
|
11
|
+
validateAuditAction(
|
|
12
|
+
`API Route ${route.method} ${route.path}`,
|
|
13
|
+
route.auditAction,
|
|
14
|
+
auditActions,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
for (const task of module.tasks) {
|
|
18
|
+
validateAuditAction(
|
|
19
|
+
`定时任务 ${task.key}`,
|
|
20
|
+
task.auditAction,
|
|
21
|
+
auditActions,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateAuditAction(
|
|
28
|
+
label: string,
|
|
29
|
+
auditAction: AuditActionType | undefined,
|
|
30
|
+
auditActions: ReadonlySet<string>,
|
|
31
|
+
): void {
|
|
32
|
+
if (!auditAction?.trim()) {
|
|
33
|
+
throw new Error(`${label} 必须声明 Audit Action`);
|
|
34
|
+
}
|
|
35
|
+
if (!auditActions.has(auditAction)) {
|
|
36
|
+
throw new Error(`${label} 引用了未注册审计动作:${auditAction}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { MenuNode } from "@southwind-ai/shared";
|
|
2
|
+
import type { ModuleManifest } from "./manifest.js";
|
|
3
|
+
|
|
4
|
+
interface CapabilityGuard {
|
|
5
|
+
label: string;
|
|
6
|
+
permissionKey?: string;
|
|
7
|
+
featureKey?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function validateCapabilityGuardCompleteness(
|
|
11
|
+
modules: readonly ModuleManifest[],
|
|
12
|
+
): void {
|
|
13
|
+
for (const module of modules) {
|
|
14
|
+
const guards: CapabilityGuard[] = [
|
|
15
|
+
...flattenMenus(module.menus)
|
|
16
|
+
.filter(({ type }) => type !== "catalog")
|
|
17
|
+
.map((menu) => guard("菜单", menu)),
|
|
18
|
+
...module.adminRoutes.map((route) => guard("Admin Route", route)),
|
|
19
|
+
...module.apiPermissions
|
|
20
|
+
.filter(({ anonymous }) => anonymous !== true)
|
|
21
|
+
.map((route) =>
|
|
22
|
+
guard("API Route", {
|
|
23
|
+
...route,
|
|
24
|
+
key: `${route.method} ${route.path}`,
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
...module.tasks.map((task) => guard("定时任务", task)),
|
|
28
|
+
...module.tools.map((tool) => guard("Agent Tool", tool)),
|
|
29
|
+
...(module.aiOperations || []).map((operation) =>
|
|
30
|
+
guard("AI Operation", operation),
|
|
31
|
+
),
|
|
32
|
+
...module.providers.map((provider) => guard("Provider", provider)),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
for (const item of guards) {
|
|
36
|
+
if (!item.permissionKey?.trim() || !item.featureKey?.trim()) {
|
|
37
|
+
throw new Error(`${item.label} 必须同时声明 Permission 与 Feature`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateCapabilityKeyAlignment(
|
|
44
|
+
modules: readonly ModuleManifest[],
|
|
45
|
+
): void {
|
|
46
|
+
const featureKeys = new Set<string>(
|
|
47
|
+
modules.flatMap((module) => module.features.map(({ key }) => key)),
|
|
48
|
+
);
|
|
49
|
+
const permissionKeys = new Set(
|
|
50
|
+
modules.flatMap((module) => module.permissions.map(({ key }) => key)),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
for (const module of modules) {
|
|
54
|
+
for (const permission of module.permissions) {
|
|
55
|
+
const expectedFeatureKey = permissionFeatureKey(permission.key);
|
|
56
|
+
if (!featureKeys.has(expectedFeatureKey)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`权限 ${permission.key} 缺少同基础 Key Feature:${expectedFeatureKey}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const guards: CapabilityGuard[] = [
|
|
64
|
+
...flattenMenus(module.menus)
|
|
65
|
+
.filter(({ type }) => type !== "catalog")
|
|
66
|
+
.map((menu) => guard("菜单", menu)),
|
|
67
|
+
...module.adminRoutes.map((route) => guard("Admin Route", route)),
|
|
68
|
+
...module.apiPermissions
|
|
69
|
+
.filter(({ anonymous }) => anonymous !== true)
|
|
70
|
+
.map((route) =>
|
|
71
|
+
guard("API Route", {
|
|
72
|
+
...route,
|
|
73
|
+
key: `${route.method} ${route.path}`,
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
...module.tasks.map((task) => guard("定时任务", task)),
|
|
77
|
+
...module.tools.map((tool) => guard("Agent Tool", tool)),
|
|
78
|
+
...(module.aiOperations || []).map((operation) =>
|
|
79
|
+
guard("AI Operation", operation),
|
|
80
|
+
),
|
|
81
|
+
...module.providers.map((provider) => guard("Provider", provider)),
|
|
82
|
+
...(module.dataPolicies || []).flatMap((policy) =>
|
|
83
|
+
[
|
|
84
|
+
policy.readPermissionKey,
|
|
85
|
+
policy.revealPermissionKey,
|
|
86
|
+
policy.exportPermissionKey,
|
|
87
|
+
].map((permissionKey) =>
|
|
88
|
+
guard("数据治理策略", {
|
|
89
|
+
key: policy.key,
|
|
90
|
+
permissionKey,
|
|
91
|
+
featureKey: policy.featureKey,
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const item of guards) {
|
|
98
|
+
validateKeyAlignment(item.label, item, permissionKeys, featureKeys);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateKeyAlignment(
|
|
104
|
+
label: string,
|
|
105
|
+
input: { permissionKey?: string; featureKey?: string },
|
|
106
|
+
permissionKeys: ReadonlySet<string>,
|
|
107
|
+
featureKeys: ReadonlySet<string>,
|
|
108
|
+
): void {
|
|
109
|
+
if (
|
|
110
|
+
!input.permissionKey ||
|
|
111
|
+
!input.featureKey ||
|
|
112
|
+
!permissionKeys.has(input.permissionKey) ||
|
|
113
|
+
!featureKeys.has(input.featureKey)
|
|
114
|
+
) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const expectedFeatureKey = permissionFeatureKey(input.permissionKey);
|
|
118
|
+
if (input.featureKey !== expectedFeatureKey) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`${label} 的 Permission 基础 Key ${expectedFeatureKey} 与 Feature ${input.featureKey} 不一致`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function permissionFeatureKey(permissionKey: string): string {
|
|
126
|
+
const separator = permissionKey.lastIndexOf(".");
|
|
127
|
+
return separator > 0 ? permissionKey.slice(0, separator) : "";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function guard(
|
|
131
|
+
kind: string,
|
|
132
|
+
input: {
|
|
133
|
+
key?: string;
|
|
134
|
+
path?: string;
|
|
135
|
+
permissionKey?: string;
|
|
136
|
+
featureKey?: string;
|
|
137
|
+
},
|
|
138
|
+
): CapabilityGuard {
|
|
139
|
+
return {
|
|
140
|
+
label: `${kind} ${input.key || input.path || "unknown"}`,
|
|
141
|
+
permissionKey: input.permissionKey,
|
|
142
|
+
featureKey: input.featureKey,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function flattenMenus(menus: readonly MenuNode[]): MenuNode[] {
|
|
147
|
+
return menus.flatMap((menu) => [menu, ...flattenMenus(menu.children || [])]);
|
|
148
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
3
|
+
import {
|
|
4
|
+
dataPolicy,
|
|
5
|
+
fixture,
|
|
6
|
+
permission,
|
|
7
|
+
} from "./composition-fixtures.test-support.js";
|
|
8
|
+
import { validateModuleComposition } from "./composition.js";
|
|
9
|
+
import type { ModuleManifest } from "./manifest.js";
|
|
10
|
+
import userModule from "../user/module.js";
|
|
11
|
+
|
|
12
|
+
describe("Capability 基础 Key 组合门禁", () => {
|
|
13
|
+
test("Permission 缺少同基础 Key Feature 时 fail closed", () => {
|
|
14
|
+
const module = fixture("permission");
|
|
15
|
+
module.permissions.push(
|
|
16
|
+
permission("permission", "permission.orphan.read", "read"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
20
|
+
"权限 permission.orphan.read 缺少同基础 Key Feature:permission.orphan",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test.each([
|
|
25
|
+
[
|
|
26
|
+
"菜单 binding.page",
|
|
27
|
+
(module: ModuleManifest) => {
|
|
28
|
+
module.menus[0]!.featureKey = featureKey("binding.other");
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"Admin Route binding.page",
|
|
33
|
+
(module: ModuleManifest) => {
|
|
34
|
+
module.adminRoutes[0]!.featureKey = featureKey("binding.other");
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"API Route GET /api/binding",
|
|
39
|
+
(module: ModuleManifest) => {
|
|
40
|
+
module.apiPermissions[0]!.featureKey = featureKey("binding.other");
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"定时任务 binding.task",
|
|
45
|
+
(module: ModuleManifest) => {
|
|
46
|
+
module.tasks[0]!.featureKey = featureKey("binding.other");
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
[
|
|
50
|
+
"Agent Tool binding.tool",
|
|
51
|
+
(module: ModuleManifest) => {
|
|
52
|
+
module.tools[0]!.featureKey = featureKey("binding.other");
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
"AI Operation binding.ai-operation",
|
|
57
|
+
(module: ModuleManifest) => {
|
|
58
|
+
module.aiOperations = [aiOperation()];
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"Provider binding.provider",
|
|
63
|
+
(module: ModuleManifest) => {
|
|
64
|
+
module.providers[0]!.featureKey = featureKey("binding.other");
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"数据治理策略 binding.feature.fields",
|
|
69
|
+
(module: ModuleManifest) => {
|
|
70
|
+
addDataPolicy(module);
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
] as const)("%s 的 Guard 基础 Key 漂移时 fail closed", (label, mutate) => {
|
|
74
|
+
const module = fixture("binding");
|
|
75
|
+
module.features.push({
|
|
76
|
+
...module.features[0]!,
|
|
77
|
+
key: featureKey("binding.other"),
|
|
78
|
+
});
|
|
79
|
+
mutate(module);
|
|
80
|
+
|
|
81
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
82
|
+
`${label} 的 Permission 基础 Key binding.feature 与 Feature binding.other 不一致`,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("显式匿名 API 不要求 Permission 与 Feature 使用同一基础 Key", () => {
|
|
87
|
+
const module = fixture("public");
|
|
88
|
+
module.features.push({
|
|
89
|
+
...module.features[0]!,
|
|
90
|
+
key: featureKey("public.other"),
|
|
91
|
+
});
|
|
92
|
+
module.apiPermissions[0]!.anonymous = true;
|
|
93
|
+
module.apiPermissions[0]!.featureKey = featureKey("public.other");
|
|
94
|
+
|
|
95
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("示例用户模块通过完整 Capability Guard 门禁", () => {
|
|
99
|
+
expect(() => validateModuleComposition([userModule()])).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function aiOperation(): NonNullable<ModuleManifest["aiOperations"]>[number] {
|
|
104
|
+
return {
|
|
105
|
+
key: "binding.ai-operation",
|
|
106
|
+
kind: "agent",
|
|
107
|
+
featureKey: featureKey("binding.other"),
|
|
108
|
+
permissionKey: "binding.feature.read",
|
|
109
|
+
auditAction: "agent.tool.execute",
|
|
110
|
+
inputSchemaKey: "binding.tool.input.v1",
|
|
111
|
+
outputSchemaKey: "binding.tool.output.v1",
|
|
112
|
+
requiredCapabilities: ["text-generation"],
|
|
113
|
+
allowedToolKeys: [],
|
|
114
|
+
dataClassification: "internal",
|
|
115
|
+
execution: "interactive",
|
|
116
|
+
providerEgress: { mode: "forbidden" },
|
|
117
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
118
|
+
limits: {
|
|
119
|
+
maxDurationSeconds: 10,
|
|
120
|
+
maxInputBytes: 4_096,
|
|
121
|
+
maxOutputBytes: 4_096,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function addDataPolicy(module: ModuleManifest): void {
|
|
127
|
+
module.permissions.push(
|
|
128
|
+
{
|
|
129
|
+
...permission("binding", "binding.feature.reveal", "manage"),
|
|
130
|
+
action: "reveal",
|
|
131
|
+
accessRisk: "sensitive-disclosure",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
...permission("binding", "binding.feature.export", "manage"),
|
|
135
|
+
action: "export",
|
|
136
|
+
accessRisk: "governed-export",
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
module.dataPolicies = [
|
|
140
|
+
{
|
|
141
|
+
...dataPolicy(),
|
|
142
|
+
key: "binding.feature.fields",
|
|
143
|
+
resourceType: "binding.feature",
|
|
144
|
+
featureKey: featureKey("binding.other"),
|
|
145
|
+
readPermissionKey: "binding.feature.read",
|
|
146
|
+
revealPermissionKey: "binding.feature.reveal",
|
|
147
|
+
exportPermissionKey: "binding.feature.export",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
}
|
|
@@ -6,8 +6,8 @@ export function fixture(
|
|
|
6
6
|
dependencies: string[] = [],
|
|
7
7
|
): ModuleManifest {
|
|
8
8
|
const featureKey = `${name}.feature`;
|
|
9
|
-
const readPermission = `${
|
|
10
|
-
const managePermission = `${
|
|
9
|
+
const readPermission = `${featureKey}.read`;
|
|
10
|
+
const managePermission = `${featureKey}.manage`;
|
|
11
11
|
return {
|
|
12
12
|
name,
|
|
13
13
|
version: "0.1.0",
|
|
@@ -46,6 +46,7 @@ export function fixture(
|
|
|
46
46
|
path: `/api/${name}`,
|
|
47
47
|
permissionKey: readPermission,
|
|
48
48
|
featureKey,
|
|
49
|
+
auditAction: "data.search",
|
|
49
50
|
},
|
|
50
51
|
],
|
|
51
52
|
features: [
|
|
@@ -83,6 +84,7 @@ export function fixture(
|
|
|
83
84
|
description: name,
|
|
84
85
|
permissionKey: managePermission,
|
|
85
86
|
featureKey,
|
|
87
|
+
auditAction: "task.execute",
|
|
86
88
|
intervalSeconds: 60,
|
|
87
89
|
maxAttempts: 3,
|
|
88
90
|
retryDelaySeconds: 10,
|
|
@@ -131,9 +133,9 @@ export function dataPolicy(): NonNullable<
|
|
|
131
133
|
resourceType: "system.user",
|
|
132
134
|
version: "1",
|
|
133
135
|
featureKey: featureKey("system.feature"),
|
|
134
|
-
readPermissionKey: "system.read",
|
|
135
|
-
revealPermissionKey: "system.reveal",
|
|
136
|
-
exportPermissionKey: "system.export",
|
|
136
|
+
readPermissionKey: "system.feature.read",
|
|
137
|
+
revealPermissionKey: "system.feature.reveal",
|
|
138
|
+
exportPermissionKey: "system.feature.export",
|
|
137
139
|
display: {
|
|
138
140
|
read: "masked",
|
|
139
141
|
reveal: "plaintext",
|
|
@@ -35,11 +35,11 @@ describe("Module Composition 契约", () => {
|
|
|
35
35
|
const duplicate = fixture("duplicate");
|
|
36
36
|
duplicate.permissions[0] = {
|
|
37
37
|
...duplicate.permissions[0]!,
|
|
38
|
-
key: "system.read",
|
|
38
|
+
key: "system.feature.read",
|
|
39
39
|
};
|
|
40
40
|
expect(() =>
|
|
41
41
|
validateModuleComposition([fixture("system"), duplicate]),
|
|
42
|
-
).toThrow("权限 Key 重复:system.read");
|
|
42
|
+
).toThrow("权限 Key 重复:system.feature.read");
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
test("未知 Permission、Feature 与 Audit 引用会被拒绝", () => {
|
|
@@ -58,6 +58,71 @@ describe("Module Composition 契约", () => {
|
|
|
58
58
|
expect(() => validateModuleComposition([audit])).toThrow("未注册审计动作");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
test.each([
|
|
62
|
+
[
|
|
63
|
+
"菜单",
|
|
64
|
+
(module: ReturnType<typeof fixture>) => {
|
|
65
|
+
module.menus[0]!.path = undefined;
|
|
66
|
+
module.menus[0]!.featureKey = undefined;
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"Admin Route",
|
|
71
|
+
(module: ReturnType<typeof fixture>) => {
|
|
72
|
+
module.menus = [];
|
|
73
|
+
module.adminRoutes[0]!.featureKey = undefined;
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"API Route",
|
|
78
|
+
(module: ReturnType<typeof fixture>) => {
|
|
79
|
+
module.apiPermissions[0]!.featureKey = undefined;
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"定时任务",
|
|
84
|
+
(module: ReturnType<typeof fixture>) => {
|
|
85
|
+
Reflect.deleteProperty(module.tasks[0]!, "featureKey");
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
"Agent Tool",
|
|
90
|
+
(module: ReturnType<typeof fixture>) => {
|
|
91
|
+
Reflect.deleteProperty(module.tools[0]!, "featureKey");
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"Provider",
|
|
96
|
+
(module: ReturnType<typeof fixture>) => {
|
|
97
|
+
module.providers[0]!.featureKey = undefined;
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
] as const)("%s 缺少 Feature Guard 时 fail closed", (_, removeFeature) => {
|
|
101
|
+
const module = fixture("guard");
|
|
102
|
+
removeFeature(module);
|
|
103
|
+
|
|
104
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
105
|
+
"必须同时声明 Permission 与 Feature",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("业务入口缺少 Permission Guard 时 fail closed", () => {
|
|
110
|
+
const module = fixture("guard");
|
|
111
|
+
Reflect.deleteProperty(module.apiPermissions[0]!, "permissionKey");
|
|
112
|
+
|
|
113
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
114
|
+
"必须同时声明 Permission 与 Feature",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("显式匿名 API 可以不声明 Feature Guard", () => {
|
|
119
|
+
const module = fixture("public");
|
|
120
|
+
module.apiPermissions[0]!.anonymous = true;
|
|
121
|
+
module.apiPermissions[0]!.featureKey = undefined;
|
|
122
|
+
|
|
123
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
61
126
|
test("业务模块可注册带展示信息的审计动作目录", () => {
|
|
62
127
|
const module = fixture("ticket");
|
|
63
128
|
module.auditActions.push({
|
|
@@ -133,7 +198,7 @@ describe("Module Composition 契约", () => {
|
|
|
133
198
|
|
|
134
199
|
test("菜单 Guard 必须与 Admin Route 一致", () => {
|
|
135
200
|
const module = fixture("system");
|
|
136
|
-
module.adminRoutes[0]!.permissionKey = "system.manage";
|
|
201
|
+
module.adminRoutes[0]!.permissionKey = "system.feature.manage";
|
|
137
202
|
expect(() => validateModuleComposition([module])).toThrow(
|
|
138
203
|
"Admin Route Guard 不一致",
|
|
139
204
|
);
|
|
@@ -144,12 +209,12 @@ describe("Module Composition 契约", () => {
|
|
|
144
209
|
module.adminRoutes[0] = {
|
|
145
210
|
...module.adminRoutes[0]!,
|
|
146
211
|
directAccess: "admin-admission",
|
|
147
|
-
permissionKey: "system.manage",
|
|
212
|
+
permissionKey: "system.feature.manage",
|
|
148
213
|
};
|
|
149
214
|
|
|
150
215
|
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
151
216
|
expect(module.menus[0]).toMatchObject({
|
|
152
|
-
permissionKey: "system.read",
|
|
217
|
+
permissionKey: "system.feature.read",
|
|
153
218
|
featureKey: "system.feature",
|
|
154
219
|
});
|
|
155
220
|
});
|
|
@@ -197,12 +262,12 @@ describe("Module Composition 契约", () => {
|
|
|
197
262
|
test("字段治理策略校验唯一资源、权限风险和已注册字段", () => {
|
|
198
263
|
const module = fixture("system");
|
|
199
264
|
module.permissions.push({
|
|
200
|
-
...permission("system", "system.reveal", "manage"),
|
|
265
|
+
...permission("system", "system.feature.reveal", "manage"),
|
|
201
266
|
action: "reveal",
|
|
202
267
|
accessRisk: "sensitive-disclosure",
|
|
203
268
|
});
|
|
204
269
|
module.permissions.push({
|
|
205
|
-
...permission("system", "system.export", "manage"),
|
|
270
|
+
...permission("system", "system.feature.export", "manage"),
|
|
206
271
|
action: "export",
|
|
207
272
|
accessRisk: "governed-export",
|
|
208
273
|
});
|
|
@@ -220,8 +285,8 @@ describe("Module Composition 契约", () => {
|
|
|
220
285
|
module.dataPolicies = [dataPolicy()];
|
|
221
286
|
expect(() => validateModuleComposition([module])).toThrow("未注册权限");
|
|
222
287
|
|
|
223
|
-
module.dataPolicies[0]!.revealPermissionKey = "system.manage";
|
|
224
|
-
module.dataPolicies[0]!.exportPermissionKey = "system.manage";
|
|
288
|
+
module.dataPolicies[0]!.revealPermissionKey = "system.feature.manage";
|
|
289
|
+
module.dataPolicies[0]!.exportPermissionKey = "system.feature.manage";
|
|
225
290
|
expect(() => validateModuleComposition([module])).toThrow(
|
|
226
291
|
"明文权限必须标记 sensitive-disclosure",
|
|
227
292
|
);
|
|
@@ -4,6 +4,11 @@ import type {
|
|
|
4
4
|
ModuleManifest,
|
|
5
5
|
} from "./manifest.js";
|
|
6
6
|
import { validateAiOperations } from "./ai-operation-composition.js";
|
|
7
|
+
import { validateCapabilityAuditActions } from "./capability-audit-validation.js";
|
|
8
|
+
import {
|
|
9
|
+
validateCapabilityGuardCompleteness,
|
|
10
|
+
validateCapabilityKeyAlignment,
|
|
11
|
+
} from "./capability-guard-validation.js";
|
|
7
12
|
import { validateImplementationBindings } from "./implementation-bindings.js";
|
|
8
13
|
import { composeModuleMenus } from "./menu-composition.js";
|
|
9
14
|
import { validateCapabilityCatalogs } from "./catalog-validation.js";
|
|
@@ -27,6 +32,7 @@ export function validateModuleComposition(
|
|
|
27
32
|
modules.map(({ name }) => name),
|
|
28
33
|
);
|
|
29
34
|
const moduleOrder = validateDependencies(modules, moduleNames);
|
|
35
|
+
validateCapabilityGuardCompleteness(modules);
|
|
30
36
|
const permissions = unique(
|
|
31
37
|
"权限",
|
|
32
38
|
modules.flatMap((module) => module.permissions.map(({ key }) => key)),
|
|
@@ -35,9 +41,11 @@ export function validateModuleComposition(
|
|
|
35
41
|
"Feature",
|
|
36
42
|
modules.flatMap((module) => module.features.map(({ key }) => key)),
|
|
37
43
|
);
|
|
44
|
+
validateCapabilityKeyAlignment(modules);
|
|
38
45
|
const catalogs = validateCapabilityCatalogs(modules);
|
|
39
46
|
const jsonSchemas = validateToolContracts(modules);
|
|
40
47
|
const audits = new Set(catalogs.auditActions.map(({ key }) => key));
|
|
48
|
+
validateCapabilityAuditActions(modules, audits);
|
|
41
49
|
const menus = composeModuleMenus(modules).flatMap((menu) =>
|
|
42
50
|
flattenMenus([menu]),
|
|
43
51
|
);
|