create-windy 0.2.34 → 0.3.1

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 (261) hide show
  1. package/README.md +26 -3
  2. package/dist/cli.js +8121 -3746
  3. package/package.json +3 -3
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +5 -0
  6. package/template/README.md +28 -4
  7. package/template/_gitignore +4 -0
  8. package/template/apps/agent-server/Dockerfile +1 -0
  9. package/template/apps/agent-server/README.md +18 -5
  10. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  12. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  13. package/template/apps/agent-server/pyproject.toml +1 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  19. package/template/apps/agent-server/tests/__init__.py +1 -0
  20. package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
  21. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  22. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  23. package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
  24. package/template/apps/server/package.json +4 -1
  25. package/template/apps/server/src/agent/run-facade.ts +2 -7
  26. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  27. package/template/apps/server/src/application-services.ts +50 -0
  28. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  29. package/template/apps/server/src/audit/search-summary.ts +2 -6
  30. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  31. package/template/apps/server/src/auth/password.ts +30 -21
  32. package/template/apps/server/src/auth/routes.test-support.ts +94 -0
  33. package/template/apps/server/src/auth/routes.test.ts +51 -78
  34. package/template/apps/server/src/auth/routes.ts +6 -1
  35. package/template/apps/server/src/auth/runtime.ts +4 -1
  36. package/template/apps/server/src/auth/service.ts +25 -7
  37. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  38. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  39. package/template/apps/server/src/configuration/definition.ts +242 -12
  40. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  41. package/template/apps/server/src/configuration/repository.ts +27 -2
  42. package/template/apps/server/src/configuration/routes.ts +4 -1
  43. package/template/apps/server/src/configuration/service.test.ts +100 -0
  44. package/template/apps/server/src/configuration/service.ts +50 -10
  45. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  46. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  47. package/template/apps/server/src/example-modules.ts +4 -1
  48. package/template/apps/server/src/guards.ts +18 -5
  49. package/template/apps/server/src/http/request-source.test.ts +85 -0
  50. package/template/apps/server/src/http/request-source.ts +90 -0
  51. package/template/apps/server/src/index.ts +40 -40
  52. package/template/apps/server/src/license/routes.ts +2 -0
  53. package/template/apps/server/src/license/runtime-service.ts +10 -11
  54. package/template/apps/server/src/mcp/adapter.test.ts +3 -2
  55. package/template/apps/server/src/mcp/adapter.ts +9 -9
  56. package/template/apps/server/src/mcp/routes.ts +4 -3
  57. package/template/apps/server/src/module-bootstrap.ts +2 -2
  58. package/template/apps/server/src/module-host/request-context.ts +6 -6
  59. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  60. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  61. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  62. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  63. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  64. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  65. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  66. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  67. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  68. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  69. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  70. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  71. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  72. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  73. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  74. package/template/apps/server/src/notification/memory-support.ts +72 -0
  75. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  76. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  77. package/template/apps/server/src/notification/repository.test.ts +171 -0
  78. package/template/apps/server/src/notification/repository.ts +179 -32
  79. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  80. package/template/apps/server/src/notification/route-support.ts +225 -0
  81. package/template/apps/server/src/notification/routes.test.ts +182 -0
  82. package/template/apps/server/src/notification/routes.ts +225 -132
  83. package/template/apps/server/src/notification/service.test.ts +104 -0
  84. package/template/apps/server/src/notification/service.ts +140 -0
  85. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  86. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  87. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  88. package/template/apps/server/src/platform-http-runtime.ts +87 -0
  89. package/template/apps/server/src/route-guards.ts +1 -0
  90. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  91. package/template/apps/server/src/runtime-development.ts +1 -0
  92. package/template/apps/server/src/runtime.test.ts +23 -9
  93. package/template/apps/server/src/runtime.ts +39 -2
  94. package/template/apps/server/src/scheduler/audit.ts +2 -11
  95. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  96. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  97. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  98. package/template/apps/server/src/scheduler/routes.ts +2 -7
  99. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  100. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  101. package/template/apps/server/src/search/service.ts +2 -6
  102. package/template/apps/server/src/settings/routes.ts +15 -3
  103. package/template/apps/server/src/settings/runtime.ts +10 -1
  104. package/template/apps/server/src/storage/runtime.ts +13 -0
  105. package/template/apps/server/src/system/audit-query.ts +33 -4
  106. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  107. package/template/apps/server/src/system/audit-routes.ts +6 -5
  108. package/template/apps/server/src/system/department-organization.ts +320 -0
  109. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  110. package/template/apps/server/src/system/department-routes.ts +114 -33
  111. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  112. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  113. package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
  114. package/template/apps/server/src/system/drizzle-system.ts +59 -311
  115. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  116. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  117. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  118. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  119. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  120. package/template/apps/server/src/system/resource-routes.ts +270 -0
  121. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  122. package/template/apps/server/src/system/route-helpers.ts +2 -5
  123. package/template/apps/server/src/system/route-types.ts +12 -3
  124. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  125. package/template/apps/server/src/system/routes.test.ts +10 -2
  126. package/template/apps/server/src/system/routes.ts +35 -267
  127. package/template/apps/server/src/system/seed.ts +7 -0
  128. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  129. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  130. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  131. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  132. package/template/apps/server/src/work-order/service.ts +42 -10
  133. package/template/apps/web/src/app/error-pages.ts +9 -0
  134. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  135. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  136. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  137. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  138. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  139. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  140. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  141. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  142. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  143. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  144. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  145. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  146. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  147. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  148. package/template/apps/web/src/lib/date-time.ts +22 -10
  149. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  150. package/template/apps/web/src/main.ts +14 -1
  151. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  152. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  153. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  154. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  155. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  156. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  157. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  158. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  159. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  160. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  161. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  162. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  163. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  164. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  165. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  166. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  167. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  168. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  169. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  170. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  171. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  172. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  173. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  174. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  175. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
  176. package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
  177. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +96 -108
  178. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  179. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  180. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  181. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  182. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  183. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  184. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  185. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  186. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  187. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  188. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  189. package/template/apps/web/src/router/error-navigation.ts +50 -0
  190. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  191. package/template/apps/web/src/router/index.ts +71 -0
  192. package/template/apps/web/src/router/index.webtest.ts +29 -0
  193. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  194. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  195. package/template/apps/web/src/services/auth-api.ts +5 -0
  196. package/template/apps/web/src/services/http.ts +43 -6
  197. package/template/apps/web/src/services/http.webtest.ts +70 -0
  198. package/template/apps/web/src/services/notification-api.ts +85 -2
  199. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  200. package/template/apps/web/src/services/search-api.ts +39 -0
  201. package/template/apps/web/src/services/system-api.ts +18 -0
  202. package/template/apps/web/vitest.config.ts +1 -0
  203. package/template/docker-compose.yml +66 -0
  204. package/template/docs/architecture/ai-runtime.md +14 -5
  205. package/template/docs/architecture/mcp-business-capabilities.md +107 -0
  206. package/template/docs/architecture/search-provider-protocol.md +38 -2
  207. package/template/docs/development/custom-error-pages.md +78 -0
  208. package/template/docs/development/custom-login-page.md +3 -3
  209. package/template/docs/platform/agent-runtime.md +32 -6
  210. package/template/docs/platform/agent-tools.md +4 -0
  211. package/template/docs/platform/audit-reliability.md +46 -2
  212. package/template/docs/platform/module-configuration.md +13 -4
  213. package/template/docs/platform/notifications.md +67 -11
  214. package/template/docs/platform/organization-membership.md +17 -3
  215. package/template/docs/platform/system-crud-api.md +16 -0
  216. package/template/docs/platform/web-system-crud.md +6 -2
  217. package/template/package.json +4 -0
  218. package/template/packages/config/index.test.ts +45 -0
  219. package/template/packages/config/package.json +1 -1
  220. package/template/packages/config/src/platform.ts +169 -48
  221. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  222. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  223. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  224. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  225. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  226. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  227. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  228. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  229. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  230. package/template/packages/database/package.json +1 -1
  231. package/template/packages/database/src/schema/identity.ts +7 -0
  232. package/template/packages/database/src/schema/notifications.ts +135 -0
  233. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  234. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  235. package/template/packages/modules/package.json +1 -1
  236. package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
  237. package/template/packages/modules/src/composition.test.ts +5 -158
  238. package/template/packages/modules/src/system-api-operational.ts +234 -0
  239. package/template/packages/modules/src/system-api-permissions.ts +14 -181
  240. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  241. package/template/packages/modules/src/system-features.ts +1 -1
  242. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  243. package/template/packages/modules/src/system-module-tasks.ts +86 -0
  244. package/template/packages/modules/src/system-modules-access.test.ts +60 -0
  245. package/template/packages/modules/src/system-modules.test.ts +13 -54
  246. package/template/packages/modules/src/system-modules.ts +2 -72
  247. package/template/packages/modules/src/system-permissions.ts +9 -5
  248. package/template/packages/server-sdk/package.json +1 -1
  249. package/template/packages/shared/index.ts +1 -0
  250. package/template/packages/shared/package.json +1 -1
  251. package/template/packages/shared/src/audit.ts +25 -0
  252. package/template/packages/shared/src/mcp.test.ts +12 -0
  253. package/template/packages/shared/src/mcp.ts +7 -0
  254. package/template/packages/shared/src/module-configuration.ts +5 -0
  255. package/template/packages/shared/src/notification.ts +92 -1
  256. package/template/packages/shared/src/password.ts +7 -2
  257. package/template/packages/shared/src/platform-settings.ts +2 -0
  258. package/template/packages/storage/package.json +1 -1
  259. package/template/packages/storage/src/upload-service.test.ts +16 -0
  260. package/template/packages/storage/src/upload-service.ts +5 -1
  261. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import tempfile
