create-windy 0.2.33 → 0.3.0

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 (284) hide show
  1. package/dist/cli.js +173 -17
  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 +9 -0
  6. package/template/README.md +3 -1
  7. package/template/apps/agent-server/Dockerfile +1 -0
  8. package/template/apps/agent-server/README.md +28 -5
  9. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  11. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  12. package/template/apps/agent-server/pyproject.toml +1 -1
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/app.py +63 -3
  18. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +35 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +2 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +31 -3
  21. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +22 -9
  22. package/template/apps/agent-server/src/southwind_agent_server/provider_audit.py +144 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +227 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/service.py +1 -0
  25. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  26. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  27. package/template/apps/agent-server/tests/test_api.py +16 -1
  28. package/template/apps/agent-server/tests/test_openai_provider.py +79 -0
  29. package/template/apps/agent-server/tests/test_provider_governance.py +215 -0
  30. package/template/apps/server/package.json +4 -1
  31. package/template/apps/server/src/agent/provider-input-governance.test.ts +82 -0
  32. package/template/apps/server/src/agent/provider-input-governance.ts +140 -0
  33. package/template/apps/server/src/agent/remote-runtime.test.ts +22 -0
  34. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  35. package/template/apps/server/src/agent/run-contracts.ts +2 -0
  36. package/template/apps/server/src/agent/run-facade.test.ts +52 -2
  37. package/template/apps/server/src/agent/run-facade.ts +38 -9
  38. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  39. package/template/apps/server/src/application-services.ts +50 -0
  40. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  41. package/template/apps/server/src/audit/search-summary.ts +2 -6
  42. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  43. package/template/apps/server/src/auth/password.ts +30 -21
  44. package/template/apps/server/src/auth/routes.test.ts +44 -3
  45. package/template/apps/server/src/auth/routes.ts +6 -1
  46. package/template/apps/server/src/auth/runtime.ts +4 -1
  47. package/template/apps/server/src/auth/service.ts +25 -7
  48. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  49. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  50. package/template/apps/server/src/configuration/definition.ts +242 -12
  51. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  52. package/template/apps/server/src/configuration/repository.ts +27 -2
  53. package/template/apps/server/src/configuration/routes.ts +4 -1
  54. package/template/apps/server/src/configuration/service.test.ts +100 -0
  55. package/template/apps/server/src/configuration/service.ts +50 -10
  56. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  57. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  58. package/template/apps/server/src/example-modules.ts +4 -1
  59. package/template/apps/server/src/guards.ts +18 -5
  60. package/template/apps/server/src/http/request-source.test.ts +85 -0
  61. package/template/apps/server/src/http/request-source.ts +90 -0
  62. package/template/apps/server/src/index.ts +45 -45
  63. package/template/apps/server/src/license/routes.ts +2 -0
  64. package/template/apps/server/src/license/runtime-service.ts +10 -11
  65. package/template/apps/server/src/module-bootstrap.ts +2 -2
  66. package/template/apps/server/src/module-host/initialize-contracts.ts +6 -0
  67. package/template/apps/server/src/module-host/initialize.ts +30 -44
  68. package/template/apps/server/src/module-host/request-context.ts +29 -23
  69. package/template/apps/server/src/module-host/resource-file-policies.test.ts +197 -0
  70. package/template/apps/server/src/module-host/resource-file-policies.ts +108 -0
  71. package/template/apps/server/src/module-host/route-declaration.ts +45 -0
  72. package/template/apps/server/src/module-host/route-installer.ts +27 -3
  73. package/template/apps/server/src/module-host/storage-port.test.ts +170 -1
  74. package/template/apps/server/src/module-host/storage-port.ts +134 -1
  75. package/template/apps/server/src/module-host/storage-scope.ts +89 -0
  76. package/template/apps/server/src/module-host/tool-handlers.test.ts +305 -0
  77. package/template/apps/server/src/module-host/tool-handlers.ts +38 -4
  78. package/template/apps/server/src/module-host/user-directory-composition.test.ts +126 -0
  79. package/template/apps/server/src/module-host/user-directory-port.test.ts +238 -0
  80. package/template/apps/server/src/module-host/user-directory-port.ts +139 -0
  81. package/template/apps/server/src/module-host.ts +4 -0
  82. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  83. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  84. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  85. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  86. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  87. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  88. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  89. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  90. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  91. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  92. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  93. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  94. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  95. package/template/apps/server/src/notification/memory-support.ts +72 -0
  96. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  97. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  98. package/template/apps/server/src/notification/repository.test.ts +171 -0
  99. package/template/apps/server/src/notification/repository.ts +179 -32
  100. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  101. package/template/apps/server/src/notification/route-support.ts +225 -0
  102. package/template/apps/server/src/notification/routes.test.ts +182 -0
  103. package/template/apps/server/src/notification/routes.ts +225 -132
  104. package/template/apps/server/src/notification/service.test.ts +104 -0
  105. package/template/apps/server/src/notification/service.ts +140 -0
  106. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  107. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  108. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  109. package/template/apps/server/src/route-guards.ts +1 -0
  110. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  111. package/template/apps/server/src/runtime-development.ts +2 -0
  112. package/template/apps/server/src/runtime.test.ts +23 -9
  113. package/template/apps/server/src/runtime.ts +39 -2
  114. package/template/apps/server/src/scheduler/audit.ts +2 -11
  115. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  116. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  117. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  118. package/template/apps/server/src/scheduler/routes.ts +2 -7
  119. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  120. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  121. package/template/apps/server/src/search/service.ts +2 -6
  122. package/template/apps/server/src/settings/routes.ts +15 -3
  123. package/template/apps/server/src/settings/runtime.ts +10 -1
  124. package/template/apps/server/src/storage/runtime.ts +13 -0
  125. package/template/apps/server/src/system/audit-query.ts +33 -4
  126. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  127. package/template/apps/server/src/system/audit-routes.ts +6 -5
  128. package/template/apps/server/src/system/built-in-roles.ts +1 -0
  129. package/template/apps/server/src/system/department-organization.ts +320 -0
  130. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  131. package/template/apps/server/src/system/department-routes.ts +114 -33
  132. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  133. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  134. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  135. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  136. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  137. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  138. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  139. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  140. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  141. package/template/apps/server/src/system/route-helpers.ts +2 -5
  142. package/template/apps/server/src/system/route-types.ts +12 -3
  143. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  144. package/template/apps/server/src/system/routes.test.ts +10 -2
  145. package/template/apps/server/src/system/routes.ts +70 -36
  146. package/template/apps/server/src/system/seed.ts +7 -0
  147. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  148. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  149. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  150. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  151. package/template/apps/server/src/work-order/service.ts +42 -10
  152. package/template/apps/web/src/app/error-pages.ts +9 -0
  153. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  154. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  155. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  156. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  157. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  158. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  159. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  160. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  161. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  162. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  163. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  164. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  165. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  166. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  167. package/template/apps/web/src/lib/date-time.ts +22 -10
  168. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  169. package/template/apps/web/src/main.ts +14 -1
  170. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  171. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  172. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  173. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  174. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  175. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  176. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  177. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  178. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  179. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  180. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  181. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  182. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  183. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  184. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  185. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  186. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  187. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  188. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  189. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  190. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  191. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  192. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  193. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  194. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  195. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  196. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  197. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  198. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  199. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  200. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  201. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  202. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  203. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  204. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  205. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  206. package/template/apps/web/src/router/error-navigation.ts +50 -0
  207. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  208. package/template/apps/web/src/router/index.ts +71 -0
  209. package/template/apps/web/src/router/index.webtest.ts +29 -0
  210. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  211. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  212. package/template/apps/web/src/services/auth-api.ts +5 -0
  213. package/template/apps/web/src/services/http.ts +43 -6
  214. package/template/apps/web/src/services/http.webtest.ts +70 -0
  215. package/template/apps/web/src/services/notification-api.ts +85 -2
  216. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  217. package/template/apps/web/src/services/search-api.ts +39 -0
  218. package/template/apps/web/src/services/system-api.ts +18 -0
  219. package/template/apps/web/vitest.config.ts +1 -0
  220. package/template/docker-compose.yml +68 -0
  221. package/template/docs/architecture/ai-egress.md +49 -7
  222. package/template/docs/architecture/ai-runtime.md +34 -6
  223. package/template/docs/architecture/data-scope-query-enforcement.md +4 -0
  224. package/template/docs/architecture/object-storage.md +22 -2
  225. package/template/docs/architecture/search-provider-protocol.md +38 -2
  226. package/template/docs/development/custom-error-pages.md +78 -0
  227. package/template/docs/development/custom-login-page.md +3 -3
  228. package/template/docs/platform/agent-runtime.md +76 -7
  229. package/template/docs/platform/audit-reliability.md +46 -2
  230. package/template/docs/platform/business-user-directory.md +82 -0
  231. package/template/docs/platform/module-configuration.md +13 -4
  232. package/template/docs/platform/notifications.md +67 -11
  233. package/template/docs/platform/organization-membership.md +17 -3
  234. package/template/docs/platform/system-crud-api.md +16 -0
  235. package/template/docs/platform/web-system-crud.md +6 -2
  236. package/template/package.json +4 -0
  237. package/template/packages/config/index.test.ts +45 -0
  238. package/template/packages/config/package.json +1 -1
  239. package/template/packages/config/src/platform.ts +169 -48
  240. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  241. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  242. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  243. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  244. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  245. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  246. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  247. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  248. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  249. package/template/packages/database/package.json +1 -1
  250. package/template/packages/database/src/schema/identity.ts +7 -0
  251. package/template/packages/database/src/schema/notifications.ts +135 -0
  252. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  253. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  254. package/template/packages/modules/package.json +1 -1
  255. package/template/packages/modules/src/ai-operation-composition.test.ts +68 -0
  256. package/template/packages/modules/src/ai-operation-composition.ts +115 -0
  257. package/template/packages/modules/src/manifest.ts +26 -0
  258. package/template/packages/modules/src/provider-data-policy-composition.test.ts +74 -0
  259. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  260. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  261. package/template/packages/modules/src/system-features.ts +6 -1
  262. package/template/packages/modules/src/system-module-catalog.test.ts +16 -0
  263. package/template/packages/modules/src/system-module-catalog.ts +28 -4
  264. package/template/packages/modules/src/system-modules.test.ts +13 -1
  265. package/template/packages/modules/src/system-modules.ts +12 -0
  266. package/template/packages/modules/src/system-permissions.ts +11 -5
  267. package/template/packages/server-sdk/index.ts +11 -0
  268. package/template/packages/server-sdk/package.json +3 -2
  269. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -1
  270. package/template/packages/server-sdk/src/module-host.ts +8 -0
  271. package/template/packages/server-sdk/src/request-context.ts +2 -0
  272. package/template/packages/server-sdk/src/storage-port.ts +66 -0
  273. package/template/packages/server-sdk/src/tool-registration.ts +6 -0
  274. package/template/packages/server-sdk/src/user-directory-port.ts +42 -0
  275. package/template/packages/shared/package.json +1 -1
  276. package/template/packages/shared/src/audit.ts +25 -0
  277. package/template/packages/shared/src/module-configuration.ts +5 -0
  278. package/template/packages/shared/src/notification.ts +92 -1
  279. package/template/packages/shared/src/password.ts +7 -2
  280. package/template/packages/shared/src/platform-settings.ts +2 -0
  281. package/template/packages/storage/package.json +1 -1
  282. package/template/packages/storage/src/upload-service.test.ts +16 -0
  283. package/template/packages/storage/src/upload-service.ts +5 -1
  284. 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"
