create-windy 0.2.33 → 0.2.34

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.
Files changed (69) hide show
  1. package/dist/cli.js +7 -1
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +4 -0
  6. package/template/apps/agent-server/README.md +10 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/app.py +62 -1
  8. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  11. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  12. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  15. package/template/apps/agent-server/tests/test_api.py +16 -1
  16. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  17. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  18. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  19. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  20. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  21. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  22. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  23. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  24. package/template/apps/server/src/agent/run-facade.ts +36 -2
  25. package/template/apps/server/src/index.ts +28 -45
  26. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  27. package/template/apps/server/src/module-host/initialize.ts +30 -44
  28. package/template/apps/server/src/module-host/request-context.ts +23 -17
  29. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  30. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  31. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  32. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  33. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  34. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  35. package/template/apps/server/src/module-host/storage-scope.ts +96 -0
  36. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  37. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  38. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  39. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  40. package/template/apps/server/src/module-host/user-directory-port.ts +143 -0
  41. package/template/apps/server/src/module-host.ts +4 -0
  42. package/template/apps/server/src/runtime-development.ts +1 -0
  43. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  44. package/template/docker-compose.yml +2 -0
  45. package/template/docs/architecture/ai-egress.md +49 -7
  46. package/template/docs/architecture/ai-runtime.md +20 -1
  47. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  48. package/template/docs/architecture/object-storage.md +22 -2
  49. package/template/docs/development/custom-login-page.md +3 -3
  50. package/template/docs/platform/agent-runtime.md +44 -1
  51. package/template/docs/platform/business-user-directory.md +82 -0
  52. package/template/packages/modules/package.json +1 -1
  53. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  54. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  55. package/template/packages/modules/src/manifest.ts +26 -0
  56. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  57. package/template/packages/modules/src/system-audit-actions.ts +5 -0
  58. package/template/packages/modules/src/system-features.ts +5 -0
  59. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  60. package/template/packages/modules/src/system-module-catalog.ts +26 -3
  61. package/template/packages/modules/src/system-permissions.ts +2 -0
  62. package/template/packages/server-sdk/index.ts +11 -0
  63. package/template/packages/server-sdk/package.json +3 -2
  64. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  65. package/template/packages/server-sdk/src/module-host.ts +8 -0
  66. package/template/packages/server-sdk/src/request-context.ts +2 -0
  67. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  68. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  69. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
@@ -37,12 +37,34 @@ providerEgress: {
37
37
  mode: "configured-endpoint",
38
38
  deploymentKey: "ai.openai-compatible",
39
39
  },
40
+ providerDataPolicy: {
41
+ inputFields: [
42
+ {
43
+ jsonPointer: "/content",
44
+ classification: "internal",
45
+ dlpAction: "redact",
46
+ },
47
+ ],
48
+ envelopeFields: [
49
+ {
50
+ field: "instructions",
51
+ classification: "internal",
52
+ dlpAction: "reject",
53
+ },
54
+ {
55
+ field: "user-prompt",
56
+ classification: "internal",
57
+ dlpAction: "redact",
58
+ },
59
+ ],
60
+ },
40
61
  toolNetworkPolicy: {
41
62
  mode: "forbidden",
42
63
  },
43
64
  ```
44
65
 
45
- Provider URL 与 Secret 不在 Manifest 或浏览器 DTO 中;Agent Server 只允许当前启动
66
+ Provider URL 与 Secret 不在 Manifest 或浏览器 DTO 中;Server 只把 `inputFields`
67
+ 选出的治理后输入交给 Handler,Agent Server 只允许当前启动
46
68
  配置的 Deployment。Tool policy 缺省为 forbidden,且不继承 Provider egress。完整
47
69
  威胁模型、兼容行为与尚未覆盖项见
48
70
  [Agent Provider 与 Tool 网络边界](../architecture/ai-egress.md)。
@@ -104,6 +126,7 @@ QWEN_API_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1
104
126
  QWEN_API_KEY=<服务端密钥>
105
127
  QWEN_MODEL=qwen-plus
106
128
  AI_PROVIDER_HTTPS_HOST_ALLOWLIST=dashscope-intl.aliyuncs.com
129
+ AI_PROVIDER_AUDIT_RETENTION_DAYS=90
107
130
 
108
131
  # 可选安全上限
109
132
  # AI_PROVIDER_CONNECT_TIMEOUT_MS=5000
@@ -125,10 +148,30 @@ bun run dev:agent
125
148
  ```text
