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,305 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { featureKey } from "@southwind-ai/shared";
4
+ import {
5
+ FileService,
6
+ InMemoryUploadRepository,
7
+ MemoryBlobStore,
8
+ UploadService,
9
+ } from "@southwind-ai/storage";
10
+ import { buildActorScopeContexts } from "../data-access/context.js";
11
+ import type { RequestActor, RequestGuardContext } from "../guards.js";
12
+ import { createServerRuntime } from "../runtime.js";
13
+ import { initializeModuleHosts } from "./initialize.js";
14
+ import type { ModuleStorageServices } from "./storage-port.js";
15
+ import { createModuleToolHandlerMap } from "./tool-handlers.js";
16
+
17
+ describe("业务 Tool Storage Port 组合", () => {
18
+ test("每次执行使用当前 Actor/Scope 重验资源绑定并允许跨上传者读取", async () => {
19
+ const declared = toolManifest();
20
+ const evidence = evidenceManifest();
21
+ const hosts = await initializeModuleHosts({
22
+ modules: [evidence, declared],
23
+ installations: [
24
+ {
25
+ manifest: evidence,
26
+ setup(host) {
27
+ host.registerUploadPolicy(uploadPolicy());
28
+ },
29
+ },
30
+ {
31
+ manifest: declared,
32
+ setup(host) {
33
+ host.registerResourceFileAccessPolicies([
34
+ {
35
+ purpose: "evidence.original",
36
+ resourceType: "media.evidence-file",
37
+ featureKey: "media.catalog",
38
+ permissionKey: "media.read",
39
+ auditAction: "media.file.read",
40
+ authorize: ({ fileId, resourceId }, context) =>
41
+ fileId === linkedFileId &&
42
+ resourceId === "evidence_file_1" &&
43
+ context.actor.id === "user_reader",
44
+ },
45
+ ]);
46
+ host.registerToolHandlers([
47
+ {
48
+ toolKey: "media.metadata.catalog",
49
+ scopeMode: "request",
50
+ async execute(input, context) {
51
+ const file = await context.storage.openResourceFile({
52
+ fileId: String(input.fileId),
53
+ purpose: "evidence.original",
54
+ resourceType: "media.evidence-file",
55
+ resourceId: String(input.resourceId),
56
+ });
57
+ return { fileId: file.id, sha256: file.sha256 };
58
+ },
59
+ },
60
+ ]);
61
+ },
62
+ },
63
+ ],
64
+ });
65
+ const storage = createTestStorage(hosts.uploadPolicies);
66
+ const seeded = await seed(storage);
67
+ linkedFileId = seeded.id;
68
+ const runtime = createServerRuntime();
69
+ const handlers = createModuleToolHandlerMap(hosts, {
70
+ runtime,
71
+ features: declared.features,
72
+ storage,
73
+ });
74
+ const handler = handlers.get("media.metadata.catalog");
75
+ if (!handler) throw new Error("缺少 Tool Handler");
76
+
77
+ await expect(
78
+ handler.execute(
79
+ { fileId: seeded.id, resourceId: "evidence_file_1" },
80
+ executionContext(actor("user_reader")),
81
+ ),
82
+ ).resolves.toEqual({
83
+ fileId: seeded.id,
84
+ sha256: seeded.contentHash.value,
85
+ });
86
+ expect(runtime.auditSink.list()).toContainEqual(
87
+ expect.objectContaining({
88
+ action: "media.file.read",
89
+ outcome: "success",
90
+ target: {
91
+ type: "media.evidence-file",
92
+ id: "evidence_file_1",
93
+ },
94
+ metadata: {
95
+ fileId: seeded.id,
96
+ purpose: "evidence.original",
97
+ },
98
+ }),
99
+ );
100
+
101
+ await expect(
102
+ handler.execute(
103
+ { fileId: seeded.id, resourceId: "evidence_file_1" },
104
+ executionContext(actor("user_other")),
105
+ ),
106
+ ).rejects.toThrow("资源文件访问未授权");
107
+
108
+ const invalidScopeActor = actor("user_reader");
109
+ delete invalidScopeActor.scopeContext;
110
+ await expect(
111
+ handler.execute(
112
+ { fileId: seeded.id, resourceId: "evidence_file_1" },
113
+ executionContext(invalidScopeActor),
114
+ ),
115
+ ).rejects.toThrow("资源文件访问未授权");
116
+ });
117
+
118
+ test("未挂载 Storage 时 Tool Port 明确 fail-closed", async () => {
119
+ const declared = toolManifest();
120
+ const hosts = await initializeModuleHosts({
121
+ modules: [declared],
122
+ installations: [
123
+ {
124
+ manifest: declared,
125
+ setup(host) {
126
+ host.registerToolHandlers([
127
+ {
128
+ toolKey: "media.metadata.catalog",
129
+ scopeMode: "request",
130
+ async execute(_input, context) {
131
+ await context.storage.openResourceFile({} as never);
132
+ return {};
133
+ },
134
+ },
135
+ ]);
136
+ },
137
+ },
138
+ ],
139
+ });
140
+ const handler = createModuleToolHandlerMap(hosts, {
141
+ runtime: createServerRuntime(),
142
+ features: declared.features,
143
+ }).get("media.metadata.catalog");
144
+ if (!handler) throw new Error("缺少 Tool Handler");
145
+
146
+ await expect(
147
+ handler.execute({}, executionContext(actor("user_reader"))),
148
+ ).rejects.toThrow("模块 media 的存储 Port 未挂载");
149
+ });
150
+ });
151
+
152
+ let linkedFileId = "";
153
+
154
+ function actor(id: string): RequestActor {
155
+ return {
156
+ id,
157
+ type: "user",
158
+ name: id,
159
+ roleCodes: ["analyst"],
160
+ permissionKeys: ["media.read"],
161
+ ...buildActorScopeContexts(id, undefined, [
162
+ {
163
+ id: "role_analyst",
164
+ code: "analyst",
165
+ dataScope: "self",
166
+ departmentIds: [],
167
+ },
168
+ ]),
169
+ };
170
+ }
171
+
172
+ function executionContext(currentActor: RequestActor) {
173
+ const guardContext: RequestGuardContext = {
174
+ actor: currentActor,
175
+ requestId: `request_${currentActor.id}`,
176
+ startedAt: new Date().toISOString(),
177
+ };
178
+ return { guardContext, signal: new AbortController().signal };
179
+ }
180
+
181
+ function toolManifest(): ModuleManifest {
182
+ return {
183
+ name: "media",
184
+ version: "0.1.0",
185
+ label: "媒体",
186
+ description: "媒体测试模块",
187
+ dependencies: ["evidence"],
188
+ menus: [],
189
+ adminRoutes: [],
190
+ permissions: [
191
+ {
192
+ key: "media.read",
193
+ label: "读取媒体",
194
+ module: "media",
195
+ moduleLabel: "媒体",
196
+ resource: "file",
197
+ resourceLabel: "文件",
198
+ action: "read",
199
+ },
200
+ ],
201
+ apiPermissions: [],
202
+ features: [
203
+ {
204
+ key: featureKey("media.catalog"),
205
+ label: "媒体目录",
206
+ module: "media",
207
+ visible: "visible",
208
+ enabled: true,
209
+ stage: "stable",
210
+ },
211
+ ],
212
+ migrations: [],
213
+ schemaExports: [],
214
+ tasks: [],
215
+ tools: [
216
+ {
217
+ key: "media.metadata.catalog",
218
+ label: "媒体元数据",
219
+ description: "读取案件范围内媒体元数据",
220
+ inputSchemaKey: "media.metadata.input",
221
+ outputSchemaKey: "media.metadata.output",
222
+ operation: "background",
223
+ permissionKey: "media.read",
224
+ featureKey: "media.catalog",
225
+ auditAction: "media.file.read",
226
+ scopeMode: "request",
227
+ dataClassification: "sensitive",
228
+ idempotency: "supported",
229
+ approval: "none",
230
+ limits: {
231
+ maxDurationSeconds: 60,
232
+ maxInputBytes: 1024,
233
+ maxOutputBytes: 1024,
234
+ },
235
+ },
236
+ ],
237
+ providers: [],
238
+ auditActions: ["media.file.read"],
239
+ };
240
+ }
241
+
242
+ function evidenceManifest(): ModuleManifest {
243
+ return {
244
+ name: "evidence",
245
+ version: "0.1.0",
246
+ label: "物证",
247
+ description: "物证原件模块",
248
+ menus: [],
249
+ adminRoutes: [],
250
+ permissions: [],
251
+ apiPermissions: [],
252
+ features: [],
253
+ migrations: [],
254
+ schemaExports: [],
255
+ tasks: [],
256
+ tools: [],
257
+ providers: [],
258
+ auditActions: [],
259
+ };
260
+ }
261
+
262
+ function uploadPolicy() {
263
+ return {
264
+ purpose: "evidence.original",
265
+ maxByteSize: 1024,
266
+ maxPartByteSize: 512,
267
+ sessionTtlMs: 60_000,
268
+ allowedTypes: { "image/jpeg": ["jpg"] },
269
+ signature: "none" as const,
270
+ };
271
+ }
272
+
273
+ function createTestStorage(
274
+ policies: ConstructorParameters<typeof UploadService>[0]["policies"],
275
+ ): ModuleStorageServices {
276
+ const repository = new InMemoryUploadRepository();
277
+ const blobStore = new MemoryBlobStore();
278
+ return {
279
+ uploads: new UploadService({ repository, blobStore, policies }),
280
+ files: new FileService(repository, blobStore),
281
+ };
282
+ }
283
+
284
+ async function seed(storage: ModuleStorageServices) {
285
+ const data = new TextEncoder().encode("abc");
286
+ const sha256 = new Bun.CryptoHasher("sha256").update(data).digest("hex");
287
+ const session = await storage.uploads.create({
288
+ ownerId: "user_uploader",
289
+ purpose: "evidence.original",
290
+ filename: "photo.jpg",
291
+ mimeType: "image/jpeg",
292
+ expectedByteSize: data.byteLength,
293
+ expectedSha256: sha256,
294
+ });
295
+ await storage.uploads.appendPart({
296
+ sessionId: session.id,
297
+ ownerId: "user_uploader",
298
+ partNumber: 0,
299
+ byteOffset: 0,
300
+ byteSize: data.byteLength,
301
+ sha256,
302
+ body: new Blob([data]).stream(),
303
+ });
304
+ return storage.uploads.complete(session.id, "user_uploader");
305
+ }
@@ -1,6 +1,22 @@
1
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
1
2
  import type { ModuleToolHandlers } from "../agent/tool-registry.js";