@@ -9,7 +9,7 @@ import httpx
9
9
  import pytest
10
10
  from fastapi import FastAPI
11
11
 
12
- from southwind_agent_server.app import create_app
12
+ from southwind_agent_server.app import create_app, provider_governance_snapshot
13
13
  from southwind_agent_server.deterministic import DeterministicEngineAdapter
14
14
 
15
15
 
@@ -233,3 +233,18 @@ async def test_liveness_does_not_claim_provider_readiness(
233
233
  assert live.json() == {"status": "live"}
234
234
  assert ready.status_code == 503
235
235
  assert ready.json()["code"] == "RUNTIME_UNAVAILABLE"
236
+
237
+
238
+ def test_provider_governance_health_filters_content_identifiers() -> None:
239
+ snapshot = provider_governance_snapshot(
240
+ {
241
+ "status": "healthy",
242
+ "retainedRecords": 3,
243
+ "runId": "run_secret",
244
+ "prompt": "do-not-expose",
245
+ }
246
+ )
247
+ assert snapshot["status"] == "healthy"
248
+ assert snapshot["retainedRecords"] == 3
249
+ assert "runId" not in snapshot
250
+ assert "prompt" not in snapshot
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import sqlite3
5
+ import tempfile
4
6
 
5
7
  import httpx
6
8
  import pytest
@@ -11,7 +13,10 @@ from southwind_agent_server.config import (
11
13
  SecretRef,
12
14
  )
13
15
  from southwind_agent_server.contracts import (
16
+ ProviderDataPolicy,
14
17
  ProviderEgress,
18
+ ProviderFieldPolicy,
19
+ ProviderInputFieldPolicy,
15
20
  RuntimeLimits,
16
21
  RuntimeToolDefinition,
17
22
  ToolNetworkPolicy,
@@ -24,6 +29,8 @@ from southwind_agent_server.engine import (
24
29
  )
25
30
  from southwind_agent_server.openai_engine import OpenAiCompatibleEngineAdapter
26
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
27
34
  from southwind_agent_server.tool_host import ToolHostClient
28
35
 
29
36
 
@@ -76,11 +83,13 @@ async def test_provider_error_never_leaks_body_or_secret() -> None:
76
83
 
77
84
  async def test_engine_executes_only_allowed_tool_through_host() -> None:
78
85
  provider_calls = 0
86
+ provider_body_bytes: list[int] = []
79
87
  tool_requests: list[dict[str, object]] = []
80
88
 
81
89
  async def provider_handler(request: httpx.Request) -> httpx.Response:
82
90
  nonlocal provider_calls
83
91
  provider_calls += 1
92
+ provider_body_bytes.append(len(request.content))
84
93
  if provider_calls == 1:
85
94
  return sse_response(
86
95
  [
@@ -111,12 +120,14 @@ async def test_engine_executes_only_allowed_tool_through_host() -> None:
111
120
  tool_requests.append(json.loads(request.content))
112
121
  return httpx.Response(200, json={"value": {"total": 1}})
113
122
 
123
+ audit_path = tempfile.NamedTemporaryFile(suffix=".sqlite3", delete=False).name
114
124
  engine = OpenAiCompatibleEngineAdapter(
115
125
  create_provider(httpx.MockTransport(provider_handler)),
116
126
  ToolHostClient(
117
127
  "internal-token",
118
128
  transport=httpx.MockTransport(tool_handler),
119
129
  ),
130
+ governance(audit_path),
120
131
  )
121
132
  signals = [
122
133
  signal
@@ -132,6 +143,20 @@ async def test_engine_executes_only_allowed_tool_through_host() -> None:
132
143
  "arguments": {"page": 2},
133
144
  }
134
145
  assert signals[-1].output == {"text": "共有一条记录"} # type: ignore[union-attr]
146
+ audits = (
147
+ sqlite3.connect(audit_path)
148
+ .execute(
149
+ """
150
+ select attempt, sent_bytes, decision
151
+ from provider_attempt_audit order by attempt
152
+ """
153
+ )
154
+ .fetchall()
155
+ )
156
+ assert audits == [
157
+ (1, provider_body_bytes[0], "allow"),
158
+ (2, provider_body_bytes[1], "allow"),
159
+ ]
135
160
 
136
161
 
137
162
  async def test_engine_denies_unknown_model_tool_before_host_callback() -> None:
@@ -172,6 +197,7 @@ async def test_engine_denies_unknown_model_tool_before_host_callback() -> None:
172
197
  "internal-token",
173
198
  transport=httpx.MockTransport(tool_handler),
174
199
  ),
200
+ governance(),
175
201
  )
176
202
  with pytest.raises(SafeEngineError) as captured:
177
203
  async for _signal in engine.execute(runtime_request("business.records.list")):
@@ -191,6 +217,7 @@ async def test_engine_denies_provider_before_network_without_explicit_egress() -
191
217
  engine = OpenAiCompatibleEngineAdapter(
192
218
  create_provider(httpx.MockTransport(provider_handler)),
193
219
  ToolHostClient("internal-token"),
220
+ governance(),
194
221
  )
195
222
  request = runtime_request("business.records.list")
196
223
  request = EngineRunRequest(
@@ -211,6 +238,7 @@ async def test_engine_denies_mismatched_provider_deployment() -> None:
211
238
  engine = OpenAiCompatibleEngineAdapter(
212
239
  create_provider(httpx.MockTransport(lambda _request: sse_response([]))),
213
240
  ToolHostClient("internal-token"),
241
+ governance(),
214
242
  )
215
243
  request = runtime_request("business.records.list")
216
244
  request = EngineRunRequest(
@@ -267,12 +295,63 @@ def runtime_request(tool_key: str) -> EngineRunRequest:
267
295
  mode="configured-endpoint",
268
296
  deployment_key="ai.openai-compatible",
269
297
  ),
298
+ provider_data_policy=provider_data_policy(),
270
299
  tool_network_policy=ToolNetworkPolicy(mode="forbidden"),
271
300
  execution_grant="grant_1",
272
301
  tool_host_url="http://server:9747",
273
302
  )
274
303
 
275
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
+
276
355
  def sse_response(chunks: list[object]) -> httpx.Response:
277
356
  body = "".join(
278
357
  f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" for chunk in chunks