create-windy 0.3.2 → 0.3.5

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 (343) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.js +825 -117
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +4 -0
  7. package/template/README.md +18 -11
  8. package/template/_gitignore +1 -0
  9. package/template/apps/agent-server/README.md +2 -0
  10. package/template/apps/agent-server/pyproject.toml +8 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
  14. package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
  15. package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
  18. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
  25. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
  26. package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
  29. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
  31. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
  32. package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
  33. package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
  34. package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
  35. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
  36. package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
  37. package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
  38. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
  39. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
  40. package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
  41. package/template/apps/agent-server/tests/test_api.py +24 -1
  42. package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
  43. package/template/apps/agent-server/tests/test_config.py +4 -0
  44. package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
  45. package/template/apps/agent-server/tests/test_multimodal.py +51 -0
  46. package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
  47. package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
  48. package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
  49. package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
  50. package/template/apps/agent-server/tests/test_telemetry.py +25 -0
  51. package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
  52. package/template/apps/agent-server/uv.lock +165 -0
  53. package/template/apps/document-server/Dockerfile +19 -0
  54. package/template/apps/document-server/pyproject.toml +43 -0
  55. package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
  56. package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
  57. package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
  58. package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
  59. package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
  60. package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
  61. package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
  62. package/template/apps/document-server/tests/__init__.py +1 -0
  63. package/template/apps/document-server/tests/test_config.py +42 -0
  64. package/template/apps/document-server/tests/test_service.py +140 -0
  65. package/template/apps/document-server/uv.lock +430 -0
  66. package/template/apps/embedding-server/Dockerfile +17 -0
  67. package/template/apps/embedding-server/pyproject.toml +44 -0
  68. package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
  69. package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
  70. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
  71. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
  72. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
  73. package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
  74. package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
  75. package/template/apps/embedding-server/tests/__init__.py +1 -0
  76. package/template/apps/embedding-server/tests/test_adapter.py +92 -0
  77. package/template/apps/embedding-server/tests/test_config.py +38 -0
  78. package/template/apps/embedding-server/uv.lock +449 -0
  79. package/template/apps/server/src/agent/background-operation-port.ts +37 -0
  80. package/template/apps/server/src/agent/background-operation.test.ts +231 -0
  81. package/template/apps/server/src/agent/background-operation.ts +252 -0
  82. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
  83. package/template/apps/server/src/agent/execution-grants.ts +7 -1
  84. package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
  85. package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
  86. package/template/apps/server/src/agent/idempotency-key.ts +11 -0
  87. package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
  88. package/template/apps/server/src/agent/remote-runtime.ts +173 -5
  89. package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
  90. package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
  91. package/template/apps/server/src/agent/run-authorization.ts +180 -0
  92. package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
  93. package/template/apps/server/src/agent/run-contracts.ts +88 -1
  94. package/template/apps/server/src/agent/run-definition-support.ts +58 -0
  95. package/template/apps/server/src/agent/run-errors.ts +25 -0
  96. package/template/apps/server/src/agent/run-facade.test.ts +57 -46
  97. package/template/apps/server/src/agent/run-facade.ts +185 -65
  98. package/template/apps/server/src/agent/run-release.ts +78 -0
  99. package/template/apps/server/src/agent/run-route-input.ts +169 -0
  100. package/template/apps/server/src/agent/run-routes.ts +63 -76
  101. package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
  102. package/template/apps/server/src/agent/sandbox-client.ts +200 -0
  103. package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
  104. package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
  105. package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
  106. package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
  107. package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
  108. package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
  109. package/template/apps/server/src/agent/tool-approval.ts +115 -0
  110. package/template/apps/server/src/agent/tool-audit.ts +62 -0
  111. package/template/apps/server/src/agent/tool-host.test.ts +39 -11
  112. package/template/apps/server/src/agent/tool-host.ts +184 -17
  113. package/template/apps/server/src/agent/tool-registry.ts +52 -47
  114. package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
  115. package/template/apps/server/src/agent/vision-input.ts +120 -0
  116. package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
  117. package/template/apps/server/src/ai-artifact/codec.ts +137 -0
  118. package/template/apps/server/src/ai-artifact/routes.ts +37 -0
  119. package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
  120. package/template/apps/server/src/application-services.ts +62 -2
  121. package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
  122. package/template/apps/server/src/auth/reauthentication.ts +19 -0
  123. package/template/apps/server/src/auth/runtime.ts +9 -1
  124. package/template/apps/server/src/data-access/context.ts +6 -4
  125. package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
  126. package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
  127. package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
  128. package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
  129. package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
  130. package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
  131. package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
  132. package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
  133. package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
  134. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
  135. package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
  136. package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
  137. package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
  138. package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
  139. package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
  140. package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
  141. package/template/apps/server/src/data-governance/export/routes.ts +11 -1
  142. package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
  143. package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
  144. package/template/apps/server/src/data-governance/export/service.ts +90 -113
  145. package/template/apps/server/src/data-governance/export/source.ts +101 -0
  146. package/template/apps/server/src/data-governance/export/types.ts +27 -0
  147. package/template/apps/server/src/document/contracts.ts +76 -0
  148. package/template/apps/server/src/document/facade.ts +339 -0
  149. package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
  150. package/template/apps/server/src/document/remote-runtime.ts +223 -0
  151. package/template/apps/server/src/document/routes.ts +193 -0
  152. package/template/apps/server/src/embedding/contracts.ts +83 -0
  153. package/template/apps/server/src/embedding/error.ts +10 -0
  154. package/template/apps/server/src/embedding/facade.ts +217 -0
  155. package/template/apps/server/src/embedding/input.ts +172 -0
  156. package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
  157. package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
  158. package/template/apps/server/src/embedding/routes.ts +152 -0
  159. package/template/apps/server/src/index.ts +40 -43
  160. package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
  161. package/template/apps/server/src/jobs/artifact-service.ts +10 -0
  162. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
  163. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
  164. package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
  165. package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
  166. package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
  167. package/template/apps/server/src/jobs/execution-repository.ts +17 -0
  168. package/template/apps/server/src/jobs/execution.test.ts +34 -0
  169. package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
  170. package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
  171. package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
  172. package/template/apps/server/src/jobs/repository.ts +3 -0
  173. package/template/apps/server/src/jobs/worker.ts +80 -41
  174. package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
  175. package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
  176. package/template/apps/server/src/module-host/durable-context.ts +124 -0
  177. package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
  178. package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
  179. package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
  180. package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
  181. package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
  182. package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
  183. package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
  184. package/template/apps/server/src/module-host/governed-resources.ts +203 -0
  185. package/template/apps/server/src/module-host/http-runtime.ts +95 -0
  186. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  187. package/template/apps/server/src/module-host/initialize.test.ts +49 -1
  188. package/template/apps/server/src/module-host/initialize.ts +39 -16
  189. package/template/apps/server/src/module-host/job-port.test.ts +262 -0
  190. package/template/apps/server/src/module-host/job-port.ts +334 -0
  191. package/template/apps/server/src/module-host/request-context.test.ts +88 -2
  192. package/template/apps/server/src/module-host/request-context.ts +56 -22
  193. package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
  194. package/template/apps/server/src/module-host/route-installer.ts +36 -1
  195. package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
  196. package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
  197. package/template/apps/server/src/module-host.ts +1 -0
  198. package/template/apps/server/src/module-runtime-validation.ts +7 -3
  199. package/template/apps/server/src/speech/contracts.ts +76 -0
  200. package/template/apps/server/src/speech/facade.ts +316 -0
  201. package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
  202. package/template/apps/server/src/speech/remote-runtime.ts +227 -0
  203. package/template/apps/server/src/speech/routes.ts +180 -0
  204. package/template/apps/server/src/system/actor.ts +39 -0
  205. package/template/apps/server/src/system/route-helpers.ts +1 -0
  206. package/template/apps/server/src/workflow/service.ts +2 -1
  207. package/template/apps/speech-server/Dockerfile +22 -0
  208. package/template/apps/speech-server/pyproject.toml +43 -0
  209. package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
  210. package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
  211. package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
  212. package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
  213. package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
  214. package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
  215. package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
  216. package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
  217. package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
  218. package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
  219. package/template/apps/speech-server/tests/__init__.py +1 -0
  220. package/template/apps/speech-server/tests/test_config.py +33 -0
  221. package/template/apps/speech-server/tests/test_service.py +154 -0
  222. package/template/apps/speech-server/uv.lock +430 -0
  223. package/template/apps/web/src/app/business-routes.ts +22 -0
  224. package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
  225. package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
  226. package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
  227. package/template/apps/web/src/components/common/FormDialog.vue +6 -0
  228. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
  229. package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
  230. package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
  231. package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
  232. package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
  233. package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
  234. package/template/apps/web/src/layout/AppShell.vue +28 -25
  235. package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
  236. package/template/apps/web/src/router/business-routes.ts +68 -0
  237. package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
  238. package/template/apps/web/src/router/index.ts +7 -0
  239. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  240. package/template/apps/web/src/services/access-api.ts +5 -1
  241. package/template/apps/web/src/services/access-api.webtest.ts +35 -0
  242. package/template/docker-compose.yml +127 -0
  243. package/template/docs/architecture/ai-egress.md +12 -9
  244. package/template/docs/architecture/ai-runtime.md +23 -11
  245. package/template/docs/architecture/durable-jobs.md +25 -3
  246. package/template/docs/architecture/foundation-infrastructure.md +163 -0
  247. package/template/docs/architecture/module-host.md +115 -0
  248. package/template/docs/architecture/server-module-interface.md +39 -0
  249. package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
  250. package/template/docs/development/business-web-routes.md +66 -0
  251. package/template/docs/development/custom-login-page.md +3 -3
  252. package/template/docs/development/module-lifecycle.md +71 -0
  253. package/template/docs/operations/observability.md +5 -1
  254. package/template/docs/platform/agent-runtime.md +21 -11
  255. package/template/docs/platform/agent-tool-sandbox.md +58 -0
  256. package/template/docs/platform/agent-tools.md +5 -0
  257. package/template/docs/platform/document-ai-runtime.md +204 -0
  258. package/template/docs/platform/governed-export-renderers.md +132 -0
  259. package/template/docs/platform/navigation-registry.md +5 -0
  260. package/template/docs/platform/organization-membership.md +2 -1
  261. package/template/docs/platform/speech-ai-runtime.md +156 -0
  262. package/template/docs/platform/system-crud-api.md +7 -1
  263. package/template/docs/platform/vision-ai-runtime.md +97 -0
  264. package/template/docs/platform/web-system-crud.md +6 -0
  265. package/template/package.json +16 -2
  266. package/template/packages/ai-client/index.ts +4 -0
  267. package/template/packages/ai-client/package.json +5 -2
  268. package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
  269. package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
  270. package/template/packages/ai-client/src/agent/client/client.ts +119 -7
  271. package/template/packages/ai-client/src/agent/client/run.ts +14 -0
  272. package/template/packages/ai-client/src/agent/client/types.ts +18 -0
  273. package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
  274. package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
  275. package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
  276. package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
  277. package/template/packages/ai-client/src/agent/index.ts +1 -0
  278. package/template/packages/ai-client/src/artifact-ref.ts +8 -0
  279. package/template/packages/ai-client/src/document/client.test.ts +114 -0
  280. package/template/packages/ai-client/src/document/client.ts +134 -0
  281. package/template/packages/ai-client/src/document/contracts.ts +152 -0
  282. package/template/packages/ai-client/src/document/error.ts +43 -0
  283. package/template/packages/ai-client/src/document/index.ts +3 -0
  284. package/template/packages/ai-client/src/document/run.ts +104 -0
  285. package/template/packages/ai-client/src/document/validation.ts +115 -0
  286. package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
  287. package/template/packages/ai-client/src/embedding/client.ts +96 -0
  288. package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
  289. package/template/packages/ai-client/src/embedding/error.ts +61 -0
  290. package/template/packages/ai-client/src/embedding/index.ts +3 -0
  291. package/template/packages/ai-client/src/embedding/validation.ts +117 -0
  292. package/template/packages/ai-client/src/speech/client.test.ts +135 -0
  293. package/template/packages/ai-client/src/speech/client.ts +104 -0
  294. package/template/packages/ai-client/src/speech/contracts.ts +129 -0
  295. package/template/packages/ai-client/src/speech/error.ts +43 -0
  296. package/template/packages/ai-client/src/speech/index.ts +3 -0
  297. package/template/packages/ai-client/src/speech/run.ts +133 -0
  298. package/template/packages/ai-client/src/speech/validation.ts +132 -0
  299. package/template/packages/config/package.json +2 -2
  300. package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
  301. package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
  302. package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
  303. package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
  304. package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
  305. package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
  306. package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
  307. package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
  308. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  309. package/template/packages/database/package.json +2 -2
  310. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  311. package/template/packages/database/src/schema/agent-runtime.ts +149 -0
  312. package/template/packages/database/src/schema/common.ts +2 -14
  313. package/template/packages/database/src/schema/durable-jobs.ts +5 -0
  314. package/template/packages/database/src/schema/governed-exports.ts +17 -5
  315. package/template/packages/database/src/schema/index.ts +3 -0
  316. package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
  317. package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
  318. package/template/packages/database/src/schema.test.ts +6 -23
  319. package/template/packages/jobs/package.json +3 -2
  320. package/template/packages/jobs/src/handlers.test.ts +13 -0
  321. package/template/packages/jobs/src/handlers.ts +7 -1
  322. package/template/packages/jobs/src/types.ts +4 -0
  323. package/template/packages/modules/package.json +2 -2
  324. package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
  325. package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
  326. package/template/packages/modules/src/ai-operation-composition.ts +59 -1
  327. package/template/packages/modules/src/manifest.ts +25 -1
  328. package/template/packages/modules/src/tool-composition.ts +18 -0
  329. package/template/packages/server-sdk/index.ts +50 -0
  330. package/template/packages/server-sdk/package.json +10 -4
  331. package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
  332. package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
  333. package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
  334. package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
  335. package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
  336. package/template/packages/server-sdk/src/durable-handler.ts +29 -0
  337. package/template/packages/server-sdk/src/jobs-port.ts +69 -0
  338. package/template/packages/server-sdk/src/module-host.ts +16 -2
  339. package/template/packages/server-sdk/src/request-context.ts +6 -2
  340. package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
  341. package/template/packages/server-sdk/src/tool-registration.ts +2 -0
  342. package/template/packages/shared/package.json +1 -1
  343. package/template/packages/shared/src/rbac.ts +30 -0