3
+ import type { createServerRuntime } from "../runtime.js";
2
4
  import type { InitializedModuleHosts } from "./initialize.js";
3
5
  import { toModuleActor } from "./request-context.js";
6
+ import {
7
+ createModuleStoragePort,
8
+ type ModuleStorageServices,
9
+ unavailableModuleStoragePort,
10
+ } from "./storage-port.js";
11
+ import { createModuleStorageScope } from "./storage-scope.js";
12
+
13
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
14
+
15
+ export interface ModuleToolHandlerMapContext {
16
+ readonly runtime: ServerRuntime;
17
+ readonly features: readonly FeatureFlagDefinition[];
18
+ readonly storage?: ModuleStorageServices;
19
+ }
4
20
 
5
21
  /**
6
22
  * 把模块经 Host 注册的 Agent Tool Handler 适配为 Agent 路由层的
@@ -8,18 +24,36 @@ import { toModuleActor } from "./request-context.js";
8
24
  */
9
25
  export function createModuleToolHandlerMap(
10
26
  hosts: InitializedModuleHosts,
27
+ hostContext: ModuleToolHandlerMapContext,
11
28
  ): ModuleToolHandlers {
12
29
  return new Map(
13
- hosts.toolHandlers.map(({ registration }) => [
30
+ hosts.toolHandlers.map(({ moduleName, registration }) => [
14
31
  registration.toolKey,
15
32
  {
16
33
  scopeMode: registration.scopeMode,
17
- execute: (input, context) =>
18
- registration.execute(input, {
34
+ execute: (input, context) => {
35
+ const storage = hostContext.storage
36
+ ? createModuleStoragePort(
37
+ hostContext.storage,
38
+ createModuleStorageScope({
39
+ moduleName,
40
+ purposes: hosts.uploadPurposesOf(moduleName),
41
+ resourceFileAccessPolicies:
42
+ hosts.resourceFileAccessPoliciesOf(moduleName),
43
+ guardContext: context.guardContext,
44
+ runtime: hostContext.runtime,
45
+ features: hostContext.features,
46
+ signal: context.signal,
47
+ }),
48
+ )
49
+ : unavailableModuleStoragePort(moduleName);
50
+ return registration.execute(input, {
19
51
  actor: toModuleActor(context.guardContext.actor),
20
52
  requestId: context.guardContext.requestId,
21
53
  signal: context.signal,
22
- }),
54
+ storage,
55
+ });
56
+ },
23
57
  },
24
58
  ]),
25
59
  );
@@ -0,0 +1,126 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { featureKey } from "@southwind-ai/shared";
4
+ import { buildScopeContext } from "../data-access/context.js";
5
+ import { createFoundationSnapshot } from "../foundation.js";
6
+ import { createServerRuntime } from "../runtime.js";
7
+ import { FakeRouteApp } from "../system/route-test-app.js";
8
+ import { createSystemRepositories } from "../system/seed.js";
9
+ import { initializeModuleHosts } from "./initialize.js";
10
+ import { installModuleRoutes } from "./route-installer.js";
11
+
12
+ test("Composition Root 向业务 Handler 注入用户目录 Port", async () => {
13
+ const manifest = businessManifest();
14
+ const foundation = createFoundationSnapshot();
15
+ const repositories = createSystemRepositories(foundation);
16
+ const hosts = await initializeModuleHosts({
17
+ modules: [manifest],
18
+ installations: [
19
+ {
20
+ manifest,
21
+ setup(host) {
22
+ host.registerRoutes([
23
+ {
24
+ method: "GET",
25
+ path: "/api/case/members",
26
+ permissionKey: "case.member.read",
27
+ handler: (context) => context.userDirectory.list(),
28
+ },
29
+ ]);
30
+ },
31
+ },
32
+ ],
33
+ });
34
+ const runtime = createServerRuntime(
35
+ {},
36
+ {
37
+ featureCatalog: [...manifest.features, ...foundation.features],
38
+ featureResolver: {
39
+ resolve: (key) => ({ key, enabled: true, visible: "visible" }),
40
+ },
41
+ },
42
+ );
43
+ const app = new FakeRouteApp();
44
+ installModuleRoutes(hosts, app, {
45
+ runtime,
46
+ features: [...manifest.features, ...foundation.features],
47
+ userDirectory: { repositories },
48
+ });
49
+
50
+ const result = await app.invoke("GET", "/case/members", {
51
+ guardContext: {
52
+ requestId: "request-composition",
53
+ actor: {
54
+ id: "case_admin",
55
+ type: "user",
56
+ name: "案件管理员",
57
+ roleCodes: ["case-admin"],
58
+ permissionKeys: ["case.member.read", "system.user-directory.read"],
59
+ scopeContext: buildScopeContext("case_admin", "dept_platform", [
60
+ {
61
+ id: "role-case-admin",
62
+ code: "case-admin",
63
+ dataScope: "all",
64
+ departmentIds: [],
65
+ },
66
+ ]),
67
+ },
68
+ startedAt: "2026-07-25T00:00:00.000Z",
69
+ },
70
+ params: {},
71
+ query: {},
72
+ });
73
+
74
+ expect(result).toEqual({
75
+ items: [{ id: "user_dev_admin", displayName: "开发管理员" }],
76
+ total: 1,
77
+ page: 1,
78
+ pageSize: 20,
79
+ });
80
+ });
81
+
82
+ function businessManifest(): ModuleManifest {
83
+ return {
84
+ name: "case",
85
+ version: "0.1.0",
86
+ label: "案件",
87
+ description: "用户目录组合测试",
88
+ menus: [],
89
+ adminRoutes: [],
90
+ permissions: [
91
+ {
92
+ key: "case.member.read",
93
+ label: "查看成员",
94
+ module: "case",
95
+ moduleLabel: "案件",
96
+ resource: "member",
97
+ resourceLabel: "案件成员",
98
+ action: "read",
99
+ },
100
+ ],
101
+ apiPermissions: [
102
+ {
103
+ method: "GET",
104
+ path: "/api/case/members",
105
+ permissionKey: "case.member.read",
106
+ featureKey: "case.member",
107
+ },
108
+ ],
109
+ features: [
110
+ {
111
+ key: featureKey("case.member"),
112
+ label: "案件成员",
113
+ module: "case",
114
+ visible: "visible",
115
+ enabled: true,
116
+ stage: "stable",
117
+ },
118
+ ],
119
+ migrations: [],
120
+ schemaExports: [],
121
+ tasks: [],
122
+ tools: [],
123
+ providers: [],
124
+ auditActions: [],
125
+ };
126
+ }