6
+ from collections.abc import Callable, Coroutine
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from southwind_agent_server.adk_adapter import AdkEngineAdapter
12
+ from southwind_agent_server.config import (
13
+ EnvironmentSecretResolver,
14
+ OpenAiCompatibleConfig,
15
+ SecretRef,
16
+ )
17
+ from southwind_agent_server.contracts import (
18
+ ProviderDataPolicy,
19
+ ProviderEgress,
20
+ ProviderFieldPolicy,
21
+ ProviderInputFieldPolicy,
22
+ RuntimeLimits,
23
+ RuntimeToolDefinition,
24
+ ToolNetworkPolicy,
25
+ )
26
+ from southwind_agent_server.engine import (
27
+ EngineCompleted,
28
+ EngineRunRequest,
29
+ OutputDelta,
30
+ SafeEngineError,
31
+ ToolCompleted,
32
+ ToolRequested,
33
+ UsageReported,
34
+ )
35
+ from southwind_agent_server.openai_provider import OpenAiCompatibleProvider
36
+ from southwind_agent_server.provider_audit import ProviderAuditStore
37
+ from southwind_agent_server.provider_governance import ProviderRequestGovernance
38
+ from southwind_agent_server.tool_host import ToolHostClient
39
+
40
+
41
+ async def test_adk_runner_maps_text_usage_and_terminal() -> None:
42
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
43
+ return sse_response(
44
+ [
45
+ {"choices": [{"delta": {"content": "你"}}]},
46
+ {
47
+ "choices": [{"delta": {"content": "好"}}],
48
+ "usage": {
49
+ "prompt_tokens": 2,
50
+ "completion_tokens": 2,
51
+ "total_tokens": 4,
52
+ },
53
+ },
54
+ ]
55
+ )
56
+
57
+ signals = [
58
+ signal async for signal in engine(provider_handler).execute(runtime_request())
59
+ ]
60
+
61
+ assert [signal.text for signal in signals if isinstance(signal, OutputDelta)] == [
62
+ "你",
63
+ "好",
64
+ ]
65
+ usage = [signal.usage for signal in signals if isinstance(signal, UsageReported)]
66
+ assert usage == [
67
+ {
68
+ "source": "provider-reported",
69
+ "inputTokens": 2,
70
+ "outputTokens": 2,
71
+ "totalTokens": 4,
72
+ }
73
+ ]
74
+ assert isinstance(signals[-1], EngineCompleted)
75
+ assert signals[-1].output == {"text": "你好"}
76
+
77
+
78
+ async def test_adk_tool_events_execute_only_through_tool_host() -> None:
79
+ provider_calls = 0
80
+ tool_requests: list[dict[str, object]] = []
81
+
82
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
83
+ nonlocal provider_calls
84
+ provider_calls += 1
85
+ if provider_calls == 1:
86
+ return tool_call_response("business.records.list")
87
+ return sse_response([{"choices": [{"delta": {"content": "完成"}}]}])
88
+
89
+ async def tool_handler(request: httpx.Request) -> httpx.Response:
90
+ tool_requests.append(json.loads(request.content))
91
+ return httpx.Response(200, json={"value": {"total": 1}})
92
+
93
+ signals = [
94
+ signal
95
+ async for signal in engine(provider_handler, tool_handler).execute(
96
+ runtime_request("business.records.list")
97
+ )
98
+ ]
99
+
100
+ assert any(isinstance(signal, ToolRequested) for signal in signals)
101
+ assert any(isinstance(signal, ToolCompleted) for signal in signals)
102
+ assert tool_requests == [
103
+ {
104
+ "executionGrant": "grant_1",
105
+ "runId": "run_1",
106
+ "toolCallId": "call_1",
107
+ "toolKey": "business.records.list",
108
+ "arguments": {"page": 2},
109
+ }
110
+ ]
111
+ assert signals[-1].output == {"text": "完成"} # type: ignore[union-attr]
112
+
113
+
114
+ async def test_adk_denies_unknown_tool_before_host_callback() -> None:
115
+ host_called = False
116
+
117
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
118
+ return tool_call_response("business.admin.delete")
119
+
120
+ async def tool_handler(_request: httpx.Request) -> httpx.Response:
121
+ nonlocal host_called
122
+ host_called = True
123
+ return httpx.Response(200, json={"value": None})
124
+
125
+ with pytest.raises(SafeEngineError) as captured:
126
+ async for _ in engine(provider_handler, tool_handler).execute(
127
+ runtime_request("business.records.list")
128
+ ):
129
+ pass
130
+
131
+ assert captured.value.code == "TOOL_DENIED"
132
+ assert host_called is False
133
+
134
+
135
+ async def test_adk_governance_rejects_before_provider_network() -> None:
136
+ provider_called = False
137
+
138
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
139
+ nonlocal provider_called
140
+ provider_called = True
141
+ return sse_response([])
142
+
143
+ request = runtime_request()
144
+ request = EngineRunRequest(
145
+ **{
146
+ **request.__dict__,
147
+ "instructions": "password=server-secret-value",
148
+ }
149
+ )
150
+
151
+ with pytest.raises(SafeEngineError) as captured:
152
+ async for _ in engine(provider_handler).execute(request):
153
+ pass
154
+
155
+ assert captured.value.code == "FORBIDDEN"
156
+ assert provider_called is False
157
+
158
+
159
+ async def test_adk_cancel_and_provider_error_map_to_safe_errors() -> None:
160
+ started = asyncio.Event()
161
+
162
+ async def slow_provider(_request: httpx.Request) -> httpx.Response:
163
+ started.set()
164
+ await asyncio.sleep(0.03)
165
+ return sse_response([{"choices": [{"delta": {"content": "late"}}]}])
166
+
167
+ adapter = engine(slow_provider)
168
+
169
+ async def consume() -> None:
170
+ async for _ in adapter.execute(runtime_request()):
171
+ pass
172
+
173
+ task = asyncio.create_task(consume())
174
+ await started.wait()
175
+ await adapter.cancel("run_1")
176
+ with pytest.raises(SafeEngineError) as cancelled:
177
+ await task
178
+ assert cancelled.value.code == "RUN_CANCELLED"
179
+
180
+ async def failed_provider(_request: httpx.Request) -> httpx.Response:
181
+ return httpx.Response(500, text="provider-secret-body")
182
+
183
+ with pytest.raises(SafeEngineError) as failed:
184
+ async for _ in engine(failed_provider).execute(runtime_request()):
185
+ pass
186
+ assert failed.value.code == "PROVIDER_UNAVAILABLE"
187
+ assert "provider-secret-body" not in str(failed.value)
188
+
189
+
190
+ def engine(
191
+ provider_handler: Callable[[httpx.Request], Coroutine[None, None, httpx.Response]],
192
+ tool_handler: Callable[[httpx.Request], Coroutine[None, None, httpx.Response]]
193
+ | None = None,
194
+ ) -> AdkEngineAdapter:
195
+ async def unused_tool(_request: httpx.Request) -> httpx.Response:
196
+ return httpx.Response(500, json={"error": "unused"})
197
+
198
+ provider = OpenAiCompatibleProvider(
199
+ OpenAiCompatibleConfig(
200
+ base_url="https://provider.test/v1",
201
+ model="qwen-compatible",
202
+ api_key_ref=SecretRef("env", "QWEN_API_KEY"),
203
+ https_host_allowlist=("provider.test",),
204
+ ),
205
+ EnvironmentSecretResolver({"QWEN_API_KEY": "server-secret"}),
206
+ transport=httpx.MockTransport(provider_handler),
207
+ )
208
+ audit_path = tempfile.NamedTemporaryFile(suffix=".sqlite3", delete=False).name
209
+ return AdkEngineAdapter(
210
+ provider,
211
+ ToolHostClient(
212
+ "internal-token",
213
+ transport=httpx.MockTransport(tool_handler or unused_tool),
214
+ ),
215
+ ProviderRequestGovernance(ProviderAuditStore(audit_path, retention_days=90)),
216
+ )
217
+
218
+
219
+ def runtime_request(tool_key: str | None = None) -> EngineRunRequest:
220
+ tools = (
221
+ (
222
+ RuntimeToolDefinition(
223
+ key=tool_key,
224
+ label="查询记录",
225
+ description="查询当前数据范围内的记录。",
226
+ ),
227
+ )
228
+ if tool_key
229
+ else ()
230
+ )
231
+ return EngineRunRequest(
232
+ run_id="run_1",
233
+ operation_key="business.answer",
234
+ input={"question": "记录"},
235
+ instructions="回答业务问题。",
236
+ user_prompt="有多少记录?",
237
+ allowed_tools=tools,
238
+ limits=RuntimeLimits(max_duration_seconds=60, max_output_bytes=4096),
239
+ provider_egress=ProviderEgress(
240
+ mode="configured-endpoint",
241
+ deployment_key="ai.openai-compatible",
242
+ ),
243
+ provider_data_policy=ProviderDataPolicy(
244
+ input_fields=[
245
+ ProviderInputFieldPolicy(
246
+ json_pointer="/question",
247
+ classification="internal",
248
+ dlp_action="redact",
249
+ )
250
+ ],
251
+ envelope_fields=[
252
+ ProviderFieldPolicy(
253
+ field=field,
254
+ classification="internal",
255
+ dlp_action="reject" if field == "instructions" else "redact",
256
+ )
257
+ for field in (
258
+ "instructions",
259
+ "user-prompt",
260
+ "tool-definitions",
261
+ "provider-output",
262
+ "tool-results",
263
+ )
264
+ ],
265
+ ),
266
+ tool_network_policy=ToolNetworkPolicy(mode="forbidden"),
267
+ execution_grant="grant_1",
268
+ tool_host_url="http://server:9747",
269
+ )
270
+
271
+
272
+ def tool_call_response(name: str) -> httpx.Response:
273
+ return sse_response(
274
+ [
275
+ {
276
+ "choices": [
277
+ {
278
+ "delta": {
279
+ "tool_calls": [
280
+ {
281
+ "index": 0,
282
+ "id": "call_1",
283
+ "function": {
284
+ "name": name,
285
+ "arguments": '{"page":2}',
286
+ },
287
+ }
288
+ ]
289
+ }
290
+ }
291
+ ]
292
+ }
293
+ ]
294
+ )
295
+
296
+
297
+ def sse_response(chunks: list[object]) -> httpx.Response:
298
+ body = "".join(
299
+ f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" for chunk in chunks
300
+ )
301
+ return httpx.Response(
302
+ 200,
303
+ content=body.encode(),
304
+ headers={"content-type": "text/event-stream"},
305
+ )
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from google.adk.models.base_llm import BaseLlm
4
+
5
+ from adk_web_agents.safe_debug.agent import root_agent
6
+
7
+
8
+ def test_adk_web_agent_has_no_business_tools_or_provider_model() -> None:
9
+ assert root_agent.name == "safe_debug"
10
+ assert root_agent.tools == []
11
+ assert isinstance(root_agent.model, BaseLlm)
12
+ assert root_agent.model.model == "windy-safe-debug"
@@ -7,20 +7,7 @@ import tempfile
7
7
  import httpx
