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
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ModuleUserDirectoryError } from "@southwind-ai/server-sdk";
3
+ import { buildScopeContext } from "../data-access/context.js";
4
+ import { createFoundationSnapshot } from "../foundation.js";
5
+ import type { RequestActor, RequestGuardContext } from "../guards.js";
6
+ import { createServerRuntime } from "../runtime.js";
7
+ import type { ManagedDepartment, ManagedUser } from "../system/entities.js";
8
+ import { createSystemRepositories } from "../system/seed.js";
9
+ import { createModuleUserDirectoryPort } from "./user-directory-port.js";
10
+
11
+ describe("模块用户目录 Port", () => {
12
+ test("只返回启用用户最小投影,并在 Repository 查询中执行部门范围", async () => {
13
+ const fixture = await createFixture();
14
+ const page = await fixture.port.list({
15
+ page: 1,
16
+ pageSize: 10,
17
+ q: "案",
18
+ ...({ actorId: "forged", departmentId: "dept_other" } as object),
19
+ });
20
+
21
+ expect(page).toEqual({
22
+ items: [
23
+ {
24
+ id: "user_child",
25
+ displayName: "子部门办案员",
26
+ },
27
+ ],
28
+ total: 1,
29
+ page: 1,
30
+ pageSize: 10,
31
+ });
32
+ expect(Object.keys(page.items[0] ?? {}).sort()).toEqual([
33
+ "displayName",
34
+ "id",
35
+ ]);
36
+ expect(fixture.runtime.auditSink.list()).toContainEqual(
37
+ expect.objectContaining({
38
+ action: "system.user-directory.query",
39
+ actor: expect.objectContaining({ id: "actor_manager" }),
40
+ metadata: expect.objectContaining({ resultCount: 1, total: 1 }),
41
+ }),
42
+ );
43
+ });
44
+
45
+ test("缺少权限或 Feature 停用时 fail closed 并记录拒绝", async () => {
46
+ const missingPermission = await createFixture({ permission: false });
47
+ await expect(missingPermission.port.list()).rejects.toMatchObject({
48
+ code: "USER_DIRECTORY_FORBIDDEN",
49
+ });
50
+ expect(missingPermission.runtime.auditSink.list()).toContainEqual(
51
+ expect.objectContaining({
52
+ action: "security.denied",
53
+ outcome: "denied",
54
+ }),
55
+ );
56
+
57
+ const disabledFeature = await createFixture({ featureEnabled: false });
58
+ await expect(disabledFeature.port.list()).rejects.toMatchObject({
59
+ code: "USER_DIRECTORY_FORBIDDEN",
60
+ });
61
+ });
62
+
63
+ test("拒绝越界分页与搜索 Schema", async () => {
64
+ const fixture = await createFixture();
65
+ for (const query of [
66
+ { page: 0 },
67
+ { pageSize: 101 },
68
+ { q: "x".repeat(101) },
69
+ { q: 23 } as never,
70
+ ]) {
71
+ await expect(fixture.port.list(query)).rejects.toBeInstanceOf(
72
+ ModuleUserDirectoryError,
73
+ );
74
+ await expect(fixture.port.list(query)).rejects.toMatchObject({
75
+ code: "USER_DIRECTORY_INVALID_QUERY",
76
+ });
77
+ }
78
+ });
79
+
80
+ test("V1 tenant、custom 与 self 均只读取宿主 nominal ScopeContext", async () => {
81
+ const tenant = await createFixture({ dataScope: "tenant" });
82
+ expect((await tenant.port.list({ q: "办案员" })).items).toHaveLength(2);
83
+
84
+ const custom = await createFixture({ dataScope: "custom" });
85
+ expect((await custom.port.list({ q: "办案员" })).items).toEqual([
86
+ { id: "user_child", displayName: "子部门办案员" },
87
+ ]);
88
+
89
+ const self = await createFixture({ dataScope: "self" });
90
+ expect((await self.port.list({ q: "办案员" })).items).toEqual([
91
+ { id: "user_child", displayName: "子部门办案员" },
92
+ ]);
93
+ });
94
+
95
+ test("缺少宿主 nominal ScopeContext 时拒绝,不回退全量目录", async () => {
96
+ const fixture = await createFixture({ dataScope: "missing" });
97
+ await expect(fixture.port.list()).rejects.toThrow("data-scope-denied");
98
+ });
99
+
100
+ test("生产关键审计不可持久化时不返回目录结果", async () => {
101
+ const fixture = await createFixture({ productionAudit: true });
102
+ await expect(fixture.port.list()).rejects.toThrow(
103
+ "关键审计事件无法持久化或进入恢复队列",
104
+ );
105
+ });
106
+ });
107
+
108
+ interface FixtureOptions {
109
+ permission?: boolean;
110
+ featureEnabled?: boolean;
111
+ dataScope?: DirectoryScope;
112
+ productionAudit?: boolean;
113
+ }
114
+
115
+ type DirectoryScope =
116
+ | "all"
117
+ | "tenant"
118
+ | "department-and-children"
119
+ | "custom"
120
+ | "self"
121
+ | "missing";
122
+
123
+ async function createFixture(options: FixtureOptions = {}) {
124
+ const snapshot = createFoundationSnapshot();
125
+ const repositories = createSystemRepositories(snapshot);
126
+ await seedDirectory(repositories);
127
+ const actor = directoryActor(
128
+ options.permission ?? true,
129
+ options.dataScope ?? "department-and-children",
130
+ );
131
+ const runtime = createServerRuntime(
132
+ options.productionAudit ? { NODE_ENV: "production" } : {},
133
+ {
134
+ featureCatalog: snapshot.features,
135
+ featureResolver: {
136
+ resolve: (key) => ({
137
+ key,
138
+ enabled: options.featureEnabled ?? true,
139
+ visible: "visible",
140
+ }),
141
+ },
142
+ },
143
+ );
144
+ const context: RequestGuardContext = {
145
+ requestId: "request-directory",
146
+ actor,
147
+ startedAt: "2026-07-25T00:00:00.000Z",
148
+ };
149
+ return {
150
+ runtime,
151
+ port: createModuleUserDirectoryPort(
152
+ { repositories, runtime, features: snapshot.features },
153
+ context,
154
+ ),
155
+ };
156
+ }
157
+
158
+ async function seedDirectory(
159
+ repositories: ReturnType<typeof createSystemRepositories>,
160
+ ) {
161
+ const system = directoryActor(true, "all");
162
+ for (const departmentInput of [
163
+ department("dept_root", "业务部门"),
164
+ department("dept_child", "子部门", "dept_root"),
165
+ department("dept_other", "其它部门"),
166
+ ]) {
167
+ await repositories.departments.create(departmentInput, system);
168
+ }
169
+ for (const userInput of [
170
+ user("user_root", "根部门用户", "dept_root"),
171
+ user("user_child", "子部门办案员", "dept_child"),
172
+ user("user_other", "其它办案员", "dept_other"),
173
+ user("user_disabled", "停用办案员", "dept_child", "disabled"),
174
+ ]) {
175
+ await repositories.users.create(userInput, system);
176
+ }
177
+ }
178
+
179
+ function directoryActor(
180
+ permission: boolean,
181
+ scope: DirectoryScope = "department-and-children",
182
+ ): RequestActor {
183
+ const actorId = scope === "self" ? "user_child" : "actor_manager";
184
+ const role =
185
+ scope === "custom"
186
+ ? {
187
+ id: "role",
188
+ code: "role",
189
+ dataScope: "custom" as const,
190
+ departmentIds: ["dept_child"],
191
+ }
192
+ : {
193
+ id: "role",
194
+ code: "role",
195
+ dataScope: scope === "missing" ? ("self" as const) : scope,
196
+ departmentIds: [],
197
+ };
198
+ return {
199
+ id: scope === "all" ? "system_seed" : actorId,
200
+ type: "user",
201
+ name: "部门负责人",
202
+ roleCodes: ["department-manager"],
203
+ permissionKeys: permission ? ["system.user-directory.read"] : [],
204
+ ...(scope === "missing"
205
+ ? {}
206
+ : {
207
+ scopeContext: buildScopeContext(
208
+ scope === "all" ? "system_seed" : actorId,
209
+ scope === "all" || scope === "tenant" ? undefined : "dept_root",
210
+ [role],
211
+ ),
212
+ }),
213
+ };
214
+ }
215
+
216
+ function department(
217
+ id: string,
218
+ name: string,
219
+ parentId?: string,
220
+ ): Partial<ManagedDepartment> {
221
+ return { id, name, code: id, order: 10, status: "active", parentId };
222
+ }
223
+
224
+ function user(
225
+ id: string,
226
+ displayName: string,
227
+ departmentId: string,
228
+ status: ManagedUser["status"] = "active",
229
+ ): Partial<ManagedUser> {
230
+ return {
231
+ id,
232
+ username: id,
233
+ displayName,
234
+ departmentId,
235
+ roleIds: [],
236
+ status,
237
+ };
238
+ }
@@ -0,0 +1,143 @@
1
+ import { randomUUIDv7 } from "bun";
2
+ import {
3
+ ModuleUserDirectoryError,
4
+ type ModuleUserDirectoryPage,
5
+ type ModuleUserDirectoryPort,
6
+ type ModuleUserDirectoryQuery,
7
+ } from "@southwind-ai/server-sdk";
8
+ import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
9
+ import { DataScopedResourceService } from "../data-access/scoped-resource.js";
10
+ import { departmentDescendantResolver } from "../data-access/department-tree.js";
11
+ import type { RequestGuardContext } from "../guards.js";
12
+ import { evaluateRouteGuards, findFeature } from "../route-guards.js";
13
+ import type { createServerRuntime } from "../runtime.js";
14
+ import type { SystemRepositories } from "../system/seed.js";
15
+
16
+ const featureKey = "system.user-directory";
17
+ const permissionKey = "system.user-directory.read";
18
+ const auditAction = "system.user-directory.query";
19
+
20
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
21
+
22
+ export interface ModuleUserDirectoryServices {
23
+ readonly repositories: Pick<SystemRepositories, "users" | "departments">;
24
+ readonly runtime: ServerRuntime;
25
+ readonly features: readonly FeatureFlagDefinition[];
26
+ }
27
+
28
+ export function createModuleUserDirectoryPort(
29
+ services: ModuleUserDirectoryServices,
30
+ context: RequestGuardContext,
31
+ ): ModuleUserDirectoryPort {
32
+ const feature = findFeature([...services.features], featureKey);
33
+ const access = new DataScopedResourceService(
34
+ services.repositories.users,
35
+ departmentDescendantResolver(services.repositories.departments),
36
+ );
37
+ return {
38
+ async list(rawQuery = {}) {
39
+ if (!feature) {
40
+ throw new ModuleUserDirectoryError(
41
+ "USER_DIRECTORY_UNAVAILABLE",
42
+ `宿主未注册 Feature:${featureKey}`,
43
+ );
44
+ }
45
+ const decision = await evaluateRouteGuards(services.runtime, context, {
46
+ permission: {
47
+ method: "GET",
48
+ path: "/internal/module-user-directory",
49
+ permissionKey,
50
+ },
51
+ feature,
52
+ });
53
+ if (!decision.allowed) {
54
+ throw new ModuleUserDirectoryError(
55
+ "USER_DIRECTORY_FORBIDDEN",
56
+ `用户目录访问被拒绝:${decision.reason}`,
57
+ );
58
+ }
59
+ const query = normalizeQuery(rawQuery);
60
+ const page = await access.list(
61
+ {
62
+ page: query.page,
63
+ pageSize: query.pageSize,
64
+ q: query.q,
65
+ status: "active",
66
+ sort: [{ field: "displayName", order: "asc" }],
67
+ },
68
+ ["displayName", "username"],
69
+ context.actor,
70
+ );
71
+ const result: ModuleUserDirectoryPage = {
72
+ items: page.items.map((user) => ({
73
+ id: user.id,
74
+ displayName:
75
+ user.displayName?.trim() || user.username || "未命名用户",
76
+ })),
77
+ total: page.total,
78
+ page: page.page,
79
+ pageSize: page.pageSize,
80
+ };
81
+ await services.runtime.auditSink.recordCriticalEvent(
82
+ queryAuditEvent(context, result),
83
+ );
84
+ return result;
85
+ },
86
+ };
87
+ }
88
+
89
+ function normalizeQuery(
90
+ query: ModuleUserDirectoryQuery,
91
+ ): Required<Pick<ModuleUserDirectoryQuery, "page" | "pageSize">> &
92
+ Pick<ModuleUserDirectoryQuery, "q"> {
93
+ const page = positiveInteger(query.page ?? 1, "page", 10_000);
94
+ const pageSize = positiveInteger(query.pageSize ?? 20, "pageSize", 100);
95
+ if (query.q !== undefined && typeof query.q !== "string") {
96
+ invalidQuery("q 必须是字符串");
97
+ }
98
+ const q = query.q?.trim();
99
+ if (q && q.length > 100) {
100
+ invalidQuery("q 最多 100 个字符");
101
+ }
102
+ return { page, pageSize, ...(q ? { q } : {}) };
103
+ }
104
+
105
+ function positiveInteger(
106
+ value: number,
107
+ field: string,
108
+ maximum: number,
109
+ ): number {
110
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
111
+ invalidQuery(`${field} 必须是 1-${maximum} 的整数`);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ function invalidQuery(message: string): never {
117
+ throw new ModuleUserDirectoryError("USER_DIRECTORY_INVALID_QUERY", message);
118
+ }
119
+
120
+ function queryAuditEvent(
121
+ context: RequestGuardContext,
122
+ result: ModuleUserDirectoryPage,
123
+ ): AuditEvent {
124
+ return {
125
+ id: randomUUIDv7(),
126
+ action: auditAction,
127
+ outcome: "success",
128
+ actor: {
129
+ id: context.actor.id,
130
+ type: context.actor.type === "anonymous" ? "user" : context.actor.type,
131
+ name: context.actor.name,
132
+ },
133
+ target: { type: "system.user-directory", id: "enabled-users" },
134
+ requestId: context.requestId,
135
+ metadata: {
136
+ page: result.page,
137
+ pageSize: result.pageSize,
138
+ resultCount: result.items.length,
139
+ total: result.total,
140
+ },
141
+ occurredAt: new Date().toISOString(),
142
+ };
143
+ }
@@ -33,6 +33,10 @@ export {
33
33
  type ModuleStorageScope,
34
34
  type ModuleStorageServices,
35
35
  } from "./module-host/storage-port.js";
36
+ export {
37
+ createModuleUserDirectoryPort,
38
+ type ModuleUserDirectoryServices,
39
+ } from "./module-host/user-directory-port.js";
36
40
  // @windy-module system.scheduler begin
37
41
  export { createModuleTaskDefinitions } from "./module-host/task-definitions.js";
38
42
  // @windy-module system.scheduler end
@@ -25,6 +25,7 @@ export function developmentAdminActor(): RequestActor {
25
25
  "system.menu.read",
26
26
  "system.user.read",
27
27
  "system.user.manage",
28
+ "system.user-directory.read",
28
29
  "system.role.read",
29
30
  "system.role.manage",
30
31
  "system.department.read",
@@ -34,6 +34,7 @@ export function createBuiltInRoleSeeds(
34
34
  ...ordinaryPermissions,
35
35
  "system.department.read",
36
36
  "system.user.read",
37
+ "system.user-directory.read",
37
38
  ].sort();
38
39
  return [
39
40
  role(
@@ -64,6 +64,8 @@ services:
64
64
  environment:
65
65
  AGENT_SERVER_INTERNAL_TOKEN: ${AGENT_SERVER_INTERNAL_TOKEN:-}
66
66
  AGENT_RUN_STORE_PATH: /app/data/agent/runs.sqlite3
67
+ AI_PROVIDER_AUDIT_STORE_PATH: /app/data/agent/provider-audit.sqlite3
68
+ AI_PROVIDER_AUDIT_RETENTION_DAYS: ${AI_PROVIDER_AUDIT_RETENTION_DAYS:-90}
67
69
  QWEN_API_URL: ${QWEN_API_URL:-}
68
70
  QWEN_API_KEY: ${QWEN_API_KEY:-}
69
71
  QWEN_MODEL: ${QWEN_MODEL:-}
@@ -23,6 +23,27 @@ Operation 分别声明 Provider egress 与 Tool execution network policy:
23
23
  mode: "configured-endpoint",
24
24
  deploymentKey: "ai.openai-compatible",
25
25
  },
26
+ providerDataPolicy: {
27
+ inputFields: [
28
+ {
29
+ jsonPointer: "/question",
30
+ classification: "internal",
31
+ dlpAction: "redact",
32
+ },
33
+ ],
34
+ envelopeFields: [
35
+ {
36
+ field: "instructions",
37
+ classification: "internal",
38
+ dlpAction: "reject",
39
+ },
40
+ {
41
+ field: "user-prompt",
42
+ classification: "internal",
43
+ dlpAction: "redact",
44
+ },
45
+ ],
46
+ },
26
47
  toolNetworkPolicy: {
27
48
  mode: "forbidden",
28
49
  },
@@ -36,6 +57,14 @@ Operation 分别声明 Provider egress 与 Tool execution network policy:
36
57
  不能携带 URL、Host 或 Secret。
37
58
  - 当前 Tool 网络策略只支持 `forbidden`,缺省也是 `forbidden`。Provider egress 不会
38
59
  继承给 Tool。
60
+ - `inputFields` 是业务 raw input 的 JSON Pointer allowlist。Server 先选择并执行 DLP,
61
+ 再把最小输入交给业务 `prepare` Handler;未列字段不会进入 Handler、Agent Server
62
+ DTO 或 Provider 请求。
63
+ - `envelopeFields` 是 Provider outbound envelope 的语义字段 allowlist,当前包含
64
+ `instructions`、`user-prompt`、`tool-definitions`、`provider-output` 和
65
+ `tool-results`。每轮实际发送前重新检查,字段遗漏或未知角色 fail closed。
66
+ - `dlpAction=reject` 在命中后禁止发送;`redact` 使用固定占位符替换。内置检测覆盖
67
+ credential/token、Bearer、邮箱、手机号、身份证号和银行卡/长账号。
39
68
 
40
69
  ## Provider 强制执行
41
70
 
@@ -50,9 +79,25 @@ Key。Agent Server 在发出模型请求前核对:
50
79
  2. Deployment Key 必须等于当前 Adapter 的配置 Key;
51
80
  3. 启动时已验证配置 Endpoint 的 HTTPS Host allowlist;
52
81
  4. URL 仍来自 Agent Server 配置,Key 仍由 `SecretRef` 最后时刻解析。
82
+ 5. raw input 已按 JSON Pointer allowlist 最小化,outbound envelope 也命中显式字段规则;
83
+ 6. DLP 已完成且本次 Provider attempt 审计成功落库。
53
84
 
54
85
  浏览器 API、SSE、错误响应和 Web 环境均不包含 URL、Host allowlist 或 API Key。
55
86
 
87
+ ## Provider attempt 审计与留存
88
+
89
+ OpenAI-compatible Adapter 不启用隐式 HTTP retry。每次
90
+ `POST /chat/completions` 都对应唯一 `(runId, attempt)`,在打开网络请求前追加一条
91
+ Provider 发送审计;审计失败即禁止该次请求。记录只包含 Deployment、Operation、
92
+ Run、Attempt、分类集合、实际序列化 HTTP request body 字节数、策略决定和时间,不
93
+ 保存 prompt、body、Tool result、Provider response 或 Secret。
94
+
95
+ `AI_PROVIDER_AUDIT_STORE_PATH` 必须在配置 Provider 时存在;
96
+ `AI_PROVIDER_AUDIT_RETENTION_DAYS` 默认 90 天、范围 1 到 3650 天。记录在留存期内
97
+ append-only,只有 TTL 清理可以删除过期行,并保留最近/累计删除计数。需要内部 Token
98
+ 的 `GET /health/provider-governance` 只返回聚合计数与时间,不返回 Run ID 或内容。
99
+ 当前 SQLite Adapter 只支持单副本;共享多副本必须换成集中式 append-only Adapter。
100
+
56
101
  ## Tool 强制执行
57
102
 
58
103
  短时 Execution Grant 保存宿主校验过的 Operation。每次 Tool 回调重新读取 Grant,并
@@ -65,16 +110,13 @@ Key。Agent Server 在发出模型请求前核对:
65
110
 
66
111
  ## 尚未覆盖
67
112
 
68
- 本 patch 完成契约拆分、Provider 精确配置绑定、HTTPS Host allowlist 和拒绝回归,但
113
+ 当前实现完成契约拆分、Provider 精确配置绑定、字段 allowlist、DLP、逐 attempt 审计、
114
+ TTL 与聚合观测,但
69
115
  不把以下能力宣称为已完成:
70
116
 
71
- - 现有 `dataClassification` 是 Operation 级分类,尚未提供字段级数据分类、发送字段
72
- allowlist、脱敏或 DLP 检查;
73
- - 现有 Run 与 Tool 审计不等于逐次 Provider 发送审计;尚未记录目标 Deployment、
74
- 数据分类、字节数与策略决定的独立 append-only 事实;
75
117
  - 同进程 project-owned Tool Handler 不是 OS 网络沙箱。平台契约不授予网络 Port,
76
118
  但无法拦截业务代码直接调用全局 `fetch`;生产 deny-egress 仍需容器网络策略或防火墙;
77
119
  - DNS 解析钉扎、代理治理和多 Provider/多 Endpoint 动态路由尚未实现。
78
120
 
79
- 在这些能力补齐前,只能声明“Provider Tool 授权语义已隔离”,不能声明业务 Tool 已
80
- 获得进程级强制断网或完整数据出境治理。
121
+ 不能声明业务 Tool 已获得进程级强制断网。DLP 是保守的模式检测,不替代行业专用实体
122
+ 识别、人工审批或部署侧代理/防火墙。
@@ -272,13 +272,32 @@ interface ModuleAiOperationDefinition {
272
272
  mode: "forbidden" | "configured-endpoint";
273
273
  deploymentKey?: string;
274
274
  };
275
+ providerDataPolicy?: {
276
+ inputFields: readonly {
277
+ jsonPointer: string;
278
+ classification: "public" | "internal" | "sensitive";
279
+ dlpAction: "reject" | "redact";
280
+ }[];
281
+ envelopeFields: readonly {
282
+ field:
283
+ | "instructions"
284
+ | "user-prompt"
285
+ | "tool-definitions"
286
+ | "provider-output"
287
+ | "tool-results";
288
+ classification: "public" | "internal" | "sensitive";
289
+ dlpAction: "reject" | "redact";
290
+ }[];
291
+ };
275
292
  toolNetworkPolicy?: {
276
293
  mode: "forbidden";
277
294
  };
278
295
  }
279
296
  ```
280
297
 
281
- 两项缺省均 fail closed。Provider 的实际 Endpoint HTTPS Host allowlist 只存在于
298
+ Provider Tool 网络策略缺省均 fail closed。configured Provider 还必须声明字段
299
+ allowlist;业务 raw input 会在 Handler 前按 JSON Pointer 裁剪,outbound envelope 在
300
+ 每个 Provider attempt 前再次治理。Provider 的实际 Endpoint 与 HTTPS Host allowlist 只存在于
282
301
  服务端 Deployment 配置,不由业务 Manifest、浏览器或模型提供。详见
283
302
  [Agent Provider 与 Tool 网络边界](./ai-egress.md)。
284
303
 
@@ -24,3 +24,7 @@ Core 只理解 ownership dimension 的稳定 key/value 契约,不知道下游
24
24
  通用资源路由的 `dataAccess` 是必填判别联合:资源必须明确声明 `request` 并提供 scoped access,或声明 `platform`,不能因漏配可选字段静默回退裸 Repository。V1 异步 bulk handler 只接受显式 `platform` 资源,由服务端构造 nominal `all` context;需要继承申请人数据范围的异步资源必须先设计不可篡改的 context snapshot,不允许套用当前平台模式。工单运营快照同样使用服务端平台任务 Actor,不再暴露或调用裸 Repository。
25
25
 
26
26
  Search Provider 只能在 D1–D3 全部闭环后接入,不得先用 Provider 内存裁剪冒充 Repository 级强制。
27
+
28
+ 业务模块用户目录同样复用这条 Repository 强制链。V1 `tenantId=single-tenant` 是宿主
29
+ nominal `ScopeContext` 的明确固定租户,不是省略租户判断;业务调用方只提交分页与
30
+ 搜索,不能提交 Actor、tenant、department 或 scope。
@@ -2,7 +2,10 @@
2
2
 
3
3
  ## 模块与 seam
4
4
 
5
- `@southwind-ai/storage` 是平台文件能力的唯一底层模块。业务模块只学习 `BlobStore` 接口:流式写入、流式打开和删除;文件名、MIME、上传会话、Owner、权限与审计不进入这个底层接口,由上层上传模块统一处理。
5
+ `@southwind-ai/storage` 是平台文件能力的唯一底层模块。`BlobStore` 只供平台 Storage
6
+ 实现使用:流式写入、流式打开和删除;文件名、MIME、上传会话、Owner、权限与审计不
7
+ 进入这个底层接口,由上层上传模块统一处理。可安装业务模块只学习
8
+ `@southwind-ai/server-sdk` 的 `ModuleStoragePort`。
6
9
 
7
10
  `ArtifactRef` 使用 `blob_` 加 128 位随机值,不包含目录、租户或内容摘要,不能由业务主键推算。`ContentHash` 当前固定为 SHA-256。调用方可以声明大小和摘要,本地 Adapter 会在写入过程中验证;任何不一致都不会发布 Blob。
8
11
 
@@ -60,14 +63,29 @@ Logo 使用 `branding-logo` 上传策略。匿名 `/api/platform/files/:id` 只
60
63
 
61
64
  ## 模块受限 Upload/BlobRead Port
62
65
 
63
- 业务模块不直接使用 `UploadService` / `FileService`,而是经 `ModuleRequestContext.storage` 获得 `ModuleStoragePort`(宿主实现见 `apps/server/src/module-host/storage-port.ts`):
66
+ 业务模块不直接使用 `UploadService` / `FileService`,而是经
67
+ `ModuleRequestContext.storage` 或 `ModuleToolContext.storage` 获得
68
+ `ModuleStoragePort`(宿主实现见 `apps/server/src/module-host/storage-port.ts`):
64
69
 
65
70
  - `createUploadSession` 的 purpose 强制属于本模块经 Host 注册的 Upload Policy;跨模块用途和平台基础用途(如 `bulk-data`、`branding-logo`)一律以 `UPLOAD_FORBIDDEN` 拒绝。Owner 由宿主以当前 Actor 补齐,模块无法代他人创建会话。
66
71
  - `openFile` 走 `FileService.open` 的 Owner 约束,并在返回前校验文件用途属于本模块——即使是本人文件,用途越界也拒绝。
67
72
  - `openPublicFile` 走 `FileService.openPublic` 的 Purpose 约束,且只允许本模块已注册用途。
73
+ - `openResourceFile` 用于同一业务范围内的跨上传者读取。模块必须先通过
74
+ `registerResourceFileAccessPolicies()` 为精确 `purpose + resourceType` 注册
75
+ project-owned 授权 Resolver,并绑定同 Manifest 的 Feature、Permission 与 Audit
76
+ Action。Host 每次调用都使用当前请求或 Tool grant 的 Actor 与服务端 ScopeContext
77
+ 重跑 Guard;Resolver 必须从业务 Repository 回查 `resourceId -> fileId` 绑定和案件/
78
+ 项目成员资格,客户端传入的 ID 不是授权证据。
79
+ - 资源读取只返回 `id/purpose/filename/mimeType/byteSize/sha256/body`;不会返回
80
+ Owner、ArtifactRef、对象存储 Key 或 Repository。成功和拒绝都写稳定审计,仅记录
81
+ `resourceType/resourceId/fileId/purpose`,不记录文件名、路径或内容。
68
82
  - 模块不直接接触 `BlobStore` 或 Upload Repository;宿主未挂载存储服务时,任何访问都以明确错误失败,不静默绕过。
69
83
 
70
84
  用途唯一性仍在 Host 注册时全局校验:命名空间重叠的模块(如 `shop` 与 `shop.sub`)注册同一 purpose 会在启动时失败。
85
+ 资源策略的 purpose 必须由当前模块或其 Manifest 显式依赖模块的 Upload Policy 注册;
86
+ 这允许 `media -> evidence` 之类的明确消费者关系,但拒绝平台基础用途和未声明的跨模块
87
+ 读取。Feature、Permission、Audit Action 和 resourceType 仍必须属于当前消费者模块,
88
+ 任一未知或越界都会在监听前失败。
71
89
 
72
90
  ## 当前切片
73
91
 
@@ -75,3 +93,5 @@ Logo 使用 `branding-logo` 上传策略。匿名 `/api/platform/files/:id` 只
75
93
  - F2:上传会话、分片续传、文件元数据、校验和清理已完成。
76
94
  - F3:服务端授权下载、审计、bulk artifact 与 Logo 接入已完成。
77
95
  - F4:模块受限 Upload/BlobRead Port 已完成,业务模块经 Host 注册用途并受命名空间约束。
96
+ - F5:Route 与 Tool 共用资源绑定读取 Port,支持受审计、受数据范围约束的跨上传者
97
+ 业务文件流读取,不扩散 Storage/Admin Repository。
@@ -78,9 +78,9 @@ composable。
78
78
  先提交当前业务改动并保持 Git 工作区干净,然后执行:
79
79
 
80
80
  ```bash
81
- bunx --bun create-windy@0.2.33 update --check --cwd .
82
- bunx --bun create-windy@0.2.33 update --dry-run --cwd .
83
- bunx --bun create-windy@0.2.33 update --cwd .
81
+ bunx --bun create-windy@0.2.34 update --check --cwd .
82
+ bunx --bun create-windy@0.2.34 update --dry-run --cwd .
83
+ bunx --bun create-windy@0.2.34 update --cwd .
84
84
  bun install
85
85
  ```
86
86