@southwind-ai/modules 0.1.1 → 0.1.3

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/index.ts CHANGED
@@ -6,3 +6,5 @@ export * from "./src/registry.js";
6
6
  export * from "./src/system-modules.js";
7
7
  export * from "./src/system-module-catalog.js";
8
8
  export * from "./src/system-api-permissions.js";
9
+ export * from "./src/tool-composition.js";
10
+ export * from "./src/ai-network-policy.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,7 @@
12
12
  "!src/**/*.test.ts"
13
13
  ],
14
14
  "dependencies": {
15
- "@southwind-ai/shared": "0.1.1"
15
+ "@southwind-ai/shared": "0.1.2"
16
16
  },
17
17
  "exports": {
18
18
  ".": "./index.ts"
@@ -0,0 +1,50 @@
1
+ import type {
2
+ AiProviderEgressPolicy,
3
+ AiToolNetworkPolicy,
4
+ ModuleAiOperationDefinition,
5
+ } from "./manifest.js";
6
+
7
+ const providerForbidden: AiProviderEgressPolicy = Object.freeze({
8
+ mode: "forbidden",
9
+ });
10
+ const toolNetworkForbidden: AiToolNetworkPolicy = Object.freeze({
11
+ mode: "forbidden",
12
+ });
13
+
14
+ export function resolveProviderEgress(
15
+ operation: ModuleAiOperationDefinition,
16
+ ): AiProviderEgressPolicy {
17
+ return operation.providerEgress ?? providerForbidden;
18
+ }
19
+
20
+ export function resolveToolNetworkPolicy(
21
+ operation: ModuleAiOperationDefinition,
22
+ ): AiToolNetworkPolicy {
23
+ return operation.toolNetworkPolicy ?? toolNetworkForbidden;
24
+ }
25
+
26
+ export function validateAiNetworkPolicy(
27
+ operation: ModuleAiOperationDefinition,
28
+ ): void {
29
+ const provider = resolveProviderEgress(operation);
30
+ if (provider.mode === "configured-endpoint") {
31
+ if (!provider.deploymentKey.trim()) {
32
+ throw new Error(`${operation.key} Provider Deployment Key 不能为空`);
33
+ }
34
+ } else if (provider.mode !== "forbidden") {
35
+ throw new Error(`${operation.key} Provider 出网策略无效`);
36
+ }
37
+
38
+ const tool = resolveToolNetworkPolicy(operation);
39
+ if (tool.mode !== "forbidden") {
40
+ throw new Error(`${operation.key} Tool 网络策略无效`);
41
+ }
42
+ if (
43
+ operation.publicNetworkAccess === "allowed" &&
44
+ operation.providerEgress === undefined
45
+ ) {
46
+ throw new Error(
47
+ `${operation.key} 旧 publicNetworkAccess=allowed 不能安全映射,请声明 providerEgress`,
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,275 @@
1
+ import type {
2
+ ModuleAiOperationDefinition,
3
+ ModuleManifest,
4
+ } from "./manifest.js";
5
+ import { validateAiNetworkPolicy } from "./ai-network-policy.js";
6
+
7
+ interface AiOperationCatalogs {
8
+ permissions: ReadonlySet<string>;
9
+ features: ReadonlySet<string>;
10
+ audits: ReadonlySet<string>;
11
+ tools: ReadonlySet<string>;
12
+ schemas: ReadonlySet<string>;
13
+ }
14
+
15
+ export function validateAiOperations(
16
+ modules: readonly ModuleManifest[],
17
+ catalogs: AiOperationCatalogs,
18
+ ): ModuleAiOperationDefinition[] {
19
+ const operations = modules.flatMap((module) => module.aiOperations || []);
20
+ uniqueValues(
21
+ "AI Operation",
22
+ operations.map(({ key }) => key),
23
+ );
24
+ for (const module of modules) {
25
+ for (const operation of module.aiOperations || []) {
26
+ validateOperation(module.name, operation, catalogs);
27
+ }
28
+ }
29
+ return operations;
30
+ }
31
+
32
+ export function validateAiOperationImplementations(
33
+ declarations: readonly ModuleAiOperationDefinition[],
34
+ implementations: readonly string[] | undefined,
35
+ ): void {
36
+ if (!implementations) return;
37
+ const declared = new Set(declarations.map(({ key }) => key));
38
+ const registered = uniqueValues("AI Operation Handler", implementations);
39
+ for (const key of declared) {
40
+ if (!registered.has(key)) {
41
+ throw new Error(`AI Operation ${key} 缺少 Handler`);
42
+ }
43
+ }
44
+ for (const key of registered) {
45
+ if (!declared.has(key)) {
46
+ throw new Error(`AI Operation Handler ${key} 缺少 Manifest 声明`);
47
+ }
48
+ }
49
+ }
50
+
51
+ function validateOperation(
52
+ moduleName: string,
53
+ operation: ModuleAiOperationDefinition,
54
+ catalogs: AiOperationCatalogs,
55
+ ): void {
56
+ const strings = [
57
+ operation.key,
58
+ operation.featureKey,
59
+ operation.permissionKey,
60
+ operation.auditAction,
61
+ operation.inputSchemaKey,
62
+ operation.outputSchemaKey,
63
+ ];
64
+ if (strings.some((value) => !value.trim())) {
65
+ throw new Error("AI Operation Key 与引用字段不能为空");
66
+ }
67
+ if (!operation.key.startsWith(`${moduleName}.`)) {
68
+ throw new Error(
69
+ `AI Operation ${operation.key} 必须使用模块命名空间 ${moduleName}`,
70
+ );
71
+ }
72
+ requiredReference(
73
+ operation.key,
74
+ "权限",
75
+ operation.permissionKey,
76
+ catalogs.permissions,
77
+ );
78
+ requiredReference(
79
+ operation.key,
80
+ "Feature",
81
+ operation.featureKey,
82
+ catalogs.features,
83
+ );
84
+ requiredReference(
85
+ operation.key,
86
+ "审计动作",
87
+ operation.auditAction,
88
+ catalogs.audits,
89
+ );
90
+ requiredReference(
91
+ operation.key,
92
+ " JSON Schema",
93
+ operation.inputSchemaKey,
94
+ catalogs.schemas,
95
+ );
96
+ requiredReference(
97
+ operation.key,
98
+ " JSON Schema",
99
+ operation.outputSchemaKey,
100
+ catalogs.schemas,
101
+ );
102
+ validateCapabilities(operation);
103
+ validateTools(operation, catalogs.tools);
104
+ validateAiNetworkPolicy(operation);
105
+ validateProviderDataPolicy(operation);
106
+ validateLimits(operation);
107
+ }
108
+
109
+ const providerDataFields = new Set([
110
+ "instructions",
111
+ "user-prompt",
112
+ "tool-definitions",
113
+ "provider-output",
114
+ "tool-results",
115
+ ]);
116
+
117
+ export function validateProviderDataPolicy(
118
+ operation: ModuleAiOperationDefinition,
119
+ ): void {
120
+ const policy = operation.providerDataPolicy;
121
+ if (operation.providerEgress?.mode !== "configured-endpoint") {
122
+ if (policy) {
123
+ throw new Error(
124
+ `${operation.key} 禁止 Provider 出网时不能声明字段发送策略`,
125
+ );
126
+ }
127
+ return;
128
+ }
129
+ if (!policy?.inputFields.length || !policy.envelopeFields.length) {
130
+ throw new Error(`${operation.key} Provider 字段发送 allowlist 缺失`);
131
+ }
132
+ validateInputFields(operation.key, policy.inputFields);
133
+ const fields = new Set<string>();
134
+ for (const rule of policy.envelopeFields) {
135
+ if (!providerDataFields.has(rule.field)) {
136
+ throw new Error(
137
+ `${operation.key} Provider 字段发送 allowlist 含未知字段`,
138
+ );
139
+ }
140
+ if (fields.has(rule.field)) {
141
+ throw new Error(
142
+ `${operation.key} Provider 字段发送规则重复:${rule.field}`,
143
+ );
144
+ }
145
+ fields.add(rule.field);
146
+ if (!["public", "internal", "sensitive"].includes(rule.classification)) {
147
+ throw new Error(`${operation.key} Provider 字段分类无效:${rule.field}`);
148
+ }
149
+ if (!["reject", "redact"].includes(rule.dlpAction)) {
150
+ throw new Error(`${operation.key} Provider DLP 动作无效:${rule.field}`);
151
+ }
152
+ }
153
+ for (const field of ["instructions", "user-prompt"]) {
154
+ if (!fields.has(field)) {
155
+ throw new Error(
156
+ `${operation.key} Provider 字段发送 allowlist 缺少 ${field}`,
157
+ );
158
+ }
159
+ }
160
+ if (operation.allowedToolKeys.length) {
161
+ for (const field of [
162
+ "tool-definitions",
163
+ "provider-output",
164
+ "tool-results",
165
+ ]) {
166
+ if (!fields.has(field)) {
167
+ throw new Error(
168
+ `${operation.key} 使用 Tool 时 Provider 字段发送 allowlist 缺少 ${field}`,
169
+ );
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ function validateInputFields(
176
+ operationKey: string,
177
+ fields: NonNullable<
178
+ ModuleAiOperationDefinition["providerDataPolicy"]
179
+ >["inputFields"],
180
+ ): void {
181
+ const pointers = new Set<string>();
182
+ for (const rule of fields) {
183
+ const segments = rule.jsonPointer
184
+ .slice(1)
185
+ .split("/")
186
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
187
+ if (
188
+ !rule.jsonPointer.startsWith("/") ||
189
+ rule.jsonPointer === "/" ||
190
+ rule.jsonPointer.endsWith("/") ||
191
+ rule.jsonPointer.includes("//") ||
192
+ /~(?:[^01]|$)/u.test(rule.jsonPointer) ||
193
+ segments.some((segment) =>
194
+ ["__proto__", "prototype", "constructor"].includes(segment),
195
+ )
196
+ ) {
197
+ throw new Error(`${operationKey} Provider 输入字段 JSON Pointer 无效`);
198
+ }
199
+ if (
200
+ [...pointers].some(
201
+ (pointer) =>
202
+ pointer.startsWith(`${rule.jsonPointer}/`) ||
203
+ rule.jsonPointer.startsWith(`${pointer}/`),
204
+ )
205
+ ) {
206
+ throw new Error(`${operationKey} Provider 输入字段规则不能父子重叠`);
207
+ }
208
+ if (pointers.has(rule.jsonPointer)) {
209
+ throw new Error(
210
+ `${operationKey} Provider 输入字段规则重复:${rule.jsonPointer}`,
211
+ );
212
+ }
213
+ pointers.add(rule.jsonPointer);
214
+ if (!["public", "internal", "sensitive"].includes(rule.classification)) {
215
+ throw new Error(`${operationKey} Provider 输入字段分类无效`);
216
+ }
217
+ if (!["reject", "redact"].includes(rule.dlpAction)) {
218
+ throw new Error(`${operationKey} Provider 输入字段 DLP 动作无效`);
219
+ }
220
+ }
221
+ }
222
+
223
+ function validateCapabilities(operation: ModuleAiOperationDefinition): void {
224
+ if (!operation.requiredCapabilities.length) {
225
+ throw new Error(`${operation.key} 至少需要一个 capability`);
226
+ }
227
+ validateNonEmptyValues(
228
+ `${operation.key} capability`,
229
+ operation.requiredCapabilities,
230
+ );
231
+ }
232
+
233
+ function validateTools(
234
+ operation: ModuleAiOperationDefinition,
235
+ tools: ReadonlySet<string>,
236
+ ): void {
237
+ validateNonEmptyValues(`${operation.key} Tool`, operation.allowedToolKeys);
238
+ for (const toolKey of operation.allowedToolKeys) {
239
+ requiredReference(operation.key, "Agent Tool", toolKey, tools);
240
+ }
241
+ }
242
+
243
+ function validateLimits(operation: ModuleAiOperationDefinition): void {
244
+ const values = Object.values(operation.limits);
245
+ if (values.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
246
+ throw new Error(`${operation.key} 运行与输入输出上限必须是正整数`);
247
+ }
248
+ }
249
+
250
+ function validateNonEmptyValues(label: string, values: readonly string[]) {
251
+ if (values.some((value) => !value.trim())) {
252
+ throw new Error(`${label} 不能为空`);
253
+ }
254
+ uniqueValues(label, values);
255
+ }
256
+
257
+ function requiredReference(
258
+ operationKey: string,
259
+ label: string,
260
+ key: string,
261
+ catalog: ReadonlySet<string>,
262
+ ): void {
263
+ if (!catalog.has(key)) {
264
+ throw new Error(`${operationKey} 引用了未注册${label}:${key}`);
265
+ }
266
+ }
267
+
268
+ function uniqueValues(label: string, values: readonly string[]): Set<string> {
269
+ const result = new Set<string>();
270
+ for (const value of values) {
271
+ if (result.has(value)) throw new Error(`${label} Key 重复:${value}`);
272
+ result.add(value);
273
+ }
274
+ return result;
275
+ }
@@ -2,6 +2,9 @@ import {
2
2
  AUDIT_ACTION_DEFINITIONS,
3
3
  isAuditActionDefinition,
4
4
  LICENSE_CATALOG,
5
+ normalizeManifestBundles,
6
+ validateAndOrderBundles,
7
+ validateLicenseCatalog,
5
8
  type AuditActionDefinition,
6
9
  type FeatureFlagDefinition,
7
10
  type LicenseCatalogDefinition,
@@ -15,15 +18,29 @@ export interface PlatformCapabilityCatalogs {
15
18
 
16
19
  export function validateCapabilityCatalogs(
17
20
  modules: readonly ModuleManifest[],
21
+ licenseCatalog: readonly LicenseCatalogDefinition[] = LICENSE_CATALOG,
18
22
  ): PlatformCapabilityCatalogs {
19
23
  const auditActions = composeAuditActionCatalog(modules);
20
- validateFeatureCatalog(modules, LICENSE_CATALOG);
24
+ validateLicenseCatalog(licenseCatalog);
25
+ validateFeatureCatalog(modules, licenseCatalog);
26
+ validateMigrationBundleCatalog(modules);
21
27
  return {
22
28
  auditActions,
23
- licenseVersions: LICENSE_CATALOG,
29
+ licenseVersions: licenseCatalog,
24
30
  };
25
31
  }
26
32
 
33
+ /**
34
+ * 启动期结构校验显式声明 migrationBundle 的模块;无 bundle 的旧式模块
35
+ * 由 drizzle 规范迁移管理,不参与模块级 Bundle 校验。
36
+ */
37
+ export function validateMigrationBundleCatalog(
38
+ modules: readonly ModuleManifest[],
39
+ ): void {
40
+ const bundled = modules.filter((module) => module.migrationBundle);
41
+ validateAndOrderBundles(normalizeManifestBundles(bundled));
42
+ }
43
+
27
44
  export function composeAuditActionCatalog(
28
45
  modules: readonly ModuleManifest[],
29
46
  ): AuditActionDefinition[] {
@@ -3,8 +3,11 @@ import type {
3
3
  ModuleImplementationBindings,
4
4
  ModuleManifest,
5
5
  } from "./manifest.js";
6
+ import { validateAiOperations } from "./ai-operation-composition.js";
7
+ import { validateImplementationBindings } from "./implementation-bindings.js";
6
8
  import { composeModuleMenus } from "./menu-composition.js";
7
9
  import { validateCapabilityCatalogs } from "./catalog-validation.js";
10
+ import { validateToolContracts } from "./tool-composition.js";
8
11
 
9
12
  export interface ValidatedModuleComposition {
10
13
  modules: readonly ModuleManifest[];
@@ -33,6 +36,7 @@ export function validateModuleComposition(
33
36
  modules.flatMap((module) => module.features.map(({ key }) => key)),
34
37
  );
35
38
  const catalogs = validateCapabilityCatalogs(modules);
39
+ const jsonSchemas = validateToolContracts(modules);
36
40
  const audits = new Set(catalogs.auditActions.map(({ key }) => key));
37
41
  const menus = composeModuleMenus(modules).flatMap((menu) =>
38
42
  flattenMenus([menu]),
@@ -58,10 +62,17 @@ export function validateModuleComposition(
58
62
  tasks.map(({ key }) => key),
59
63
  );
60
64
  const tools = modules.flatMap((module) => module.tools);
61
- unique(
65
+ const toolKeys = unique(
62
66
  "Agent Tool",
63
67
  tools.map(({ key }) => key),
64
68
  );
69
+ const aiOperations = validateAiOperations(modules, {
70
+ permissions,
71
+ features,
72
+ audits,
73
+ tools: toolKeys,
74
+ schemas: new Set(jsonSchemas.keys()),
75
+ });
65
76
  const providers = modules.flatMap((module) => module.providers);
66
77
  unique(
67
78
  "Provider",
@@ -92,30 +103,16 @@ export function validateModuleComposition(
92
103
  }
93
104
  validateMenuRoutes(menus, adminRoutes);
94
105
  validateSchemaMigrations(schemas);
95
- validateImplementations(
96
- "Admin Route",
97
- adminRoutes.map(({ key }) => key),
98
- implementations.adminRoutes,
99
- );
100
- validateImplementations(
101
- "API Route",
102
- apiRoutes.map(apiRouteKey),
103
- implementations.apiRoutes,
104
- );
105
- validateImplementations(
106
- "定时任务",
107
- tasks.map(({ key }) => key),
108
- implementations.tasks,
109
- );
110
- validateImplementations(
111
- "Agent Tool",
112
- tools.map(({ key }) => key),
113
- implementations.tools,
114
- );
115
- validateImplementations(
116
- "Provider",
117
- providers.map(({ key }) => key),
118
- implementations.providers,
106
+ validateImplementationBindings(
107
+ {
108
+ adminRoutes: adminRoutes.map(({ key }) => key),
109
+ apiRoutes: apiRoutes.map(apiRouteKey),
110
+ tasks: tasks.map(({ key }) => key),
111
+ tools: tools.map(({ key }) => key),
112
+ providers: providers.map(({ key }) => key),
113
+ },
114
+ aiOperations,
115
+ implementations,
119
116
  );
120
117
 
121
118
  return { modules, moduleOrder };
@@ -318,18 +315,6 @@ function validateSchemaMigrations(
318
315
  }
319
316
  }
320
317
 
321
- function validateImplementations(
322
- label: string,
323
- declarations: readonly string[],
324
- implementations: readonly string[] | undefined,
325
- ): void {
326
- if (!implementations) return;
327
- const registered = unique(`${label} Handler`, implementations);
328
- for (const key of declarations) {
329
- if (!registered.has(key)) throw new Error(`${label} ${key} 缺少 Handler`);
330
- }
331
- }
332
-
333
318
  function unique(label: string, values: readonly string[]): Set<string> {
334
319
  const result = new Set<string>();
335
320
  for (const value of values) {
@@ -0,0 +1,70 @@
1
+ import { validateAiOperationImplementations } from "./ai-operation-composition.js";
2
+ import type {
3
+ ModuleAiOperationDefinition,
4
+ ModuleImplementationBindings,
5
+ } from "./manifest.js";
6
+
7
+ interface ModuleDeclarationKeys {
8
+ adminRoutes: readonly string[];
9
+ apiRoutes: readonly string[];
10
+ tasks: readonly string[];
11
+ tools: readonly string[];
12
+ providers: readonly string[];
13
+ }
14
+
15
+ export function validateImplementationBindings(
16
+ declarations: ModuleDeclarationKeys,
17
+ aiOperations: readonly ModuleAiOperationDefinition[],
18
+ implementations: ModuleImplementationBindings,
19
+ ): void {
20
+ validateImplementations(
21
+ "Admin Route",
22
+ declarations.adminRoutes,
23
+ implementations.adminRoutes,
24
+ );
25
+ validateImplementations(
26
+ "API Route",
27
+ declarations.apiRoutes,
28
+ implementations.apiRoutes,
29
+ );
30
+ validateImplementations(
31
+ "定时任务",
32
+ declarations.tasks,
33
+ implementations.tasks,
34
+ );
35
+ validateImplementations(
36
+ "Agent Tool",
37
+ declarations.tools,
38
+ implementations.tools,
39
+ );
40
+ validateAiOperationImplementations(
41
+ aiOperations,
42
+ implementations.aiOperations,
43
+ );
44
+ validateImplementations(
45
+ "Provider",
46
+ declarations.providers,
47
+ implementations.providers,
48
+ );
49
+ }
50
+
51
+ function validateImplementations(
52
+ label: string,
53
+ declarations: readonly string[],
54
+ implementations: readonly string[] | undefined,
55
+ ): void {
56
+ if (!implementations) return;
57
+ const registered = unique(`${label} Handler`, implementations);
58
+ for (const key of declarations) {
59
+ if (!registered.has(key)) throw new Error(`${label} ${key} 缺少 Handler`);
60
+ }
61
+ }
62
+
63
+ function unique(label: string, values: readonly string[]): Set<string> {
64
+ const result = new Set<string>();
65
+ for (const value of values) {
66
+ if (result.has(value)) throw new Error(`${label} Key 重复:${value}`);
67
+ result.add(value);
68
+ }
69
+ return result;
70
+ }
package/src/manifest.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  AuditActionType,
5
5
  DataGovernancePolicyDefinition,
6
6
  FeatureFlagDefinition,
7
+ JsonObject,
7
8
  MigrationBundle,
8
9
  MigrationDefinition,
9
10
  MenuNode,
@@ -11,15 +12,35 @@ import type {
11
12
  SearchProviderManifestDefinition,
12
13
  } from "@southwind-ai/shared";
13
14
 
14
- export type { MigrationDefinition, MigrationDirection } from "@southwind-ai/shared";
15
+ export type {
16
+ MigrationDefinition,
17
+ MigrationDirection,
18
+ } from "@southwind-ai/shared";
15
19
 
16
20
  export interface ModuleToolDefinition {
17
21
  key: string;
18
22
  label: string;
19
23
  description: string;
24
+ inputSchemaKey: string;
25
+ outputSchemaKey: string;
26
+ operation: "query" | "command" | "background";
20
27
  permissionKey: string;
21
- featureKey?: string;
28
+ featureKey: string;
22
29
  auditAction: AuditActionType;
30
+ scopeMode: "platform" | "request";
31
+ dataClassification: "public" | "internal" | "sensitive";
32
+ idempotency: "none" | "supported" | "required";
33
+ approval: "none" | "required";
34
+ limits: {
35
+ maxDurationSeconds: number;
36
+ maxInputBytes: number;
37
+ maxOutputBytes: number;
38
+ };
39
+ }
40
+
41
+ export interface ModuleJsonSchemaDefinition {
42
+ key: string;
43
+ schema: JsonObject;
23
44
  }
24
45
 
25
46
  export type AdminRouteSurface = "sidebar" | "account" | "hidden";
@@ -59,6 +80,75 @@ export interface ModuleScheduledTaskDefinition {
59
80
  retryDelaySeconds: number;
60
81
  }
61
82
 
83
+ export type AiOperationKind = "agent" | "document" | "speech";
84
+ export type AiOperationDataClassification = "public" | "internal" | "sensitive";
85
+ export type AiOperationExecution =
86
+ | "interactive"
87
+ | "streaming"
88
+ | "background"
89
+ | "live";
90
+ export type AiOperationPublicNetworkAccess = "forbidden" | "allowed";
91
+ export type AiProviderEgressPolicy =
92
+ | { mode: "forbidden" }
93
+ | {
94
+ mode: "configured-endpoint";
95
+ deploymentKey: string;
96
+ };
97
+ export interface AiToolNetworkPolicy {
98
+ mode: "forbidden";
99
+ }
100
+
101
+ export type AiProviderDataField =
102
+ | "instructions"
103
+ | "user-prompt"
104
+ | "tool-definitions"
105
+ | "provider-output"
106
+ | "tool-results";
107
+ export type AiProviderDlpAction = "reject" | "redact";
108
+
109
+ export interface AiProviderFieldPolicy {
110
+ field: AiProviderDataField;
111
+ classification: AiOperationDataClassification;
112
+ dlpAction: AiProviderDlpAction;
113
+ }
114
+
115
+ export interface AiProviderInputFieldPolicy {
116
+ jsonPointer: string;
117
+ classification: AiOperationDataClassification;
118
+ dlpAction: AiProviderDlpAction;
119
+ }
120
+
121
+ export interface AiProviderDataPolicy {
122
+ inputFields: readonly AiProviderInputFieldPolicy[];
123
+ envelopeFields: readonly AiProviderFieldPolicy[];
124
+ }
125
+
126
+ export interface AiOperationLimits {
127
+ maxDurationSeconds: number;
128
+ maxInputBytes: number;
129
+ maxOutputBytes: number;
130
+ }
131
+
132
+ export interface ModuleAiOperationDefinition {
133
+ key: string;
134
+ kind: AiOperationKind;
135
+ featureKey: string;
136
+ permissionKey: string;
137
+ auditAction: AuditActionType;
138
+ inputSchemaKey: string;
139
+ outputSchemaKey: string;
140
+ requiredCapabilities: readonly string[];
141
+ allowedToolKeys: readonly string[];
142
+ dataClassification: AiOperationDataClassification;
143
+ execution: AiOperationExecution;
144
+ /** @deprecated 使用 providerEgress 与 toolNetworkPolicy 拆分两类网络边界。 */
145
+ publicNetworkAccess?: AiOperationPublicNetworkAccess;
146
+ providerEgress?: AiProviderEgressPolicy;
147
+ providerDataPolicy?: AiProviderDataPolicy;
148
+ toolNetworkPolicy?: AiToolNetworkPolicy;
149
+ limits: AiOperationLimits;
150
+ }
151
+
62
152
  export interface ModuleManifest {
63
153
  name: string;
64
154
  version: string;
@@ -73,8 +163,10 @@ export interface ModuleManifest {
73
163
  migrations: MigrationDefinition[];
74
164
  migrationBundle?: MigrationBundle;
75
165
  schemaExports: ModuleSchemaExportDefinition[];
166
+ jsonSchemas?: ModuleJsonSchemaDefinition[];
76
167
  tasks: ModuleScheduledTaskDefinition[];
77
168
  tools: ModuleToolDefinition[];
169
+ aiOperations?: ModuleAiOperationDefinition[];
78
170
  providers: Array<ModuleProviderDefinition | SearchProviderManifestDefinition>;
79
171
  dataPolicies?: DataGovernancePolicyDefinition[];
80
172
  auditActions: AuditActionRegistration[];
@@ -88,5 +180,6 @@ export interface ModuleImplementationBindings {
88
180
  apiRoutes?: readonly string[];
89
181
  tasks?: readonly string[];
90
182
  tools?: readonly string[];
183
+ aiOperations?: readonly string[];
91
184
  providers?: readonly string[];
92
185
  }
@@ -1,15 +1,66 @@
1
- import type { ModuleManifest } from "./manifest.js";
1
+ import type { ModuleJsonSchemaDefinition, ModuleManifest } from "./manifest.js";
2
+
3
+ export const systemAgentToolSchemas: ModuleJsonSchemaDefinition[] = [
4
+ {
5
+ key: "system.pagination.input.v1",
6
+ schema: {
7
+ type: "object",
8
+ properties: {
9
+ page: { type: "integer", minimum: 1, default: 1 },
10
+ pageSize: {
11
+ type: "integer",
12
+ minimum: 1,
13
+ maximum: 50,
14
+ default: 10,
15
+ },
16
+ },
17
+ additionalProperties: false,
18
+ },
19
+ },
20
+ {
21
+ key: "system.page.output.v1",
22
+ schema: {
23
+ type: "object",
24
+ properties: {
25
+ items: { type: "array", items: { type: "object" } },
26
+ total: { type: "integer", minimum: 0 },
27
+ page: { type: "integer", minimum: 1 },
28
+ pageSize: { type: "integer", minimum: 1 },
29
+ },
30
+ required: ["items", "total", "page", "pageSize"],
31
+ additionalProperties: false,
32
+ },
33
+ },
34
+ ];
35
+
36
+ const queryContract = {
37
+ inputSchemaKey: "system.pagination.input.v1",
38
+ outputSchemaKey: "system.page.output.v1",
39
+ operation: "query" as const,
40
+ scopeMode: "platform" as const,
41
+ dataClassification: "internal" as const,
42
+ idempotency: "none" as const,
43
+ approval: "none" as const,
44
+ limits: {
45
+ maxDurationSeconds: 10,
46
+ maxInputBytes: 4_096,
47
+ maxOutputBytes: 1_048_576,
48
+ },
49
+ };
2
50
 
3
51
  export const systemAgentTools: ModuleManifest["tools"] = [
4
52
  {
53
+ ...queryContract,
5
54
  key: "system.users.list",
6
55
  label: "查询用户",
7
56
  description: "分页查询平台用户账号。",
8
57
  permissionKey: "system.user.read",
9
58
  featureKey: "system.rbac",
10
59
  auditAction: "agent.tool.execute",
60
+ scopeMode: "request",
11
61
  },
12
62
  {
63
+ ...queryContract,
13
64
  key: "system.features.list",
14
65
  label: "查询功能开关",
15
66
  description: "分页查询平台功能开关状态。",
@@ -18,6 +69,7 @@ export const systemAgentTools: ModuleManifest["tools"] = [
18
69
  auditAction: "agent.tool.execute",
19
70
  },
20
71
  {
72
+ ...queryContract,
21
73
  key: "system.audit.list",
22
74
  label: "查询审计日志",
23
75
  description: "分页查询平台审计事件。",
@@ -1,5 +1,8 @@
1
1
  import type { ApiPermissionBinding } from "@southwind-ai/shared";
2
2
  import { binding, resourceBindings } from "./system-api-binding.js";
3
+ // @windy-module system.settings begin
4
+ import { watermarkApiPermissionBindings } from "./system-api-watermark.js";
5
+ // @windy-module system.settings end
3
6
  // @windy-module system.data-governance begin
4
7
  import { dataGovernanceApiPermissionBindings } from "./system-api-data-governance.js";
5
8
  // @windy-module system.data-governance end
@@ -22,6 +25,11 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
22
25
  ),
23
26
  binding("POST", "/api/search", "system.search.read", "system.search"),
24
27
  // @windy-module system.search end
28
+ // @windy-module system.agent begin
29
+ binding("GET", "/api/mcp", "system.agent.read", "system.agent"),
30
+ binding("POST", "/api/mcp", "system.agent.read", "system.agent"),
31
+ binding("DELETE", "/api/mcp", "system.agent.read", "system.agent"),
32
+ // @windy-module system.agent end
25
33
  ...resourceBindings({
26
34
  path: "users",
27
35
  permissionResource: "user",
@@ -109,6 +117,7 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
109
117
  "system.settings.manage",
110
118
  "system.settings",
111
119
  ),
120
+ ...watermarkApiPermissionBindings(),
112
121
  // @windy-module system.settings end
113
122
  // @windy-module system.configuration begin
114
123
  binding(
@@ -0,0 +1,26 @@
1
+ import type { ApiPermissionBinding } from "@southwind-ai/shared";
2
+ import { binding } from "./system-api-binding.js";
3
+
4
+ export function watermarkApiPermissionBindings(): ApiPermissionBinding[] {
5
+ return [
6
+ binding(
7
+ "GET",
8
+ "/api/platform/watermark-policy",
9
+ "system.watermark.read",
10
+ undefined,
11
+ true,
12
+ ),
13
+ binding(
14
+ "GET",
15
+ "/api/system/settings/watermark",
16
+ "system.watermark.read",
17
+ "system.watermark",
18
+ ),
19
+ binding(
20
+ "PUT",
21
+ "/api/system/settings/watermark",
22
+ "system.watermark.manage",
23
+ "system.watermark",
24
+ ),
25
+ ];
26
+ }
@@ -8,6 +8,11 @@ export const systemAuditActions: ModuleManifest["auditActions"] = [
8
8
  "crud.create",
9
9
  "crud.update",
10
10
  "crud.delete",
11
+ {
12
+ key: "system.user-directory.query",
13
+ label: "查询业务用户目录",
14
+ description: "业务模块在平台授权与数据范围内查询启用用户最小投影。",
15
+ },
11
16
  // @windy-module system.configuration begin
12
17
  "config.update",
13
18
  "config.rollback",
@@ -17,6 +17,11 @@ export const systemFeatures: ModuleManifest["features"] = [
17
17
  "RBAC 权限链路",
18
18
  "用户、角色、权限点与数据范围的统一访问控制链路。",
19
19
  ),
20
+ feature(
21
+ "system.user-directory",
22
+ "业务用户目录",
23
+ "向获授权业务模块提供受数据范围约束的启用用户最小只读投影。",
24
+ ),
20
25
  feature(
21
26
  "system.profile",
22
27
  "个人设置",
@@ -43,7 +48,7 @@ export const systemFeatures: ModuleManifest["features"] = [
43
48
  feature(
44
49
  "system.watermark",
45
50
  "页面水印",
46
- "带身份信息的页面水印与用户显示偏好。",
51
+ "由平台统一管理并覆盖 Admin 与业务页面的身份水印策略。",
47
52
  ),
48
53
  // @windy-module system.settings end
49
54
  // @windy-module system.license begin
@@ -27,9 +27,17 @@ export const systemModuleDefinitions = [
27
27
  version: "0.1.0",
28
28
  label: "身份与权限",
29
29
  description: "用户、角色、部门、菜单、字典、个人资料和数据范围。",
30
- resources: ["user", "role", "department", "menu", "dict", "profile"],
30
+ resources: [
31
+ "user",
32
+ "user-directory",
33
+ "role",
34
+ "department",
35
+ "menu",
36
+ "dict",
37
+ "profile",
38
+ ],
31
39
  excludedPermissionKeys: ["system.user.reveal", "system.user.export"],
32
- featureKeys: ["system.rbac", "system.profile"],
40
+ featureKeys: ["system.rbac", "system.user-directory", "system.profile"],
33
41
  },
34
42
  // @windy-module system.settings begin
35
43
  {
@@ -218,6 +226,7 @@ function createSystemCapabilityModule(
218
226
  .map((feature) => ({ ...feature, module: definition.name })),
219
227
  migrations: isCore ? aggregate.migrations : [],
220
228
  schemaExports: isCore ? aggregate.schemaExports : [],
229
+ jsonSchemas: isCore ? aggregate.jsonSchemas : [],
221
230
  tasks: aggregate.tasks.filter(ownsGuard),
222
231
  tools: aggregate.tools.filter(ownsGuard),
223
232
  providers: aggregate.providers.filter(ownsGuard),
@@ -225,11 +234,26 @@ function createSystemCapabilityModule(
225
234
  definition.name === "system.data-governance"
226
235
  ? aggregate.dataPolicies
227
236
  : [],
228
- auditActions: isCore ? aggregate.auditActions : [],
237
+ auditActions: isCore
238
+ ? aggregate.auditActions.filter(
239
+ (action) => auditActionKey(action) !== "system.user-directory.query",
240
+ )
241
+ : definition.name === "system.identity"
242
+ ? aggregate.auditActions.filter(
243
+ (action) =>
244
+ auditActionKey(action) === "system.user-directory.query",
245
+ )
246
+ : [],
229
247
  metadata: { kind: "platform-capability" },
230
248
  };
231
249
  }
232
250
 
251
+ function auditActionKey(
252
+ action: ModuleManifest["auditActions"][number],
253
+ ): string {
254
+ return typeof action === "string" ? action : action.key;
255
+ }
256
+
233
257
  function permissionResource(permissionKey: string): string {
234
258
  const parts = permissionKey.split(".");
235
259
  return parts.length >= 3 ? parts.slice(1, -1).join(".") : "";
@@ -1,6 +1,9 @@
1
1
  import type { ModuleManifest } from "./manifest.js";
2
2
  // @windy-module system.agent begin
3
- import { systemAgentTools } from "./system-agent-tools.js";
3
+ import {
4
+ systemAgentToolSchemas,
5
+ systemAgentTools,
6
+ } from "./system-agent-tools.js";
4
7
  // @windy-module system.agent end
5
8
  import { systemAdminRoutes } from "./system-admin-routes.js";
6
9
  import { systemApiPermissionBindings } from "./system-api-permissions.js";
@@ -249,6 +252,11 @@ export function systemFoundationModule(): ModuleManifest {
249
252
  migrationIds: ["0000..0028"],
250
253
  },
251
254
  ],
255
+ jsonSchemas: [
256
+ // @windy-module system.agent begin
257
+ ...systemAgentToolSchemas,
258
+ // @windy-module system.agent end
259
+ ],
252
260
  tasks: [
253
261
  // @windy-module system.scheduler begin
254
262
  {
@@ -3,6 +3,7 @@ import type { ModuleManifest } from "./manifest.js";
3
3
  const systemResourceLabels = {
4
4
  admin: "管理面准入",
5
5
  user: "用户管理",
6
+ "user-directory": "业务用户目录",
6
7
  role: "角色与权限",
7
8
  department: "部门组织",
8
9
  menu: "菜单管理",
@@ -75,6 +76,7 @@ export const systemPermissions: ModuleManifest["permissions"] = [
75
76
  permission("user", "export", "governed-export"),
76
77
  // @windy-module system.data-governance end
77
78
  permission("user", "manage"),
79
+ permission("user-directory", "read"),
78
80
  permission("role", "read"),
79
81
  permission("role", "manage"),
80
82
  permission("department", "read"),
@@ -0,0 +1,86 @@
1
+ import type {
2
+ ModuleJsonSchemaDefinition,
3
+ ModuleManifest,
4
+ ModuleToolDefinition,
5
+ } from "./manifest.js";
6
+
7
+ export function validateToolContracts(
8
+ modules: readonly ModuleManifest[],
9
+ ): ReadonlyMap<string, ModuleJsonSchemaDefinition> {
10
+ const schemas = new Map<string, ModuleJsonSchemaDefinition>();
11
+ for (const module of modules) {
12
+ for (const definition of module.jsonSchemas ?? []) {
13
+ validateSchema(module.name, definition);
14
+ if (schemas.has(definition.key)) {
15
+ throw new Error(`JSON Schema Key 重复:${definition.key}`);
16
+ }
17
+ schemas.set(definition.key, definition);
18
+ }
19
+ }
20
+ for (const module of modules) {
21
+ for (const tool of module.tools) {
22
+ validateTool(module.name, tool, schemas);
23
+ }
24
+ }
25
+ return schemas;
26
+ }
27
+
28
+ function validateSchema(
29
+ moduleName: string,
30
+ definition: ModuleJsonSchemaDefinition,
31
+ ): void {
32
+ if (!definition.key.trim()) throw new Error("JSON Schema Key 不能为空");
33
+ if (!definition.key.startsWith(`${moduleName}.`)) {
34
+ throw new Error(
35
+ `JSON Schema ${definition.key} 必须使用模块命名空间 ${moduleName}`,
36
+ );
37
+ }
38
+ if (definition.schema.type !== "object") {
39
+ throw new Error(`JSON Schema ${definition.key} 顶层类型必须是 object`);
40
+ }
41
+ }
42
+
43
+ function validateTool(
44
+ moduleName: string,
45
+ tool: ModuleToolDefinition,
46
+ schemas: ReadonlyMap<string, ModuleJsonSchemaDefinition>,
47
+ ): void {
48
+ const fields: Array<string | undefined> = [
49
+ tool.key,
50
+ tool.label,
51
+ tool.description,
52
+ tool.inputSchemaKey,
53
+ tool.outputSchemaKey,
54
+ tool.featureKey,
55
+ tool.permissionKey,
56
+ tool.auditAction,
57
+ ];
58
+ if (fields.some((value) => !value?.trim())) {
59
+ throw new Error("Agent Tool Key、展示信息与引用字段不能为空");
60
+ }
61
+ const rootNamespace = moduleName.split(".")[0]!;
62
+ if (!tool.key.startsWith(`${rootNamespace}.`)) {
63
+ throw new Error(
64
+ `Agent Tool ${tool.key} 必须使用模块根命名空间 ${rootNamespace}`,
65
+ );
66
+ }
67
+ requireSchema(tool.key, tool.inputSchemaKey, schemas);
68
+ requireSchema(tool.key, tool.outputSchemaKey, schemas);
69
+ if (tool.operation === "query" && tool.approval !== "none") {
70
+ throw new Error(`${tool.key} 查询能力不能要求执行审批`);
71
+ }
72
+ const limits = Object.values(tool.limits);
73
+ if (limits.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
74
+ throw new Error(`${tool.key} 运行与输入输出上限必须是正整数`);
75
+ }
76
+ }
77
+
78
+ function requireSchema(
79
+ toolKey: string,
80
+ schemaKey: string,
81
+ schemas: ReadonlyMap<string, ModuleJsonSchemaDefinition>,
82
+ ): void {
83
+ if (!schemas.has(schemaKey)) {
84
+ throw new Error(`${toolKey} 引用了未注册 JSON Schema:${schemaKey}`);
85
+ }
86
+ }