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
@@ -1,15 +1,25 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
- import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
2
  import type { RequestGuardContext } from "../guards.js";
4
3
  import type { RouteApp } from "../system/routes.js";
5
4
  import type { AgentRunFacade } from "./run-facade.js";
6
- import { AgentRunFacadeError } from "./run-facade.js";
5
+ import { AgentRunFacadeError } from "./run-errors.js";
7
6
  import type { AgentRuntimePort } from "./run-contracts.js";
8
7
  import {
9
8
  AgentToolHostError,
10
9
  attachExecutionGrant,
11
10
  type RestrictedAgentToolHost,
12
11
  } from "./tool-host.js";
12
+ import {
13
+ AgentToolApprovalError,
14
+ AgentToolApprovalPending,
15
+ } from "./tool-approval.js";
16
+ import {
17
+ parseAttachRequest,
18
+ parseCursor,
19
+ parseOperationKey,
20
+ parseStartRequest,
21
+ parseToolRequest,
22
+ } from "./run-route-input.js";
13
23
 
14
24
  interface RouteContext {
15
25
  body?: unknown;
@@ -39,6 +49,8 @@ export function registerAgentRunRoutes(
39
49
  request.input,
40
50
  idempotencyKey,
41
51
  context.guardContext,
52
+ request.artifacts,
53
+ context.request.signal,
42
54
  );
43
55
  } catch (error) {
44
56
  return facadeError(error);
@@ -49,7 +61,8 @@ export function registerAgentRunRoutes(
49
61
  const context = raw as RouteContext;
50
62
  try {
51
63
  const runId = required(context.params.runId);
52
- facade.authorizeRun(runId, context.guardContext);
64
+ const operationKey = parseOperationKey(context.query.operationKey);
65
+ await facade.authorizeRun(operationKey, runId, context.guardContext);
53
66
  const cursor = parseCursor(
54
67
  context.query.after,
55
68
  context.request.headers.get("last-event-id"),
@@ -64,7 +77,8 @@ export function registerAgentRunRoutes(
64
77
  const context = raw as RouteContext;
65
78
  try {
66
79
  const runId = required(context.params.runId);
67
- facade.authorizeRun(runId, context.guardContext);
80
+ const operationKey = parseOperationKey(context.query.operationKey);
81
+ await facade.authorizeRun(operationKey, runId, context.guardContext);
68
82
  return await remote.result(runId, context.request.signal);
69
83
  } catch (error) {
70
84
  return facadeError(error);
@@ -75,7 +89,8 @@ export function registerAgentRunRoutes(
75
89
  const context = raw as RouteContext;
76
90
  try {
77
91
  const runId = required(context.params.runId);
78
- facade.authorizeRun(runId, context.guardContext);
92
+ const operationKey = parseOperationKey(context.query.operationKey);
93
+ await facade.authorizeRun(operationKey, runId, context.guardContext);
79
94
  const reason =
80
95
  isRecord(context.body) && typeof context.body.reason === "string"
81
96
  ? context.body.reason.slice(0, 200)
@@ -86,6 +101,23 @@ export function registerAgentRunRoutes(
86
101
  }
87
102
  });
88
103
 
104
+ app.post("/agent/runs/:runId/attach", async (raw) => {
105
+ const context = raw as RouteContext;
106
+ try {
107
+ const runId = required(context.params.runId);
108
+ const request = parseAttachRequest(context.body);
109
+ return await facade.attach(
110
+ request.operationKey,
111
+ runId,
112
+ request.cursor,
113
+ context.guardContext,
114
+ context.request.signal,
115
+ );
116
+ } catch (error) {
117
+ return facadeError(error);
118
+ }
119
+ });
120
+
89
121
  app.post("/internal/agent/tools/execute", async (raw) => {
90
122
  const context = raw as RouteContext;
91
123
  if (!validInternalToken(context.request, internalToken)) {
@@ -97,6 +129,28 @@ export function registerAgentRunRoutes(
97
129
  attachExecutionGrant(parsed.invocation, parsed.executionGrant),
98
130
  );
99
131
  } catch (error) {
132
+ if (error instanceof AgentToolApprovalPending) {
133
+ return Response.json(
134
+ {
135
+ code: "APPROVAL_REQUIRED",
136
+ message: error.message,
137
+ workItemId: error.workItemId,
138
+ retryable: true,
139
+ },
140
+ { status: 202 },
141
+ );
142
+ }
143
+ if (error instanceof AgentToolApprovalError) {
144
+ return Response.json(
145
+ {
146
+ code: error.code,
147
+ message: error.message,
148
+ workItemId: error.workItemId,
149
+ retryable: false,
150
+ },
151
+ { status: error.code === "APPROVAL_UNAVAILABLE" ? 503 : 409 },
152
+ );
153
+ }
100
154
  if (error instanceof AgentToolHostError) {
101
155
  return errorResponse(error.code, error.message, 403);
102
156
  }
@@ -107,69 +161,10 @@ export function registerAgentRunRoutes(
107
161
  return app;
108
162
  }
109
163
 
110
- function parseStartRequest(body: unknown): {
111
- operationKey: string;
112
- input: ModuleAiJsonValue;
113
- } {
114
- if (
115
- !isRecord(body) ||
116
- Object.keys(body).some((key) => !["operationKey", "input"].includes(key))
117
- ) {
118
- throw new AgentRunFacadeError("INVALID_INPUT", "请求格式无效", 400);
119
- }
120
- if (typeof body.operationKey !== "string" || !body.operationKey.trim()) {
121
- throw new AgentRunFacadeError("INVALID_INPUT", "Operation Key 无效", 400);
122
- }
123
- assertJson(body.input);
124
- return { operationKey: body.operationKey, input: body.input };
125
- }
126
-
127
- function parseToolRequest(body: unknown) {
128
- const allowed = [
129
- "executionGrant",
130
- "runId",
131
- "toolCallId",
132
- "toolKey",
133
- "arguments",
134
- ];
135
- if (
136
- !isRecord(body) ||
137
- Object.keys(body).some((key) => !allowed.includes(key))
138
- ) {
139
- throw new Error("Tool 请求格式无效");
140
- }
141
- if (
142
- typeof body.executionGrant !== "string" ||
143
- typeof body.runId !== "string" ||
144
- typeof body.toolCallId !== "string" ||
145
- typeof body.toolKey !== "string" ||
146
- !isRecord(body.arguments)
147
- ) {
148
- throw new Error("Tool 请求字段无效");
149
- }
150
- assertJson(body.arguments);
151
- return {
152
- executionGrant: body.executionGrant,
153
- invocation: {
154
- runId: body.runId,
155
- toolCallId: body.toolCallId,
156
- toolKey: body.toolKey,
157
- arguments: body.arguments,
158
- },
159
- };
160
- }
161
-
162
- function parseCursor(after: string | undefined, header: string | null): number {
163
- const values = [after, header].filter(
164
- (value): value is string => value !== undefined && value !== null,
165
- );
166
- if (values.some((value) => !/^\d+$/.test(value))) {
167
- throw new AgentRunFacadeError("INVALID_INPUT", "事件游标无效", 400);
168
- }
169
- return Math.max(0, ...values.map(Number));
170
- }
171
-
172
- function validInternalToken(request: Request, expected: string): boolean {
164
+ export function validInternalToken(
165
+ request: Request,
166
+ expected: string,
167
+ ): boolean {
173
168
  const provided = request.headers
174
169
  .get("authorization")
175
170
  ?.replace(/^Bearer /, "");
@@ -212,11 +207,3 @@ function required(value: string | undefined): string {
212
207
  function isRecord(value: unknown): value is Record<string, unknown> {
213
208
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
214
209
  }
215
-
216
- function assertJson(value: unknown): asserts value is ModuleAiJsonValue {
217
- try {
218
- if (JSON.stringify(value) === undefined) throw new Error();
219
- } catch {
220
- throw new AgentRunFacadeError("INVALID_INPUT", "请求必须是 JSON", 400);
221
- }
222
- }
@@ -3,12 +3,49 @@ import type { ModuleManifest } from "@southwind-ai/modules";
3
3
  import type { InitializedModuleHosts } from "../module-host.js";
4
4
  import type { createServerRuntime } from "../runtime.js";
5
5
  import type { RouteApp } from "../system/routes.js";
6
+ import type { createStorageRuntime } from "../storage/runtime.js";
7
+ import type { SystemRepositories } from "../system/seed.js";
8
+ import { resolveCurrentSystemActor } from "../system/actor.js";
9
+ import { AiArtifactCodec } from "../ai-artifact/codec.js";
10
+ import { registerAiArtifactRoutes } from "../ai-artifact/routes.js";
11
+ import { AiArtifactRuntime } from "../ai-artifact/runtime.js";
12
+ import { DocumentRunFacade } from "../document/facade.js";
13
+ import {
14
+ RemoteDocumentRuntime,
15
+ unavailableDocumentRuntime,
16
+ } from "../document/remote-runtime.js";
17
+ import { registerDocumentRunRoutes } from "../document/routes.js";
18
+ import { EmbeddingFacade } from "../embedding/facade.js";
19
+ import {
20
+ RemoteEmbeddingRuntime,
21
+ unavailableEmbeddingRuntime,
22
+ } from "../embedding/remote-runtime.js";
23
+ import { registerEmbeddingRoutes } from "../embedding/routes.js";
24
+ import { SpeechRunFacade } from "../speech/facade.js";
25
+ import {
26
+ RemoteSpeechRuntime,
27
+ unavailableSpeechRuntime,
28
+ } from "../speech/remote-runtime.js";
29
+ import { registerSpeechRunRoutes } from "../speech/routes.js";
6
30
  import { ExecutionGrantStore } from "./execution-grants.js";
7
31
  import { RemoteAgentRuntime } from "./remote-runtime.js";
8
- import type { GovernedToolRegistry } from "./tool-registry.js";
32
+ import { GovernedToolRegistry } from "./tool-registry.js";
9
33
  import { AgentRunFacade } from "./run-facade.js";
10
34
  import { registerAgentRunRoutes } from "./run-routes.js";
11
35
  import { RestrictedAgentToolHost } from "./tool-host.js";
36
+ import { createSystemToolHandlers, registerAgentRoutes } from "./routes.js";
37
+ import { createModuleToolHandlerMap } from "../module-host.js";
38
+ import { PlatformMcpAdapter } from "../mcp/adapter.js";
39
+ import { registerMcpRoutes } from "../mcp/routes.js";
40
+ import type { WorkflowService } from "../workflow/service.js";
41
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
42
+ import { DrizzleAgentRunBindingRepository } from "./drizzle-run-binding-repository.js";
43
+ import { AgentRunAuthorizationStore } from "./run-authorization.js";
44
+ import { InMemoryAgentRunBindingRepository } from "./run-binding-repository.js";
45
+ import { AgentBackgroundOperationExecutor } from "./background-operation.js";
46
+ import type { DeferredAgentBackgroundOperationPortFactory } from "./background-operation-port.js";
47
+ import { UnixSandboxToolClient } from "./sandbox-client.js";
48
+ import { createModuleAiOperationPort } from "../module-host/ai-operation-port.js";
12
49
 
13
50
  export function registerAgentPlatformRuntime(input: {
14
51
  app: RouteApp;
@@ -16,34 +53,153 @@ export function registerAgentPlatformRuntime(input: {
16
53
  modules: readonly ModuleManifest[];
17
54
  hosts: InitializedModuleHosts;
18
55
  runtime: ReturnType<typeof createServerRuntime>;
19
- toolRegistry: GovernedToolRegistry;
20
- }): void {
21
- if (!input.platformConfig.enableAI) return;
56
+ systemRepositories: SystemRepositories;
57
+ storage: ReturnType<typeof createStorageRuntime>;
58
+ workflow: WorkflowService;
59
+ db?: NodePgDatabase;
60
+ backgroundAi: DeferredAgentBackgroundOperationPortFactory;
61
+ }) {
62
+ const features = input.modules.flatMap(({ features }) => features);
63
+ const toolHandlers = new Map([
64
+ ...createSystemToolHandlers(input.systemRepositories, input.runtime),
65
+ ...createModuleToolHandlerMap(input.hosts, {
66
+ runtime: input.runtime,
67
+ features,
68
+ storage: input.storage,
69
+ sandbox: process.env.TOOL_SANDBOX_SOCKET?.trim()
70
+ ? new UnixSandboxToolClient(process.env.TOOL_SANDBOX_SOCKET)
71
+ : undefined,
72
+ }),
73
+ ]);
74
+ const toolRegistry = new GovernedToolRegistry(
75
+ input.modules,
76
+ toolHandlers,
77
+ input.runtime,
78
+ );
79
+ registerAgentRoutes(input.app, toolRegistry);
80
+ registerMcpRoutes(
81
+ input.app,
82
+ new PlatformMcpAdapter(toolRegistry, input.runtime, features),
83
+ );
84
+ if (!input.platformConfig.enableAI) {
85
+ return {
86
+ toolHandlers,
87
+ aiArtifacts: undefined,
88
+ aiOperations: undefined,
89
+ };
90
+ }
22
91
  const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
92
+ const aiArtifacts = new AiArtifactRuntime(
93
+ new AiArtifactCodec(requiredEnvironment("AI_ARTIFACT_ENCRYPTION_SECRET")),
94
+ input.hosts,
95
+ input.runtime,
96
+ features,
97
+ input.storage,
98
+ (actorId) => resolveCurrentSystemActor(input.systemRepositories, actorId),
99
+ );
100
+ registerAiArtifactRoutes(input.app, aiArtifacts, internalToken);
23
101
  const remote = new RemoteAgentRuntime({
24
102
  baseUrl: process.env.AGENT_SERVER_URL || "http://agent-server:8080",
25
103
  internalToken,
26
104
  toolHostUrl: process.env.AGENT_TOOL_HOST_URL || "http://server:9747",
27
105
  });
28
106
  const grants = new ExecutionGrantStore();
107
+ const runAuthorizations = new AgentRunAuthorizationStore(
108
+ input.db
109
+ ? new DrizzleAgentRunBindingRepository(input.db)
110
+ : new InMemoryAgentRunBindingRepository(),
111
+ internalToken,
112
+ );
113
+ const documentRemote = process.env.DOCUMENT_SERVER_URL?.trim()
114
+ ? new RemoteDocumentRuntime({
115
+ baseUrl: process.env.DOCUMENT_SERVER_URL,
116
+ internalToken,
117
+ })
118
+ : unavailableDocumentRuntime;
119
+ const documentFacade = new DocumentRunFacade(
120
+ input.modules,
121
+ input.hosts,
122
+ input.runtime,
123
+ documentRemote,
124
+ aiArtifacts,
125
+ grants,
126
+ );
127
+ registerDocumentRunRoutes(input.app, documentFacade, documentRemote);
128
+ const speechRemote = process.env.SPEECH_SERVER_URL?.trim()
129
+ ? new RemoteSpeechRuntime({
130
+ baseUrl: process.env.SPEECH_SERVER_URL,
131
+ internalToken,
132
+ })
133
+ : unavailableSpeechRuntime;
134
+ const speechFacade = new SpeechRunFacade(
135
+ input.modules,
136
+ input.hosts,
137
+ input.runtime,
138
+ speechRemote,
139
+ aiArtifacts,
140
+ grants,
141
+ );
142
+ registerSpeechRunRoutes(input.app, speechFacade, speechRemote);
143
+ const embeddingRemote = process.env.EMBEDDING_SERVER_URL?.trim()
144
+ ? new RemoteEmbeddingRuntime({
145
+ baseUrl: process.env.EMBEDDING_SERVER_URL,
146
+ internalToken,
147
+ })
148
+ : unavailableEmbeddingRuntime;
149
+ const embeddingFacade = new EmbeddingFacade(
150
+ input.modules,
151
+ input.hosts,
152
+ input.runtime,
153
+ embeddingRemote,
154
+ aiArtifacts,
155
+ );
156
+ registerEmbeddingRoutes(input.app, embeddingFacade);
157
+ const agentFacade = new AgentRunFacade(
158
+ input.modules,
159
+ input.hosts,
160
+ input.runtime,
161
+ remote,
162
+ runAuthorizations,
163
+ aiArtifacts,
164
+ );
29
165
  registerAgentRunRoutes(
30
166
  input.app,
31
- new AgentRunFacade(
32
- input.modules,
33
- input.hosts,
34
- input.runtime,
35
- remote,
36
- grants,
37
- ),
167
+ agentFacade,
38
168
  remote,
39
169
  new RestrictedAgentToolHost(
40
170
  input.modules,
41
- input.toolRegistry,
171
+ input.hosts,
172
+ toolRegistry,
42
173
  input.runtime,
43
- grants,
174
+ runAuthorizations,
175
+ input.workflow,
176
+ (actorId) => resolveCurrentSystemActor(input.systemRepositories, actorId),
44
177
  ),
45
178
  internalToken,
46
179
  );
180
+ input.backgroundAi.bind(
181
+ new AgentBackgroundOperationExecutor(
182
+ input.modules,
183
+ input.hosts,
184
+ agentFacade,
185
+ remote,
186
+ ),
187
+ );
188
+ const aiOperations = (
189
+ moduleName: string,
190
+ guardContext: Parameters<typeof createModuleAiOperationPort>[1],
191
+ signal: AbortSignal,
192
+ ) =>
193
+ createModuleAiOperationPort(moduleName, guardContext, signal, {
194
+ agentFacade,
195
+ agentRemote: remote,
196
+ documentFacade,
197
+ documentRemote,
198
+ speechFacade,
199
+ speechRemote,
200
+ embeddingFacade,
201
+ });
202
+ return { toolHandlers, aiArtifacts, aiOperations };
47
203
  }
48
204
 
49
205
  function requiredEnvironment(key: string): string {
@@ -0,0 +1,200 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createConnection } from "node:net";
3
+ import type {
4
+ ModuleToolInput,
5
+ ModuleToolResult,
6
+ } from "@southwind-ai/server-sdk";
7
+ import {
8
+ type SandboxToolRequest,
9
+ type SandboxToolResponse,
10
+ signSandboxRequest,
11
+ } from "./sandbox-protocol.js";
12
+
13
+ const maxResponseBytes = 2 * 1_024 * 1_024;
14
+
15
+ export interface SandboxToolExecutor {
16
+ execute(
17
+ toolKey: string,
18
+ input: ModuleToolInput,
19
+ idempotencyKey: string,
20
+ signal: AbortSignal,
21
+ ): Promise<ModuleToolResult>;
22
+ }
23
+
24
+ export class UnixSandboxToolClient implements SandboxToolExecutor {
25
+ constructor(private readonly socketPath: string) {
26
+ if (
27
+ !socketPath.startsWith("/") ||
28
+ socketPath.includes("..") ||
29
+ socketPath.length > 180
30
+ ) {
31
+ throw new Error("Tool Sandbox Socket 路径无效");
32
+ }
33
+ }
34
+
35
+ async execute(
36
+ toolKey: string,
37
+ input: ModuleToolInput,
38
+ idempotencyKey: string,
39
+ signal: AbortSignal,
40
+ ): Promise<ModuleToolResult> {
41
+ signal.throwIfAborted();
42
+ const invocationId = crypto.randomUUID();
43
+ let lastError: unknown;
44
+ for (let attempt = 0; attempt < 2; attempt += 1) {
45
+ try {
46
+ return await this.attempt(
47
+ { invocationId, toolKey, idempotencyKey, input },
48
+ signal,
49
+ );
50
+ } catch (error) {
51
+ lastError = error;
52
+ if (
53
+ signal.aborted ||
54
+ (error instanceof SandboxToolClientError &&
55
+ !error.retryableConnection)
56
+ ) {
57
+ throw error;
58
+ }
59
+ }
60
+ }
61
+ throw asClientError(lastError);
62
+ }
63
+
64
+ private async attempt(
65
+ input: Omit<SandboxToolRequest, "deadline" | "grant">,
66
+ signal: AbortSignal,
67
+ ) {
68
+ const key = await readFile(`${this.socketPath}.key`);
69
+ if (key.byteLength !== 32) {
70
+ throw new SandboxToolClientError(
71
+ "SANDBOX_AUTH_INVALID",
72
+ "Tool Sandbox 授权材料无效",
73
+ true,
74
+ );
75
+ }
76
+ const unsigned = {
77
+ ...input,
78
+ deadline: Date.now() + 5 * 60_000,
79
+ };
80
+ const request: SandboxToolRequest = {
81
+ ...unsigned,
82
+ grant: signSandboxRequest(unsigned, key),
83
+ };
84
+ const response = await exchange(this.socketPath, request, signal);
85
+ if (response.invocationId !== input.invocationId) {
86
+ throw new SandboxToolClientError(
87
+ "SANDBOX_AUTH_INVALID",
88
+ "Tool Sandbox 响应标识无效",
89
+ true,
90
+ );
91
+ }
92
+ if (!response.ok) {
93
+ throw new SandboxToolClientError(
94
+ response.code,
95
+ response.message,
96
+ response.code === "SANDBOX_AUTH_INVALID",
97
+ );
98
+ }
99
+ return response.result;
100
+ }
101
+ }
102
+
103
+ export class SandboxToolClientError extends Error {
104
+ constructor(
105
+ readonly code:
106
+ | "SANDBOX_UNAVAILABLE"
107
+ | "SANDBOX_AUTH_INVALID"
108
+ | "SANDBOX_POLICY_DENIED"
109
+ | "SANDBOX_INPUT_INVALID"
110
+ | "SANDBOX_HANDLER_FAILED"
111
+ | "SANDBOX_TIMEOUT",
112
+ message: string,
113
+ readonly retryableConnection: boolean,
114
+ ) {
115
+ super(message);
116
+ }
117
+ }
118
+
119
+ async function exchange(
120
+ socketPath: string,
121
+ request: SandboxToolRequest,
122
+ signal: AbortSignal,
123
+ ): Promise<SandboxToolResponse> {
124
+ signal.throwIfAborted();
125
+ return new Promise((resolve, reject) => {
126
+ const socket = createConnection(socketPath);
127
+ let body = "";
128
+ const fail = (error: unknown) => {
129
+ socket.destroy();
130
+ reject(asClientError(error));
131
+ };
132
+ const abort = () => fail(signal.reason);
133
+ signal.addEventListener("abort", abort, { once: true });
134
+ socket.setEncoding("utf8");
135
+ socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
136
+ socket.on("data", (chunk: string) => {
137
+ body += chunk;
138
+ if (Buffer.byteLength(body) > maxResponseBytes) {
139
+ fail(new Error("Tool Sandbox 响应超过上限"));
140
+ return;
141
+ }
142
+ const newline = body.indexOf("\n");
143
+ if (newline < 0) return;
144
+ try {
145
+ resolve(parseResponse(JSON.parse(body.slice(0, newline))));
146
+ socket.end();
147
+ } catch (error) {
148
+ fail(error);
149
+ }
150
+ });
151
+ socket.once("error", fail);
152
+ socket.once("close", () => {
153
+ signal.removeEventListener("abort", abort);
154
+ if (!body.includes("\n")) {
155
+ reject(
156
+ new SandboxToolClientError(
157
+ "SANDBOX_UNAVAILABLE",
158
+ "Tool Sandbox 连接中断",
159
+ true,
160
+ ),
161
+ );
162
+ }
163
+ });
164
+ });
165
+ }
166
+
167
+ function parseResponse(value: unknown): SandboxToolResponse {
168
+ if (
169
+ !isRecord(value) ||
170
+ typeof value.invocationId !== "string" ||
171
+ typeof value.ok !== "boolean"
172
+ ) {
173
+ throw new Error("Tool Sandbox 响应无效");
174
+ }
175
+ if (value.ok && isRecord(value.result)) {
176
+ return value as unknown as SandboxToolResponse;
177
+ }
178
+ if (
179
+ !value.ok &&
180
+ typeof value.code === "string" &&
181
+ typeof value.message === "string" &&
182
+ typeof value.retryable === "boolean"
183
+ ) {
184
+ return value as unknown as SandboxToolResponse;
185
+ }
186
+ throw new Error("Tool Sandbox 响应字段无效");
187
+ }
188
+
189
+ function asClientError(error: unknown) {
190
+ if (error instanceof SandboxToolClientError) return error;
191
+ return new SandboxToolClientError(
192
+ "SANDBOX_UNAVAILABLE",
193
+ "Tool Sandbox 暂不可用",
194
+ true,
195
+ );
196
+ }
197
+
198
+ function isRecord(value: unknown): value is Record<string, unknown> {
199
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
200
+ }
@@ -0,0 +1,76 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import type {
3
+ ModuleToolInput,
4
+ ModuleToolResult,
5
+ } from "@southwind-ai/server-sdk";
6
+
7
+ export interface SandboxToolRequest {
8
+ readonly invocationId: string;
9
+ readonly toolKey: string;
10
+ readonly idempotencyKey: string;
11
+ readonly deadline: number;
12
+ readonly input: ModuleToolInput;
13
+ readonly grant: string;
14
+ }
15
+
16
+ export interface SandboxToolSuccess {
17
+ readonly invocationId: string;
18
+ readonly ok: true;
19
+ readonly result: ModuleToolResult;
20
+ }
21
+
22
+ export interface SandboxToolFailure {
23
+ readonly invocationId: string;
24
+ readonly ok: false;
25
+ readonly code:
26
+ | "SANDBOX_AUTH_INVALID"
27
+ | "SANDBOX_POLICY_DENIED"
28
+ | "SANDBOX_INPUT_INVALID"
29
+ | "SANDBOX_HANDLER_FAILED"
30
+ | "SANDBOX_TIMEOUT";
31
+ readonly message: string;
32
+ readonly retryable: boolean;
33
+ }
34
+
35
+ export type SandboxToolResponse = SandboxToolSuccess | SandboxToolFailure;
36
+
37
+ export function signSandboxRequest(
38
+ request: Omit<SandboxToolRequest, "grant">,
39
+ key: Uint8Array,
40
+ ): string {
41
+ return createHmac("sha256", key).update(canonicalJson(request)).digest("hex");
42
+ }
43
+
44
+ export function validSandboxGrant(
45
+ request: SandboxToolRequest,
46
+ key: Uint8Array,
47
+ ): boolean {
48
+ if (!/^[a-f0-9]{64}$/.test(request.grant)) return false;
49
+ const expected = signSandboxRequest(
50
+ {
51
+ invocationId: request.invocationId,
52
+ toolKey: request.toolKey,
53
+ idempotencyKey: request.idempotencyKey,
54
+ deadline: request.deadline,
55
+ input: request.input,
56
+ },
57
+ key,
58
+ );
59
+ return timingSafeEqual(Buffer.from(request.grant), Buffer.from(expected));
60
+ }
61
+
62
+ function canonicalJson(value: unknown): string {
63
+ return JSON.stringify(canonicalValue(value));
64
+ }
65
+
66
+ function canonicalValue(value: unknown): unknown {
67
+ if (Array.isArray(value)) return value.map(canonicalValue);
68
+ if (value !== null && typeof value === "object") {
69
+ return Object.fromEntries(
70
+ Object.entries(value as Record<string, unknown>)
71
+ .sort(([left], [right]) => left.localeCompare(right))
72
+ .map(([key, item]) => [key, canonicalValue(item)]),
73
+ );
74
+ }
75
+ return value;
76
+ }