8
8
  import pytest
9
9
 
10
- from southwind_agent_server.config import (
11
- EnvironmentSecretResolver,
12
- OpenAiCompatibleConfig,
13
- SecretRef,
14
- )
15
- from southwind_agent_server.contracts import (
16
- ProviderDataPolicy,
17
- ProviderEgress,
18
- ProviderFieldPolicy,
19
- ProviderInputFieldPolicy,
20
- RuntimeLimits,
21
- RuntimeToolDefinition,
22
- ToolNetworkPolicy,
23
- )
10
+ from southwind_agent_server.contracts import ProviderEgress
24
11
  from southwind_agent_server.engine import (
25
12
  EngineRunRequest,
26
13
  SafeEngineError,
@@ -28,10 +15,13 @@ from southwind_agent_server.engine import (
28
15
  ToolRequested,
29
16
  )
30
17
  from southwind_agent_server.openai_engine import OpenAiCompatibleEngineAdapter
31
- from southwind_agent_server.openai_provider import OpenAiCompatibleProvider
32
- from southwind_agent_server.provider_audit import ProviderAuditStore
33
- from southwind_agent_server.provider_governance import ProviderRequestGovernance
34
18
  from southwind_agent_server.tool_host import ToolHostClient
19
+ from tests.openai_provider_test_support import (
20
+ create_provider,
21
+ governance,
22
+ runtime_request,
23
+ sse_response,
24
+ )
35
25
 
36
26
 
37
27
  async def test_provider_stream_and_health_use_bounded_safe_protocol() -> None:
@@ -255,109 +245,3 @@ async def test_engine_denies_mismatched_provider_deployment() -> None:
255
245
  async for _signal in engine.execute(request):
256
246
  pass
257
247
  assert captured.value.code == "FORBIDDEN"
258
-
259
-
260
- def create_provider(
261
- transport: httpx.AsyncBaseTransport,
262
- ) -> OpenAiCompatibleProvider:
263
- environment = {"QWEN_API_KEY": "server-secret"}
264
- return OpenAiCompatibleProvider(
265
- OpenAiCompatibleConfig(
266
- base_url="https://provider.test/v1",
267
- model="qwen-compatible",
268
- api_key_ref=SecretRef("env", "QWEN_API_KEY"),
269
- https_host_allowlist=("provider.test",),
270
- ),
271
- EnvironmentSecretResolver(environment),
272
- transport=transport,
273
- )
274
-
275
-
276
- def runtime_request(tool_key: str) -> EngineRunRequest:
277
- return EngineRunRequest(
278
- run_id="run_1",
279
- operation_key="business.answer",
280
- input={"question": "记录"},
281
- instructions="回答业务问题。",
282
- user_prompt="有多少记录?",
283
- allowed_tools=(
284
- RuntimeToolDefinition(
285
- key=tool_key,
286
- label="查询记录",
287
- description="查询当前数据范围内的记录。",
288
- ),
289
- ),
290
- limits=RuntimeLimits(
291
- max_duration_seconds=60,
292
- max_output_bytes=4096,
293
- ),
294
- provider_egress=ProviderEgress(
295
- mode="configured-endpoint",
296
- deployment_key="ai.openai-compatible",
297
- ),
298
- provider_data_policy=provider_data_policy(),
299
- tool_network_policy=ToolNetworkPolicy(mode="forbidden"),
300
- execution_grant="grant_1",
301
- tool_host_url="http://server:9747",
302
- )
303
-
304
-
305
- def provider_data_policy() -> ProviderDataPolicy:
306
- return ProviderDataPolicy(
307
- input_fields=[
308
- ProviderInputFieldPolicy(
309
- json_pointer="/question",
310
- classification="internal",
311
- dlp_action="redact",
312
- )
313
- ],
314
- envelope_fields=[
315
- ProviderFieldPolicy(
316
- field="instructions",
317
- classification="internal",
318
- dlp_action="reject",
319
- ),
320
- ProviderFieldPolicy(
321
- field="user-prompt",
322
- classification="internal",
323
- dlp_action="redact",
324
- ),
325
- ProviderFieldPolicy(
326
- field="tool-definitions",
327
- classification="internal",
328
- dlp_action="reject",
329
- ),
330
- ProviderFieldPolicy(
331
- field="provider-output",
332
- classification="internal",
333
- dlp_action="redact",
334
- ),
335
- ProviderFieldPolicy(
336
- field="tool-results",
337
- classification="sensitive",
338
- dlp_action="redact",
339
- ),
340
- ],
341
- )
342
-
343
-
344
- def governance(path: str | None = None) -> ProviderRequestGovernance:
345
- path = (
346
- path
347
- or tempfile.NamedTemporaryFile(
348
- suffix=".sqlite3",
349
- delete=False,
350
- ).name
351
- )
352
- return ProviderRequestGovernance(ProviderAuditStore(path, retention_days=90))
353
-
354
-
355
- def sse_response(chunks: list[object]) -> httpx.Response:
356
- body = "".join(
357
- f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" for chunk in chunks
358
- )
359
- return httpx.Response(
360
- 200,
361
- headers={"content-type": "text/event-stream"},
362
- content=f"{body}data: [DONE]\n\n",
363
- )
@@ -26,9 +26,12 @@
26
26
  "drizzle-orm": "^0.45.2",
27
27
  "elysia": "^1.4.29",
28
28
  "mysql2": "^3.22.6",
29
+ "nodemailer": "^9.0.3",
29
30
  "pg": "^8.22.0",
30
31
  "pino": "^10.3.1",
31
32
  "zod": "^4.4.3"
32
33
  },