126
149
  GET http://agent-server:8080/health/live
127
150
  GET http://agent-server:8080/health/ready
151
+ GET http://agent-server:8080/health/provider-governance
128
152
  ```
129
153
 
130
154
  `ready` 会访问 Provider 的 `/models`。失败只返回稳定错误,不回显 Provider body、
131
155
  Authorization、Secret、Endpoint query 或底层异常文本。
156
+ `provider-governance` 需要内部 Bearer Token,只返回留存、允许/拒绝/脱敏和 TTL 清理
157
+ 聚合,不返回 Run ID、prompt 或正文。
158
+
159
+ ## 字段治理迁移
160
+
161
+ 启用 `providerEgress.mode=configured-endpoint` 的既有业务 Operation 必须同时新增
162
+ `providerDataPolicy`。升级后缺少策略会在 Manifest Composition 阶段以
163
+ `Provider 字段发送 allowlist 缺失` fail fast,不会静默发送或降级放行。
164
+
165
+ 迁移时先枚举业务 input schema 中确实需要出境的最小 JSON Pointer;为每个字段声明
166
+ 分类与 `reject/redact`。再枚举 outbound envelope:无 Tool 的单轮问答通常只需要
167
+ `instructions` 和 `user-prompt`;Tool schema、模型上一轮 Tool Call 和 Tool result
168
+ 分别需要 `tool-definitions`、`provider-output`、`tool-results`。不要为消除启动错误
169
+ 一次性允许全部字段。原始 input 会在进入 `prepare` 前裁剪,因此 Handler 必须只依赖
170
+ allowlist 中的字段。
171
+
172
+ Agent profile 还必须挂载 `AI_PROVIDER_AUDIT_STORE_PATH`。create-windy Starter 使用
173
+ `/app/data/agent/provider-audit.sqlite3` 并复用单副本持久卷;自行维护 Compose 的项目
174
+ 需要显式补齐该变量。未配置时 Provider 启动 fail closed,并给出缺失变量名。
132
175
 
133
176
  ## 客户端协议
134
177
 
@@ -0,0 +1,82 @@
1
+ # 业务模块只读用户目录
2
+
3
+ ## Ownership 与 Seam
4
+
5
+ 用户账号、启停状态、部门关系和数据范围属于 `system.identity`。业务模块需要选择案件
6
+ 成员、协作人或负责人时,不得导入 Admin Repository、Drizzle schema 或 Server 内部
7
+ `RequestActor`。稳定 seam 是 `ModuleRequestContext.userDirectory`,其公开 Interface
8
+ 只有一个方法:
9
+
10
+ ```ts
11
+ const page = await context.userDirectory.list({
12
+ page: 1,
13
+ pageSize: 20,
14
+ q: "张",
15
+ });
16
+ // page.items: Array<{ id: string; displayName: string }>
17
+ ```
18
+
19
+ 这个深 Module 把授权、范围、状态过滤、搜索、分页、投影和审计藏在宿主 Adapter 中,
20
+ 业务调用方只需学习查询条件与稳定错误码。删除该 Module 会迫使每个业务模块重复实现
21
+ 同一组安全约束,因此该 seam 提供了跨模块的 leverage 和维护 locality。
22
+
23
+ ## 安全不变量
24
+
25
+ - Actor、租户、部门和 Data Scope 只来自已验证请求的宿主
26
+ `RequestGuardContext`。Interface 不接受这些字段;HTTP body/query 中同名字段不能覆盖。
27
+ - V1 的 `tenantId` 是宿主 nominal `ScopeContext` 中的固定
28
+ `single-tenant`,不是忽略租户。`tenant` grant 仍经 Repository predicate 执行;未来
29
+ 多租户只替换宿主 Adapter,不扩大业务 Interface。
30
+ - `all`、`tenant`、`department`、`department-and-children`、`custom` 和 `self`
31
+ 都编译为用户 Repository 的查询条件,与启用状态、搜索、count 和分页共用同一个
32
+ `WHERE`。缺少 nominal `ScopeContext` 时返回 `data-scope-denied`,不会回退全量。
33
+ - 只返回启用、未删除用户的 `id` 和 `displayName`。部门、用户名、联系方式、角色、
34
+ 凭据状态和审计字段不跨 seam 暴露。
35
+ - 调用者必须同时具备启用的 `system.user-directory` Feature 和
36
+ `system.user-directory.read` Permission。拒绝走统一 `security.denied` 审计。
37
+ - 每次成功查询在返回结果前以 critical 策略持久化
38
+ `system.user-directory.query`,元数据只含页码、页大小、结果数和总数,不记录搜索词
39
+ 或返回用户明细;生产审计不可用时查询 fail closed。
40
+ - 页码与页大小必须为正整数;页码最多 10000、页大小最多 100,搜索词必须是最多 100
41
+ 字符的字符串。非法输入返回 `USER_DIRECTORY_INVALID_QUERY`。
42
+
43
+ ## 业务角色接入
44
+
45
+ 业务 `ModuleRolePreset` 只能注册本模块命名空间内的 Permission,不能夹带
46
+ `system.user-directory.read` 绕过平台 ownership。部署管理员应在角色管理中显式为
47
+ 需要选择用户的办案角色授予该平台 Permission,并根据角色配置 `all`、`tenant`、
48
+ 部门、`custom` 或 `self` 数据范围。
49
+
50
+ 业务 Handler 不要把浏览器提交的 `actorId`、`tenantId`、`departmentId` 或 scope
51
+ 转发为授权参数:
52
+
53
+ ```ts
54
+ handler: async (context) => {
55
+ const page = await context.userDirectory.list({
56
+ page: Number(context.query.page || 1),
57
+ pageSize: Number(context.query.pageSize || 20),
58
+ q: context.query.q,
59
+ });
60
+ return page;
61
+ };
62
+ ```
63
+
64
+ 未挂载平台 Adapter 时返回 `USER_DIRECTORY_UNAVAILABLE`;Permission 或 Feature 拒绝
65
+ 返回 `USER_DIRECTORY_FORBIDDEN`。业务路由可以把稳定错误码映射为自己的 HTTP 错误
66
+ 语义,但不得捕获后改用 Admin 用户接口或裸 Repository。
67
+
68
+ ## 生成与升级
69
+
70
+ `packages/server-sdk` 的 Interface、Server Adapter、Composition Root、Manifest、
71
+ 本文件和 Starter skill 都是 Windy 模板受管内容。新项目自动生成这些文件;既有项目
72
+ 由 `windy update` 通过 Base/Ours/Theirs 创建新增文件并合并 Composition Root。
73
+ 升级后由管理员授予 Permission,再由 project-owned 业务 Handler 调用 Port;升级器
74
+ 不会自动修改客户业务角色或业务页面。
75
+
76
+ 运维排查顺序:
77
+
78
+ 1. 确认 `system.user-directory` 在 Feature 管理中启用;
79
+ 2. 确认 Actor 拥有 `system.user-directory.read`;
80
+ 3. 检查 Actor 的角色数据范围与部门关系;
81
+ 4. 查询 `security.denied` 和 `system.user-directory.query` 审计;
82
+ 5. 若返回 `USER_DIRECTORY_UNAVAILABLE`,检查 Starter Composition Root 是否完成升级。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -112,6 +112,7 @@ describe("AI Operation Composition", () => {
112
112
  mode: "configured-endpoint",
113
113
  deploymentKey: "ai.openai-compatible",
114
114
  },
115
+ providerDataPolicy: providerDataPolicy(),
115
116
  toolNetworkPolicy: { mode: "forbidden" },
116
117
  },
117
118
  ];
@@ -133,6 +134,7 @@ describe("AI Operation Composition", () => {
133
134
  mode: "configured-endpoint",
134
135
  deploymentKey: " ",
135
136
  },
137
+ providerDataPolicy: providerDataPolicy(),
136
138
  },
137
139
  ];
138
140
  expect(() => validateModuleComposition([module])).toThrow(
@@ -148,6 +150,48 @@ describe("AI Operation Composition", () => {
148
150
  );
149
151
  });
150
152
 
153
+ test("Provider 字段策略必须完整、唯一且与出网授权绑定", () => {
154
+ const module = fixture("business");
155
+ module.aiOperations = [
156
+ {
157
+ ...operation("business.generate"),
158
+ providerEgress: {
159
+ mode: "configured-endpoint",
160
+ deploymentKey: "ai.openai-compatible",
161
+ },
162
+ },
163
+ ];
164
+ expect(() => validateModuleComposition([module])).toThrow(
165
+ "字段发送 allowlist 缺失",
166
+ );
167
+
168
+ module.aiOperations[0] = {
169
+ ...module.aiOperations[0],
170
+ providerDataPolicy: {
171
+ ...providerDataPolicy(),
172
+ envelopeFields: [
173
+ ...providerDataPolicy().envelopeFields,
174
+ {
175
+ field: "instructions",
176
+ classification: "internal",
177
+ dlpAction: "reject",
178
+ },
179
+ ],
180
+ },
181
+ };
182
+ expect(() => validateModuleComposition([module])).toThrow(
183
+ "字段发送规则重复",
184
+ );
185
+
186
+ module.aiOperations[0] = {
187
+ ...operation("business.generate"),
188
+ providerDataPolicy: providerDataPolicy(),
189
+ };
190
+ expect(() => validateModuleComposition([module])).toThrow(
191
+ "禁止 Provider 出网时不能声明字段发送策略",
192
+ );
193
+ });
194
+
151
195
  test("Operation Handler 必须与 Manifest 声明一一对应", () => {
152
196
  const module = fixture("business");
153
197
  module.aiOperations = [operation("business.generate")];
@@ -190,6 +234,30 @@ function operation(
190
234
  };
191
235
  }
192
236
 
237
+ function providerDataPolicy() {
238
+ return {
239
+ inputFields: [
240
+ {
241
+ jsonPointer: "/question",
242
+ classification: "internal" as const,
243
+ dlpAction: "redact" as const,
244
+ },
245
+ ],
246
+ envelopeFields: [
247
+ {
248
+ field: "instructions" as const,
249
+ classification: "internal" as const,
250
+ dlpAction: "reject" as const,
251
+ },
252
+ {
253
+ field: "user-prompt" as const,
254
+ classification: "internal" as const,
255
+ dlpAction: "redact" as const,
256
+ },
257
+ ],
258
+ };
259
+ }
260
+
193
261
  function fixture(name: string): ModuleManifest {
194
262
  const permissionKey = `${name}.ai.execute`;
195
263
  const featureKey = `${name}.ai`;
@@ -102,9 +102,124 @@ function validateOperation(
102
102
  validateCapabilities(operation);
103
103
  validateTools(operation, catalogs.tools);
104
104
  validateAiNetworkPolicy(operation);
105
+ validateProviderDataPolicy(operation);
105
106
  validateLimits(operation);
106
107
  }
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
+
108
223
  function validateCapabilities(operation: ModuleAiOperationDefinition): void {
109
224
  if (!operation.requiredCapabilities.length) {
110
225
  throw new Error(`${operation.key} 至少需要一个 capability`);
@@ -98,6 +98,31 @@ export interface AiToolNetworkPolicy {
98
98
  mode: "forbidden";
99
99
  }
100
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
+
101
126
  export interface AiOperationLimits {
102
127
  maxDurationSeconds: number;
103
128
  maxInputBytes: number;
@@ -119,6 +144,7 @@ export interface ModuleAiOperationDefinition {
119
144
  /** @deprecated 使用 providerEgress 与 toolNetworkPolicy 拆分两类网络边界。 */
120
145
  publicNetworkAccess?: AiOperationPublicNetworkAccess;
121
146
  providerEgress?: AiProviderEgressPolicy;
147
+ providerDataPolicy?: AiProviderDataPolicy;
122
148
  toolNetworkPolicy?: AiToolNetworkPolicy;
123
149
  limits: AiOperationLimits;
124
150
  }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateProviderDataPolicy } from "./ai-operation-composition.js";
3
+ import type { ModuleAiOperationDefinition } from "./manifest.js";
4
+
5
+ describe("Provider 输入字段 Schema", () => {
6
+ test.each(["/bad~2escape", "/__proto__/polluted", "/constructor/value"])(
7
+ "拒绝危险或非法 JSON Pointer:%s",
8
+ (jsonPointer) => {
9
+ expect(() =>
10
+ validateProviderDataPolicy(operation([jsonPointer])),
11
+ ).toThrow("JSON Pointer 无效");
12
+ },
13
+ );
14
+
15
+ test("拒绝父子重叠路径,避免规则顺序改变结果", () => {
16
+ expect(() =>
17
+ validateProviderDataPolicy(operation(["/case", "/case/name"])),
18
+ ).toThrow("不能父子重叠");
19
+ });
20
+
21
+ test("使用 Tool 时要求声明定义、上一轮输出和 Tool 结果", () => {
22
+ expect(() =>
23
+ validateProviderDataPolicy(operation(["/question"], ["business.query"])),
24
+ ).toThrow("使用 Tool 时");
25
+ });
26
+ });
27
+
28
+ function operation(
29
+ inputPointers: readonly string[],
30
+ allowedToolKeys: readonly string[] = [],
31
+ ): ModuleAiOperationDefinition {
32
+ return {
33
+ key: "business.answer",
34
+ kind: "agent",
35
+ featureKey: "business.ai",
36
+ permissionKey: "business.ai.execute",
37
+ auditAction: "business.ai.execute",
38
+ inputSchemaKey: "business.input.v1",
39
+ outputSchemaKey: "business.output.v1",
40
+ requiredCapabilities: ["text-generation"],
41
+ allowedToolKeys,
42
+ dataClassification: "internal",
43
+ execution: "streaming",
44
+ providerEgress: {
45
+ mode: "configured-endpoint",
46
+ deploymentKey: "ai.openai-compatible",
47
+ },
48
+ providerDataPolicy: {
49
+ inputFields: inputPointers.map((jsonPointer) => ({
50
+ jsonPointer,
51
+ classification: "internal",
52
+ dlpAction: "redact",
53
+ })),
54
+ envelopeFields: [
55
+ {
56
+ field: "instructions",
57
+ classification: "internal",
58
+ dlpAction: "reject",
59
+ },
60
+ {
61
+ field: "user-prompt",
62
+ classification: "internal",
63
+ dlpAction: "redact",
64
+ },
65
+ ],
66
+ },
67
+ toolNetworkPolicy: { mode: "forbidden" },
68
+ limits: {
69
+ maxDurationSeconds: 60,
70
+ maxInputBytes: 4096,
71
+ maxOutputBytes: 4096,
72
+ },
73
+ };
74
+ }
@@ -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
  "个人设置",
@@ -116,6 +116,22 @@ describe("系统能力模块目录", () => {
116
116
  "system.workflow.timeout-dispatch",
117
117
  ]);
118
118
  expect(identity.dataPolicies).toEqual([]);
119
+ expect(identity.features.map(({ key }) => key)).toContain(
120
+ "system.user-directory",
121
+ );
122
+ expect(identity.permissions.map(({ key }) => key)).toContain(
123
+ "system.user-directory.read",
124
+ );
125
+ expect(
126
+ identity.auditActions.map((action) =>
127
+ typeof action === "string" ? action : action.key,
128
+ ),
129
+ ).toContain("system.user-directory.query");
130
+ expect(
131
+ systemFoundationModule().auditActions.map((action) =>
132
+ typeof action === "string" ? action : action.key,
133
+ ),
134
+ ).toContain("system.user-directory.query");
119
135
  expect(
120
136
  systemModuleManifest("system.data-governance").dataPolicies?.map(
121
137
  ({ key }) => key,
@@ -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
  {
@@ -226,11 +234,26 @@ function createSystemCapabilityModule(
226
234
  definition.name === "system.data-governance"
227
235
  ? aggregate.dataPolicies
228
236
  : [],
229
- 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
+ : [],
230
247
  metadata: { kind: "platform-capability" },
231
248
  };
232
249
  }
233
250
 
251
+ function auditActionKey(
252
+ action: ModuleManifest["auditActions"][number],
253
+ ): string {
254
+ return typeof action === "string" ? action : action.key;
255
+ }
256
+
234
257
  function permissionResource(permissionKey: string): string {
235
258
  const parts = permissionKey.split(".");
236
259
  return parts.length >= 3 ? parts.slice(1, -1).join(".") : "";
@@ -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"),
@@ -32,9 +32,20 @@ export type {
32
32
  ModuleSearchQuery,
33
33
  } from "./src/search-provider-port.js";
34
34
  export type {
35
+ ModuleOpenedResourceFile,
36
+ ModuleResourceFileAccessContext,
37
+ ModuleResourceFileAccessPolicy,
38
+ ModuleResourceFileRequest,
35
39
  ModuleStoragePort,
36
40
  ModuleUploadSessionInput,
37
41
  } from "./src/storage-port.js";
42
+ export type {
43
+ ModuleUserDirectoryEntry,
44
+ ModuleUserDirectoryPage,
45
+ ModuleUserDirectoryPort,
46
+ ModuleUserDirectoryQuery,
47
+ } from "./src/user-directory-port.js";
48
+ export { ModuleUserDirectoryError } from "./src/user-directory-port.js";
38
49
  export type {
39
50
  ModuleTaskHandler,
40
51
  ModuleTaskHandlerRegistration,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/server-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -21,7 +21,8 @@
21
21
  "src/storage-port.ts",
22
22
  "src/task-registration.ts",
23
23
  "src/tool-registration.ts",
24
- "src/tool-host-port.ts"
24
+ "src/tool-host-port.ts",
25
+ "src/user-directory-port.ts"
25
26
  ],
26
27
  "scripts": {
27
28
  "test": "bun test",
@@ -9,7 +9,8 @@ export interface ModuleAiOperationContext {
9
9
  }
10
10
 
11
11
  /**
12
- * 业务模块只负责把类型化输入收敛为模型无关的指令和用户消息。
12
+ * 业务模块只接收 Manifest `providerDataPolicy.inputFields` allowlist 选出并完成
13
+ * DLP 的最小输入,再将其收敛为模型无关的指令和用户消息。
13
14
  * Provider、Deployment、Secret、Execution Grant 与 Tool 授权均由宿主持有。
14
15
  */
15
16
  export interface ModuleAiOperationPlan {