@@ -0,0 +1,249 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import json
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ from .config import SpeechRuntimeConfig
12
+ from .contracts import JsonValue, StartSpeechRunRequest
13
+ from .errors import SafeSpeechError
14
+ from .normalizer import AudioNormalizer, NormalizationError
15
+ from .output_validation import canonicalize_output
16
+
17
+ SUPPORTED_MEDIA_TYPES = frozenset(
18
+ {
19
+ "audio/mpeg",
20
+ "audio/mp4",
21
+ "audio/ogg",
22
+ "audio/wav",
23
+ "audio/x-wav",
24
+ "audio/flac",
25
+ "audio/webm",
26
+ "video/mp4",
27
+ "video/webm",
28
+ }
29
+ )
30
+
31
+
32
+ class SpeechAdapter:
33
+ def __init__(
34
+ self,
35
+ config: SpeechRuntimeConfig,
36
+ normalizer: AudioNormalizer | None = None,
37
+ ) -> None:
38
+ self.config = config
39
+ self.normalizer = normalizer or AudioNormalizer()
40
+
41
+ @property
42
+ def available(self) -> bool:
43
+ return self.config.adapter_url is not None and self.normalizer.available
44
+
45
+ async def execute(self, request: StartSpeechRunRequest) -> JsonValue:
46
+ if not self.config.adapter_url:
47
+ raise SafeSpeechError(
48
+ "CAPABILITY_UNAVAILABLE",
49
+ "Speech Adapter 未配置",
50
+ True,
51
+ )
52
+ if request.source.media_type.lower() not in SUPPORTED_MEDIA_TYPES:
53
+ raise SafeSpeechError(
54
+ "INVALID_INPUT",
55
+ "音频或视频格式不受支持",
56
+ False,
57
+ )
58
+ missing = set(request.required_capabilities) - set(self.config.capabilities)
59
+ if missing:
60
+ raise SafeSpeechError(
61
+ "CAPABILITY_UNAVAILABLE",
62
+ "Speech Adapter 缺少所需能力",
63
+ True,
64
+ )
65
+ self._authorize_deployment(request)
66
+ enforce_runtime_quotas(request, self.config)
67
+ source = await self._download(request)
68
+ normalized = Path(f"{source}.wav")
69
+ try:
70
+ try:
71
+ audio = await self.normalizer.normalize(
72
+ source,
73
+ normalized,
74
+ request.limits.max_media_duration_seconds,
75
+ request.limits.max_duration_seconds,
76
+ )
77
+ except NormalizationError as error:
78
+ raise SafeSpeechError(
79
+ error.code,
80
+ str(error),
81
+ error.code == "CAPABILITY_UNAVAILABLE",
82
+ ) from error
83
+ return await self._invoke(request, audio.path, audio.duration_ms)
84
+ finally:
85
+ await remove(source)
86
+ await remove(normalized)
87
+
88
+ async def _download(self, request: StartSpeechRunRequest) -> Path:
89
+ digest = hashlib.sha256()
90
+ total = 0
91
+ with tempfile.NamedTemporaryFile(delete=False) as target:
92
+ path = Path(target.name)
93
+ try:
94
+ async with httpx.AsyncClient(
95
+ timeout=httpx.Timeout(
96
+ self.config.request_timeout_seconds,
97
+ connect=self.config.connect_timeout_seconds,
98
+ )
99
+ ) as client:
100
+ async with client.stream(
101
+ "GET",
102
+ self.config.artifact_url,
103
+ headers={
104
+ "authorization": f"Bearer {self.config.internal_token}",
105
+ "x-ai-artifact-ref": request.source.value,
106
+ },
107
+ ) as response:
108
+ response.raise_for_status()
109
+ async for chunk in response.aiter_bytes():
110
+ total += len(chunk)
111
+ if total > min(
112
+ request.source.byte_size,
113
+ request.limits.max_input_bytes,
114
+ ):
115
+ raise SafeSpeechError(
116
+ "BUDGET_EXCEEDED",
117
+ "Speech 输入超过声明上限",
118
+ False,
119
+ )
120
+ digest.update(chunk)
121
+ target.write(chunk)
122
+ except SafeSpeechError:
123
+ await remove(path)
124
+ raise
125
+ except Exception as error:
126
+ await remove(path)
127
+ raise SafeSpeechError(
128
+ "ARTIFACT_UNAVAILABLE",
129
+ "Speech Runtime 无法读取 Artifact",
130
+ True,
131
+ ) from error
132
+ if (
133
+ total != request.source.byte_size
134
+ or digest.hexdigest() != request.source.sha256
135
+ ):
136
+ await remove(path)
137
+ raise SafeSpeechError(
138
+ "ARTIFACT_UNAVAILABLE",
139
+ "Artifact 完整性校验失败",
140
+ False,
141
+ )
142
+ return path
143
+
144
+ async def _invoke(
145
+ self,
146
+ request: StartSpeechRunRequest,
147
+ path: Path,
148
+ duration_ms: int,
149
+ ) -> JsonValue:
150
+ adapter_url = self.config.adapter_url
151
+ if not adapter_url:
152
+ raise SafeSpeechError(
153
+ "CAPABILITY_UNAVAILABLE", "Speech Adapter 未配置", True
154
+ )
155
+ headers = (
156
+ {"authorization": f"Bearer {self.config.adapter_token}"}
157
+ if self.config.adapter_token
158
+ else {}
159
+ )
160
+ try:
161
+ async with httpx.AsyncClient(
162
+ timeout=httpx.Timeout(
163
+ self.config.request_timeout_seconds,
164
+ connect=self.config.connect_timeout_seconds,
165
+ )
166
+ ) as client:
167
+ with path.open("rb") as source:
168
+ response = await client.post(
169
+ f"{adapter_url.rstrip('/')}/v1/speech/transcribe",
170
+ headers=headers,
171
+ files={"file": ("source.wav", source, "audio/wav")},
172
+ data={
173
+ "operationKey": request.operation_key,
174
+ "durationMs": str(duration_ms),
175
+ "dataClassification": request.data_classification,
176
+ "options": encode(request.options),
177
+ "providerEgress": request.provider_egress.model_dump_json(
178
+ by_alias=True
179
+ ),
180
+ "providerDataPolicy": (
181
+ request.provider_data_policy.model_dump_json(
182
+ by_alias=True
183
+ )
184
+ if request.provider_data_policy
185
+ else "null"
186
+ ),
187
+ "limits": request.limits.model_dump_json(by_alias=True),
188
+ },
189
+ )
190
+ response.raise_for_status()
191
+ if len(response.content) > request.limits.max_output_bytes:
192
+ raise SafeSpeechError(
193
+ "BUDGET_EXCEEDED",
194
+ "Speech 输出超过上限",
195
+ False,
196
+ )
197
+ value: JsonValue = response.json()
198
+ return canonicalize_output(
199
+ value,
200
+ duration_ms,
201
+ request.limits.max_units,
202
+ )
203
+ except SafeSpeechError:
204
+ raise
205
+ except Exception as error:
206
+ raise SafeSpeechError(
207
+ "RUNTIME_UNAVAILABLE",
208
+ "Speech Adapter 执行失败",
209
+ True,
210
+ ) from error
211
+
212
+ def _authorize_deployment(self, request: StartSpeechRunRequest) -> None:
213
+ if request.provider_egress.mode == "forbidden":
214
+ return
215
+ if not request.provider_data_policy:
216
+ raise SafeSpeechError(
217
+ "FORBIDDEN",
218
+ "Speech Operation 缺少 Provider 字段治理策略",
219
+ False,
220
+ )
221
+ if (
222
+ not self.config.deployment_key
223
+ or request.provider_egress.deployment_key != self.config.deployment_key
224
+ ):
225
+ raise SafeSpeechError(
226
+ "FORBIDDEN",
227
+ "Speech Operation 未授权当前 Deployment",
228
+ False,
229
+ )
230
+
231
+
232
+ def enforce_runtime_quotas(
233
+ request: StartSpeechRunRequest,
234
+ config: SpeechRuntimeConfig,
235
+ ) -> None:
236
+ if request.source.byte_size > request.limits.max_input_bytes:
237
+ raise SafeSpeechError("BUDGET_EXCEEDED", "Speech Artifact 超过输入上限", False)
238
+ if request.limits.max_units > config.max_units:
239
+ raise SafeSpeechError("BUDGET_EXCEEDED", "转写分段上限超过 Runtime 配置", False)
240
+ if request.limits.max_media_duration_seconds > config.max_media_duration_seconds:
241
+ raise SafeSpeechError("BUDGET_EXCEEDED", "媒体时长上限超过 Runtime 配置", False)
242
+
243
+
244
+ def encode(value: object) -> str:
245
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
246
+
247
+
248
+ async def remove(path: Path) -> None:
249
+ await asyncio.to_thread(path.unlink, missing_ok=True)
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hmac
5
+ import os
6
+ import uuid
7
+ from collections.abc import AsyncIterator
8
+
9
+ from fastapi import Depends, FastAPI, Header, HTTPException, Query
10
+ from fastapi.exceptions import RequestValidationError
11
+ from fastapi.responses import JSONResponse, Response, StreamingResponse
12
+
13
+ from .adapter import SpeechAdapter
14
+ from .config import SpeechRuntimeConfig
15
+ from .contracts import ErrorResponse, StartRunResponse, StartSpeechRunRequest
16
+ from .errors import SafeSpeechError
17
+ from .service import SpeechRunService
18
+ from .store import RunConflictError, SpeechRunStore
19
+
20
+
21
+ def create_app(config: SpeechRuntimeConfig) -> FastAPI:
22
+ app = FastAPI(title="Southwind Speech Runtime", docs_url=None, redoc_url=None)
23
+ store = SpeechRunStore(config.store_path)
24
+ service = SpeechRunService(SpeechAdapter(config), store)
25
+
26
+ def authorize(authorization: str | None = Header(default=None)) -> None:
27
+ provided = (authorization or "").removeprefix("Bearer ")
28
+ if not provided or not hmac.compare_digest(provided, config.internal_token):
29
+ raise HTTPException(status_code=403, detail="FORBIDDEN")
30
+
31
+ @app.get("/health/ready")
32
+ async def ready(_: None = Depends(authorize)) -> JSONResponse:
33
+ if not service.adapter.available:
34
+ return JSONResponse(
35
+ {"status": "unavailable", "capabilities": []},
36
+ status_code=503,
37
+ )
38
+ return JSONResponse(
39
+ {
40
+ "status": "ready",
41
+ "capabilities": list(config.capabilities),
42
+ "limits": {
43
+ "maxUnits": config.max_units,
44
+ "maxMediaDurationSeconds": config.max_media_duration_seconds,
45
+ },
46
+ }
47
+ )
48
+
49
+ @app.post("/api/speech/runs", status_code=202)
50
+ async def start(
51
+ request: StartSpeechRunRequest,
52
+ idempotency_key: str = Header(alias="idempotency-key"),
53
+ _: None = Depends(authorize),
54
+ ) -> StartRunResponse:
55
+ if not idempotency_key.strip() or len(idempotency_key) > 160:
56
+ raise HTTPException(status_code=400, detail="INVALID_INPUT")
57
+ try:
58
+ record = await service.start(request, idempotency_key)
59
+ except RunConflictError:
60
+ raise HTTPException(status_code=409, detail="RUN_CONFLICT") from None
61
+ return StartRunResponse(
62
+ run_id=record.run_id,
63
+ status=record.status,
64
+ events_url=f"/api/speech/runs/{record.run_id}/events",
65
+ )
66
+
67
+ @app.get("/api/speech/runs/{run_id}/events")
68
+ async def events(
69
+ run_id: str,
70
+ after: int = Query(default=0, ge=0),
71
+ _: None = Depends(authorize),
72
+ ) -> StreamingResponse:
73
+ service.require(run_id)
74
+
75
+ async def stream() -> AsyncIterator[str]:
76
+ cursor = after
77
+ for _attempt in range(30):
78
+ current = service.require(run_id)
79
+ events = store.events_after(run_id, cursor)
80
+ for event in events:
81
+ sequence = event.get("sequence")
82
+ if not isinstance(sequence, int):
83
+ raise RuntimeError("Speech Event sequence 无效")
84
+ cursor = sequence
85
+ yield f"data: {encode_event(event)}\n\n"
86
+ if current.terminal:
87
+ return
88
+ if not events:
89
+ yield ": heartbeat\n\n"
90
+ await asyncio.sleep(1)
91
+
92
+ return StreamingResponse(stream(), media_type="text/event-stream")
93
+
94
+ @app.get("/api/speech/runs/{run_id}/result")
95
+ async def result(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
96
+ record = service.require(run_id)
97
+ if record.status != "completed" or not isinstance(record.output, dict):
98
+ return JSONResponse({"code": "RUN_NOT_READY"}, status_code=409)
99
+ artifact = store.artifact(run_id)
100
+ return JSONResponse(
101
+ {
102
+ "runId": run_id,
103
+ "status": "completed",
104
+ "output": {
105
+ **record.output,
106
+ "artifact": {
107
+ "mediaType": artifact.media_type,
108
+ "byteSize": artifact.byte_size,
109
+ "sha256": artifact.sha256,
110
+ },
111
+ },
112
+ }
113
+ )
114
+
115
+ @app.get("/api/speech/runs/{run_id}/artifact")
116
+ async def artifact(run_id: str, _: None = Depends(authorize)) -> Response:
117
+ record = service.require(run_id)
118
+ if record.status != "completed":
119
+ return JSONResponse(
120
+ encode_error("RUN_NOT_READY", "Speech 结果尚未就绪", False),
121
+ status_code=409,
122
+ )
123
+ result = store.artifact(run_id)
124
+ return Response(
125
+ content=result.body,
126
+ media_type=result.media_type,
127
+ headers={
128
+ "content-length": str(result.byte_size),
129
+ "x-content-sha256": result.sha256,
130
+ "cache-control": "private, no-store",
131
+ },
132
+ )
133
+
134
+ @app.post("/api/speech/runs/{run_id}/cancel")
135
+ async def cancel(run_id: str, _: None = Depends(authorize)) -> JSONResponse:
136
+ record = await service.cancel(run_id)
137
+ return JSONResponse({"runId": run_id, "status": record.status})
138
+
139
+ @app.exception_handler(KeyError)
140
+ async def missing(_request: object, _error: KeyError) -> JSONResponse:
141
+ return safe_error("RUN_NOT_FOUND", "Speech Run 不存在", 404, False)
142
+
143
+ @app.exception_handler(HTTPException)
144
+ async def http_error(_request: object, error: HTTPException) -> JSONResponse:
145
+ code = str(error.detail)
146
+ return safe_error(code, "Speech 请求被拒绝", error.status_code, False)
147
+
148
+ @app.exception_handler(RequestValidationError)
149
+ async def validation_error(
150
+ _request: object, _error: RequestValidationError
151
+ ) -> JSONResponse:
152
+ return safe_error("INVALID_INPUT", "Speech 请求格式无效", 400, False)
153
+
154
+ @app.exception_handler(SafeSpeechError)
155
+ async def safe_speech_error(
156
+ _request: object, error: SafeSpeechError
157
+ ) -> JSONResponse:
158
+ status = 403 if error.code == "FORBIDDEN" else 400
159
+ return safe_error(error.code, error.safe_message, status, error.retryable)
160
+
161
+ return app
162
+
163
+
164
+ def safe_error(code: str, message: str, status: int, retryable: bool) -> JSONResponse:
165
+ return JSONResponse(encode_error(code, message, retryable), status_code=status)
166
+
167
+
168
+ def encode_error(code: str, message: str, retryable: bool) -> dict[str, object]:
169
+ return ErrorResponse(
170
+ code=code,
171
+ message=message,
172
+ request_id=f"speech_{uuid.uuid4().hex}",
173
+ retryable=retryable,
174
+ ).model_dump(by_alias=True)
175
+
176
+
177
+ def encode_event(event: dict[str, object]) -> str:
178
+ import json
179
+
180
+ return json.dumps(event, ensure_ascii=False, separators=(",", ":"))
181
+
182
+
183
+ app = create_app(SpeechRuntimeConfig.from_environment(dict(os.environ)))
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import ipaddress
4
+ from dataclasses import dataclass
5
+ from urllib.parse import urlparse
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SpeechRuntimeConfig:
10
+ internal_token: str
11
+ store_path: str
12
+ artifact_url: str
13
+ adapter_url: str | None
14
+ adapter_token: str | None
15
+ deployment_key: str | None
16
+ capabilities: tuple[str, ...]
17
+ max_units: int = 100_000
18
+ max_media_duration_seconds: int = 24 * 60 * 60
19
+ connect_timeout_seconds: float = 5
20
+ request_timeout_seconds: float = 3600
21
+
22
+ @classmethod
23
+ def from_environment(cls, environment: dict[str, str]) -> SpeechRuntimeConfig:
24
+ artifact_url = environment.get(
25
+ "SPEECH_ARTIFACT_URL",
26
+ "http://server:9747/api/internal/ai/artifacts",
27
+ ).strip()
28
+ validate_intranet_url(artifact_url)
29
+ adapter_url = environment.get("SPEECH_ADAPTER_URL", "").strip() or None
30
+ if adapter_url:
31
+ validate_intranet_url(adapter_url)
32
+ capabilities = tuple(
33
+ sorted(
34
+ {
35
+ item.strip()
36
+ for item in environment.get(
37
+ "SPEECH_ADAPTER_CAPABILITIES",
38
+ (
39
+ "speech.transcribe.batch,speech.normalize,"
40
+ "speech.vad,speech.timestamps"
41
+ ),
42
+ ).split(",")
43
+ if item.strip()
44
+ }
45
+ )
46
+ )
47
+ return cls(
48
+ internal_token=required(environment, "SPEECH_SERVER_INTERNAL_TOKEN"),
49
+ store_path=environment.get(
50
+ "SPEECH_RUN_STORE_PATH", "/app/data/speech/runs.sqlite3"
51
+ ).strip(),
52
+ artifact_url=artifact_url,
53
+ adapter_url=adapter_url,
54
+ adapter_token=environment.get("SPEECH_ADAPTER_TOKEN", "").strip() or None,
55
+ deployment_key=environment.get("SPEECH_DEPLOYMENT_KEY", "").strip() or None,
56
+ capabilities=capabilities,
57
+ max_units=positive_int(environment, "SPEECH_MAX_UNITS", 100_000),
58
+ max_media_duration_seconds=positive_int(
59
+ environment,
60
+ "SPEECH_MAX_MEDIA_DURATION_SECONDS",
61
+ 24 * 60 * 60,
62
+ ),
63
+ )
64
+
65
+
66
+ def required(environment: dict[str, str], key: str) -> str:
67
+ value = environment.get(key, "").strip()
68
+ if not value:
69
+ raise ValueError(f"缺少 {key}")
70
+ return value
71
+
72
+
73
+ def positive_int(environment: dict[str, str], key: str, default: int) -> int:
74
+ raw = environment.get(key, str(default)).strip()
75
+ try:
76
+ value = int(raw)
77
+ except ValueError:
78
+ raise ValueError(f"{key} 必须是正整数") from None
79
+ if value <= 0:
80
+ raise ValueError(f"{key} 必须是正整数")
81
+ return value
82
+
83
+
84
+ def validate_intranet_url(raw: str) -> None:
85
+ parsed = urlparse(raw)
86
+ if parsed.scheme not in {"http", "https"} or not parsed.hostname:
87
+ raise ValueError("Speech 内部 URL 无效")
88
+ if parsed.username or parsed.password or parsed.query or parsed.fragment:
89
+ raise ValueError("Speech 内部 URL 不允许凭据、查询串或片段")
90
+ host = parsed.hostname.lower()
91
+ if host == "localhost" or host.endswith(".internal") or "." not in host:
92
+ return
93
+ try:
94
+ address = ipaddress.ip_address(host)
95
+ except ValueError:
96
+ raise ValueError(
97
+ "Speech 内部 URL 必须使用 localhost、内部服务名、私网 IP 或 .internal"
98
+ ) from None
99
+ if not (address.is_private or address.is_loopback):
100
+ raise ValueError("Speech 内部 URL 禁止使用公网地址")
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, Self
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+ type JsonValue = (
8
+ str | int | float | bool | None | list[JsonValue] | dict[str, JsonValue]
9
+ )
10
+ type RunStatus = Literal["accepted", "running", "completed", "failed", "cancelled"]
11
+
12
+
13
+ def to_camel(value: str) -> str:
14
+ first, *rest = value.split("_")
15
+ return first + "".join(part.capitalize() for part in rest)
16
+
17
+
18
+ class CanonicalModel(BaseModel):
19
+ model_config = ConfigDict(
20
+ alias_generator=to_camel,
21
+ populate_by_name=True,
22
+ extra="forbid",
23
+ )
24
+
25
+
26
+ class ArtifactInput(CanonicalModel):
27
+ value: str = Field(min_length=1, max_length=4096)
28
+ media_type: str = Field(min_length=1, max_length=160)
29
+ byte_size: int = Field(ge=1, le=2 * 1024 * 1024 * 1024)
30
+ sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
31
+
32
+
33
+ class RuntimeLimits(CanonicalModel):
34
+ max_duration_seconds: int = Field(ge=1, le=3600)
35
+ max_input_bytes: int = Field(ge=1, le=2 * 1024 * 1024 * 1024)
36
+ max_output_bytes: int = Field(ge=1024, le=64 * 1024 * 1024)
37
+ max_units: int = Field(ge=1, le=100_000)
38
+ max_media_duration_seconds: int = Field(ge=1, le=24 * 60 * 60)
39
+
40
+
41
+ class ProviderEgress(CanonicalModel):
42
+ mode: Literal["forbidden", "configured-endpoint"]
43
+ deployment_key: str | None = None
44
+
45
+ @model_validator(mode="after")
46
+ def validate_deployment(self) -> Self:
47
+ if self.mode == "configured-endpoint":
48
+ if not self.deployment_key or not self.deployment_key.strip():
49
+ raise ValueError("Provider Deployment Key 缺失")
50
+ elif self.deployment_key is not None:
51
+ raise ValueError("禁止出网时不能指定 Provider Deployment")
52
+ return self
53
+
54
+
55
+ class ProviderFieldPolicy(CanonicalModel):
56
+ field: Literal["artifact-content", "provider-output"]
57
+ classification: Literal["public", "internal", "sensitive"]
58
+ dlp_action: Literal["reject", "redact"]
59
+
60
+
61
+ class ProviderInputFieldPolicy(CanonicalModel):
62
+ json_pointer: str = Field(pattern=r"^/[^/]+(?:/[^/]+)*$")
63
+ classification: Literal["public", "internal", "sensitive"]
64
+ dlp_action: Literal["reject", "redact"]
65
+
66
+
67
+ class ProviderDataPolicy(CanonicalModel):
68
+ input_fields: list[ProviderInputFieldPolicy] = Field(min_length=1)
69
+ envelope_fields: list[ProviderFieldPolicy] = Field(min_length=2)
70
+
71
+ @model_validator(mode="after")
72
+ def validate_fields(self) -> Self:
73
+ fields = [rule.field for rule in self.envelope_fields]
74
+ if set(fields) != {"artifact-content", "provider-output"}:
75
+ raise ValueError("Speech Provider 字段治理策略不完整")
76
+ if len(fields) != len(set(fields)):
77
+ raise ValueError("Speech Provider 字段治理策略重复")
78
+ return self
79
+
80
+
81
+ class StartSpeechRunRequest(CanonicalModel):
82
+ kind: Literal["speech"]
83
+ operation_key: str = Field(min_length=1, max_length=160)
84
+ options: JsonValue
85
+ source: ArtifactInput
86
+ required_capabilities: list[str] = Field(min_length=1, max_length=32)
87
+ data_classification: Literal["public", "internal", "sensitive"]
88
+ limits: RuntimeLimits
89
+ provider_egress: ProviderEgress
90
+ provider_data_policy: ProviderDataPolicy | None = None
91
+
92
+
93
+ class StartRunResponse(CanonicalModel):
94
+ run_id: str
95
+ status: RunStatus
96
+ events_url: str
97
+
98
+
99
+ class ErrorResponse(CanonicalModel):
100
+ code: str
101
+ message: str
102
+ request_id: str
103
+ retryable: bool
@@ -0,0 +1,6 @@
1
+ class SafeSpeechError(Exception):
2
+ def __init__(self, code: str, message: str, retryable: bool) -> None:
3
+ super().__init__(message)
4
+ self.code = code
5
+ self.safe_message = message
6
+ self.retryable = retryable