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.
- package/dist/cli.js +7 -1
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +10 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/apps/agent-server/README.md +10 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +62 -1
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
- package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
- package/template/apps/agent-server/tests/test_api.py +16 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +2 -0
- package/template/apps/server/src/agent/run-facade.test.ts +52 -2
- package/template/apps/server/src/agent/run-facade.ts +36 -2
- package/template/apps/server/src/index.ts +28 -45
- package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
- package/template/apps/server/src/module-host/initialize.ts +30 -44
- package/template/apps/server/src/module-host/request-context.ts +23 -17
- package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
- package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
- package/template/apps/server/src/module-host/route-declaration.ts +45 -0
- package/template/apps/server/src/module-host/route-installer.ts +27 -3
- package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
- package/template/apps/server/src/module-host/storage-port.ts +134 -1
- package/template/apps/server/src/module-host/storage-scope.ts +96 -0
- package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
- package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
- package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
- package/template/apps/server/src/module-host/user-directory-port.ts +143 -0
- package/template/apps/server/src/module-host.ts +4 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/system/built-in-roles.ts +1 -0
- package/template/docker-compose.yml +2 -0
- package/template/docs/architecture/ai-egress.md +49 -7
- package/template/docs/architecture/ai-runtime.md +20 -1
- package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
- package/template/docs/architecture/object-storage.md +22 -2
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +44 -1
- package/template/docs/platform/business-user-directory.md +82 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
- package/template/packages/modules/src/ai-operation-composition.ts +115 -0
- package/template/packages/modules/src/manifest.ts +26 -0
- package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
- package/template/packages/modules/src/system-audit-actions.ts +5 -0
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
- package/template/packages/modules/src/system-module-catalog.ts +26 -3
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +3 -2
- package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
- package/template/packages/server-sdk/src/module-host.ts +8 -0
- package/template/packages/server-sdk/src/request-context.ts +2 -0
- package/template/packages/server-sdk/src/storage-port.ts +66 -0
- package/template/packages/server-sdk/src/tool-registration.ts +6 -0
- 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 中;
|
|
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 是否完成升级。
|
|
@@ -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",
|
|
@@ -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: [
|
|
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
|
|
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
|
+
"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 {
|