33
- "devDependencies": {}
34
+ "devDependencies": {
35
+ "@types/nodemailer": "^8.0.1"
36
+ }
34
37
  }
@@ -12,7 +12,7 @@ import type {
12
12
  ModuleAiOperationHandlerRegistration,
13
13
  } from "@southwind-ai/server-sdk";
14
14
  import { evaluateAccessGuards } from "../access-guards.js";
15
- import type { RequestGuardContext } from "../guards.js";
15
+ import { auditActor, type RequestGuardContext } from "../guards.js";
16
16
  import type { InitializedModuleHosts } from "../module-host.js";
17
17
  import { toModuleActor } from "../module-host.js";
18
18
  import type { createServerRuntime } from "../runtime.js";
@@ -154,12 +154,7 @@ export class AgentRunFacade {
154
154
  id: randomUUIDv7(),
155
155
  action: operation.definition.auditAction,
156
156
  outcome: "success",
157
- actor: {
158
- id: context.actor.id,
159
- type:
160
- context.actor.type === "anonymous" ? "user" : context.actor.type,
161
- name: context.actor.name,
162
- },
157
+ actor: auditActor(context),
163
158
  target: {
164
159
  type: "agent-run",
165
160
  id: created.runId,
@@ -11,7 +11,7 @@ import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
11
11
  import Ajv, { type ValidateFunction } from "ajv";
12
12
  import { evaluateAccessGuards } from "../access-guards.js";
13
13
  import { isServerScopeContext } from "../data-access/context.js";
14
- import type { RequestGuardContext } from "../guards.js";
14
+ import { auditActor, type RequestGuardContext } from "../guards.js";
15
15
  import { findFeature } from "../route-guards.js";
16
16
  import type { createServerRuntime } from "../runtime.js";
17
17
 
@@ -292,11 +292,7 @@ function toolEvent(
292
292
  id: crypto.randomUUID(),
293
293
  action: tool.auditAction,
294
294
  outcome: "success",
295
- actor: {
296
- id: context.actor.id,
297
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
298
- name: context.actor.name,
299
- },
295
+ actor: auditActor(context),
300
296
  target: { type: "agent-tool", id: tool.key, label: tool.label },
301
297
  module: tool.module,
302
298
  requestId: context.requestId,
@@ -20,6 +20,7 @@ import { DrizzleSchedulerRepository } from "./scheduler/drizzle-repository.js";
20
20
  import { InMemorySchedulerRepository } from "./scheduler/repository.js";
21
21
  import type { ScheduledTaskDefinition } from "./scheduler/types.js";
22
22
  import type { createStorageRuntime } from "./storage/runtime.js";
23
+ import type { PlatformConfig } from "@southwind-ai/config";
23
24
  import type { DurableHandlerRegistry } from "@southwind-ai/jobs";
24
25
  import { collectActiveBlobRefs } from "./storage/blob-reference-registry.js";
25
26
  import type { createSystemRepositories } from "./system/seed.js";
@@ -33,6 +34,17 @@ import {
33
34
  } from "./workflow/effects.js";
34
35
  import { InMemoryWorkflowRepository } from "./workflow/repository.js";
35
36
  import { WorkflowService } from "./workflow/service.js";
37
+ import { NotificationChannelRegistry } from "./notification/channel-adapter.js";
38
+ import { DrizzleNotificationDeliveryRepository } from "./notification/delivery-repository.js";
39
+ import { NotificationDeliveryWorker } from "./notification/delivery-worker.js";
40
+ import { InMemoryNotificationDeliveryRepository } from "./notification/memory-delivery-repository.js";
41
+ import {
42
+ DrizzleNotificationPreferenceRepository,
43
+ InMemoryNotificationPreferenceRepository,
44
+ } from "./notification/preference-repository.js";
45
+ import { SystemNotificationRecipientDirectory } from "./notification/recipient-directory.js";
46
+ import { NotificationService } from "./notification/service.js";
47
+ import { SmtpNotificationChannelAdapter } from "./notification/smtp-adapter.js";
36
48
 
37
49
  interface ApplicationServicesInput {
38
50
  persistence: ReturnType<typeof createServerPersistence>;
@@ -42,6 +54,7 @@ interface ApplicationServicesInput {
42
54
  storage: ReturnType<typeof createStorageRuntime>;
43
55
  moduleTasks: ScheduledTaskDefinition[];
44
56
  bulkRetentionDays: number;
57
+ mailConfig?: PlatformConfig["mail"];
45
58
  installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
46
59
  }
47
60
 
@@ -52,6 +65,32 @@ export async function createApplicationServices(
52
65
  const notifications = persistence
53
66
  ? new DrizzleNotificationRepository(persistence.db)
54
67
  : new InMemoryNotificationRepository();
68
+ const notificationPreferences = persistence
69
+ ? new DrizzleNotificationPreferenceRepository(persistence.db)
70
+ : new InMemoryNotificationPreferenceRepository();
71
+ const notificationDeliveries = persistence
72
+ ? new DrizzleNotificationDeliveryRepository(persistence.db)
73
+ : new InMemoryNotificationDeliveryRepository();
74
+ const notificationRecipients = new SystemNotificationRecipientDirectory(
75
+ input.systemRepositories,
76
+ );
77
+ const notificationChannels = new NotificationChannelRegistry(
78
+ input.mailConfig?.enabled
79
+ ? [new SmtpNotificationChannelAdapter(input.mailConfig)]
80
+ : [],
81
+ );
82
+ const notificationService = new NotificationService(
83
+ notifications,
84
+ notificationPreferences,
85
+ notificationDeliveries,
86
+ notificationRecipients,
87
+ { mandatoryCategories: ["安全"] },
88
+ );
89
+ const notificationDeliveryWorker = new NotificationDeliveryWorker(
90
+ notificationDeliveries,
91
+ notificationRecipients,
92
+ notificationChannels,
93
+ );
55
94
  const deferredTimeout = new DeferredWorkflowTimeoutScheduler();
56
95
  const workflow = new WorkflowService(
57
96
  persistence
@@ -89,6 +128,15 @@ export async function createApplicationServices(
89
128
  const result = await governedExportDispatch.runNext?.();
90
129
  return { claimed: Boolean(result && result.status !== "idle") };
91
130
  },
131
+ notificationDispatch: async () => {
132
+ const scheduled = await notificationService.dispatchScheduled();
133
+ const delivery = await notificationDeliveryWorker.runNext();
134
+ return {
135
+ claimed: scheduled.claimed + delivery.claimed > 0,
136
+ scheduled,
137
+ delivery,
138
+ };
139
+ },
92
140
  }),
93
141
  ...input.moduleTasks,
94
142
  ];
@@ -149,6 +197,8 @@ export async function createApplicationServices(
149
197
  return {
150
198
  ...services,
151
199
  notifications,
200
+ notificationService,
201
+ notificationDeliveryWorker,
152
202
  workflow,
153
203
  governedExports,
154
204
  governedExportService,
@@ -74,6 +74,7 @@ export class DrizzleAuditLogReader {
74
74
 
75
75
  function auditWhere(query: AuditEventListQuery): SQL | undefined {
76
76
  const clauses = [
77
+ query.q ? keywordClause(query.q) : undefined,
77
78
  query.includeScheduler
78
79
  ? undefined
79
80
  : ne(auditLogs.actorId, "system_scheduler"),
@@ -91,6 +92,30 @@ function auditWhere(query: AuditEventListQuery): SQL | undefined {
91
92
  return clauses.length ? and(...clauses) : undefined;
92
93
  }
93
94
 
95
+ function keywordClause(query: string): SQL {
96
+ const pattern = `%${escapeLike(query.toLowerCase())}%`;
97
+ return sql`lower(concat_ws(' ',
98
+ ${auditLogs.action},
99
+ ${auditLogs.actorId},
100
+ ${auditLogs.resource},
101
+ ${auditLogs.resourceId},
102
+ ${auditLogs.ip},
103
+ coalesce(${auditLogs.metadata} ->> 'actorName', ''),
104
+ coalesce(${auditLogs.metadata} ->> 'targetLabel', ''),
105
+ coalesce(${auditLogs.metadata} ->> 'message', ''),
106
+ coalesce(${auditLogs.metadata} ->> 'module', ''),
107
+ coalesce(${auditLogs.metadata} ->> 'requestId', ''),
108
+ coalesce(${auditLogs.metadata}::text, '')
109
+ )) like ${pattern} escape '\\'`;
110
+ }
111
+
112
+ function escapeLike(value: string): string {
113
+ return value
114
+ .replaceAll("\\", "\\\\")
115
+ .replaceAll("%", "\\%")
116
+ .replaceAll("_", "\\_");
117
+ }
118
+
94
119
  function rowToAuditEvent(row: typeof auditLogs.$inferSelect): AuditEvent {
95
120
  const metadata = readMetadata(row.metadata);
96
121
  const outcome = readString(metadata.outcome, "success") as AuditOutcome;
@@ -1,6 +1,6 @@
1
1
  import type { AuditEvent } from "@southwind-ai/shared";
2
2
  import { randomUUIDv7 } from "bun";
3
- import type { RequestGuardContext } from "../guards.js";
3
+ import { auditActor, type RequestGuardContext } from "../guards.js";
4
4
 
5
5
  export function searchAuditMetadata(
6
6
  query: string,
@@ -26,11 +26,7 @@ export function searchAuditEvent(
26
26
  id: randomUUIDv7(),
27
27
  action: "data.search",
28
28
  outcome: "success",
29
- actor: {
30
- id: context.actor.id,
31
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
32
- name: context.actor.name,
33
- },
29
+ actor: auditActor(context),
34
30
  target: { type: resourceName },
35
31
  requestId: context.requestId,
36
32
  metadata: searchAuditMetadata(query, matched),
@@ -2,6 +2,7 @@ import type { RequestGuardContext } from "../guards.js";
2
2
 
3
3
  const PASSWORD_CHANGE_ALLOWLIST = new Set([
4
4
  "GET /api/auth/session",
5
+ "GET /api/auth/password/policy",
5
6
  "POST /api/auth/password/change",
6
7
  "POST /api/auth/logout",
7
8
  ]);