enya-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (389) hide show
  1. package/.env.example +20 -0
  2. package/.github/workflows/ci.yml +70 -0
  3. package/.github/workflows/publish.yml +250 -0
  4. package/.gitmodules +3 -0
  5. package/Cargo.lock +3584 -0
  6. package/Cargo.toml +97 -0
  7. package/crates/enact/Cargo.toml +27 -0
  8. package/crates/enact/src/lib.rs +60 -0
  9. package/crates/enact-a2a/Cargo.toml +25 -0
  10. package/crates/enact-a2a/src/lib.rs +411 -0
  11. package/crates/enact-channels/Cargo.toml +64 -0
  12. package/crates/enact-channels/examples/README.md +80 -0
  13. package/crates/enact-channels/examples/channel_bot.rs +169 -0
  14. package/crates/enact-channels/examples/telegram-echo.rs +34 -0
  15. package/crates/enact-channels/examples/whatsapp-echo.rs +142 -0
  16. package/crates/enact-channels/src/config.rs +213 -0
  17. package/crates/enact-channels/src/lib.rs +25 -0
  18. package/crates/enact-channels/src/runtime.rs +237 -0
  19. package/crates/enact-channels/src/security/mod.rs +5 -0
  20. package/crates/enact-channels/src/security/pairing.rs +205 -0
  21. package/crates/enact-channels/src/teams.rs +601 -0
  22. package/crates/enact-channels/src/telegram.rs +2833 -0
  23. package/crates/enact-channels/src/traits.rs +200 -0
  24. package/crates/enact-channels/src/webhook.rs +262 -0
  25. package/crates/enact-channels/src/whatsapp.rs +310 -0
  26. package/crates/enact-cli/Cargo.toml +40 -0
  27. package/crates/enact-cli/src/commands/doctor.rs +62 -0
  28. package/crates/enact-cli/src/commands/mod.rs +3 -0
  29. package/crates/enact-cli/src/commands/run.rs +69 -0
  30. package/crates/enact-cli/src/commands/serve.rs +81 -0
  31. package/crates/enact-cli/src/config.rs +2 -0
  32. package/crates/enact-cli/src/main.rs +79 -0
  33. package/crates/enact-config/Cargo.toml +36 -0
  34. package/crates/enact-config/ENV_VAR_MAPPING.md +135 -0
  35. package/crates/enact-config/QUICK_REFERENCE.md +92 -0
  36. package/crates/enact-config/README.md +107 -0
  37. package/crates/enact-config/TESTING.md +161 -0
  38. package/crates/enact-config/examples/test-env-vars.rs +100 -0
  39. package/crates/enact-config/src/config.rs +399 -0
  40. package/crates/enact-config/src/encrypted_store.rs +211 -0
  41. package/crates/enact-config/src/lib.rs +298 -0
  42. package/crates/enact-config/src/secrets.rs +149 -0
  43. package/crates/enact-config/src/sync.rs +260 -0
  44. package/crates/enact-config/test-env-vars.sh +34 -0
  45. package/crates/enact-config/tests/README.md +99 -0
  46. package/crates/enact-config/tests/config_integration_test.rs +202 -0
  47. package/crates/enact-config/tests/security_test.rs +140 -0
  48. package/crates/enact-context/Cargo.toml +41 -0
  49. package/crates/enact-context/src/budget.rs +314 -0
  50. package/crates/enact-context/src/calibrator.rs +535 -0
  51. package/crates/enact-context/src/compactor.rs +392 -0
  52. package/crates/enact-context/src/condenser.rs +826 -0
  53. package/crates/enact-context/src/lib.rs +94 -0
  54. package/crates/enact-context/src/segment.rs +238 -0
  55. package/crates/enact-context/src/step_context.rs +645 -0
  56. package/crates/enact-context/src/token_counter.rs +148 -0
  57. package/crates/enact-context/src/window.rs +372 -0
  58. package/crates/enact-core/Cargo.toml +42 -0
  59. package/crates/enact-core/README.md +98 -0
  60. package/crates/enact-core/src/background/executor.rs +524 -0
  61. package/crates/enact-core/src/background/mod.rs +48 -0
  62. package/crates/enact-core/src/background/target_binding.rs +390 -0
  63. package/crates/enact-core/src/background/trigger.rs +511 -0
  64. package/crates/enact-core/src/callable/callable.rs +152 -0
  65. package/crates/enact-core/src/callable/composite.rs +817 -0
  66. package/crates/enact-core/src/callable/graph.rs +104 -0
  67. package/crates/enact-core/src/callable/llm.rs +211 -0
  68. package/crates/enact-core/src/callable/mod.rs +64 -0
  69. package/crates/enact-core/src/callable/registry.rs +206 -0
  70. package/crates/enact-core/src/context/execution_context.rs +757 -0
  71. package/crates/enact-core/src/context/invocation.rs +99 -0
  72. package/crates/enact-core/src/context/mod.rs +50 -0
  73. package/crates/enact-core/src/context/tenant.rs +175 -0
  74. package/crates/enact-core/src/context/trace.rs +127 -0
  75. package/crates/enact-core/src/flow/conditional.rs +293 -0
  76. package/crates/enact-core/src/flow/mod.rs +43 -0
  77. package/crates/enact-core/src/flow/parallel.rs +437 -0
  78. package/crates/enact-core/src/flow/repeat.rs +534 -0
  79. package/crates/enact-core/src/flow/sequential.rs +248 -0
  80. package/crates/enact-core/src/graph/checkpoint.rs +79 -0
  81. package/crates/enact-core/src/graph/checkpoint_store.rs +76 -0
  82. package/crates/enact-core/src/graph/compiled.rs +189 -0
  83. package/crates/enact-core/src/graph/edge.rs +59 -0
  84. package/crates/enact-core/src/graph/graph_schema.rs +218 -0
  85. package/crates/enact-core/src/graph/loader.rs +155 -0
  86. package/crates/enact-core/src/graph/mod.rs +18 -0
  87. package/crates/enact-core/src/graph/node/function.rs +49 -0
  88. package/crates/enact-core/src/graph/node/mod.rs +48 -0
  89. package/crates/enact-core/src/graph/schema.rs +62 -0
  90. package/crates/enact-core/src/inbox/message.rs +405 -0
  91. package/crates/enact-core/src/inbox/mod.rs +31 -0
  92. package/crates/enact-core/src/inbox/store.rs +355 -0
  93. package/crates/enact-core/src/kernel/artifact/filesystem.rs +546 -0
  94. package/crates/enact-core/src/kernel/artifact/metadata.rs +283 -0
  95. package/crates/enact-core/src/kernel/artifact/mod.rs +27 -0
  96. package/crates/enact-core/src/kernel/artifact/store.rs +427 -0
  97. package/crates/enact-core/src/kernel/enforcement.rs +1315 -0
  98. package/crates/enact-core/src/kernel/error.rs +1200 -0
  99. package/crates/enact-core/src/kernel/event.rs +1394 -0
  100. package/crates/enact-core/src/kernel/execution_model.rs +831 -0
  101. package/crates/enact-core/src/kernel/execution_state.rs +189 -0
  102. package/crates/enact-core/src/kernel/execution_strategy.rs +117 -0
  103. package/crates/enact-core/src/kernel/ids.rs +2086 -0
  104. package/crates/enact-core/src/kernel/interrupt.rs +125 -0
  105. package/crates/enact-core/src/kernel/kernel.rs +1283 -0
  106. package/crates/enact-core/src/kernel/mod.rs +205 -0
  107. package/crates/enact-core/src/kernel/persistence/event_store.rs +270 -0
  108. package/crates/enact-core/src/kernel/persistence/message_store.rs +908 -0
  109. package/crates/enact-core/src/kernel/persistence/mod.rs +102 -0
  110. package/crates/enact-core/src/kernel/persistence/state_store.rs +228 -0
  111. package/crates/enact-core/src/kernel/persistence/vector_store.rs +299 -0
  112. package/crates/enact-core/src/kernel/reducer.rs +808 -0
  113. package/crates/enact-core/src/kernel/replay.rs +153 -0
  114. package/crates/enact-core/src/lib.rs +413 -0
  115. package/crates/enact-core/src/memory/episodic.rs +0 -0
  116. package/crates/enact-core/src/memory/mod.rs +6 -0
  117. package/crates/enact-core/src/memory/semantic.rs +0 -0
  118. package/crates/enact-core/src/memory/trait.rs +0 -0
  119. package/crates/enact-core/src/memory/vector_db.rs +0 -0
  120. package/crates/enact-core/src/memory/working.rs +0 -0
  121. package/crates/enact-core/src/policy/execution_policy.rs +292 -0
  122. package/crates/enact-core/src/policy/filters.rs +458 -0
  123. package/crates/enact-core/src/policy/input_processor.rs +407 -0
  124. package/crates/enact-core/src/policy/long_running.rs +134 -0
  125. package/crates/enact-core/src/policy/mod.rs +193 -0
  126. package/crates/enact-core/src/policy/pii_input.rs +274 -0
  127. package/crates/enact-core/src/policy/tenant_policy.rs +453 -0
  128. package/crates/enact-core/src/policy/tool_policy.rs +407 -0
  129. package/crates/enact-core/src/providers/mod.rs +63 -0
  130. package/crates/enact-core/src/providers/trait.rs +292 -0
  131. package/crates/enact-core/src/runner/callbacks.rs +6 -0
  132. package/crates/enact-core/src/runner/execution_runner.rs +476 -0
  133. package/crates/enact-core/src/runner/loop.rs +117 -0
  134. package/crates/enact-core/src/runner/mod.rs +58 -0
  135. package/crates/enact-core/src/runner/protected_runner.rs +280 -0
  136. package/crates/enact-core/src/signal/inmemory.rs +231 -0
  137. package/crates/enact-core/src/signal/mod.rs +108 -0
  138. package/crates/enact-core/src/streaming/event_logger.rs +195 -0
  139. package/crates/enact-core/src/streaming/event_stream.rs +1423 -0
  140. package/crates/enact-core/src/streaming/mod.rs +108 -0
  141. package/crates/enact-core/src/streaming/pause_cancel.rs +0 -0
  142. package/crates/enact-core/src/streaming/protected_emitter.rs +173 -0
  143. package/crates/enact-core/src/streaming/protection/context.rs +136 -0
  144. package/crates/enact-core/src/streaming/protection/encryption.rs +289 -0
  145. package/crates/enact-core/src/streaming/protection/mod.rs +43 -0
  146. package/crates/enact-core/src/streaming/protection/pii_protection.rs +243 -0
  147. package/crates/enact-core/src/streaming/protection/processor.rs +166 -0
  148. package/crates/enact-core/src/streaming/sse.rs +0 -0
  149. package/crates/enact-core/src/telemetry/exporter.rs +0 -0
  150. package/crates/enact-core/src/telemetry/init.rs +0 -0
  151. package/crates/enact-core/src/telemetry/mod.rs +49 -0
  152. package/crates/enact-core/src/telemetry/spans.rs +245 -0
  153. package/crates/enact-core/src/tool/agent_tool.rs +177 -0
  154. package/crates/enact-core/src/tool/browser/mod.rs +0 -0
  155. package/crates/enact-core/src/tool/browser/webdriver.rs +0 -0
  156. package/crates/enact-core/src/tool/cost.rs +247 -0
  157. package/crates/enact-core/src/tool/discovery.rs +0 -0
  158. package/crates/enact-core/src/tool/dispatcher.rs +347 -0
  159. package/crates/enact-core/src/tool/filesystem.rs +231 -0
  160. package/crates/enact-core/src/tool/function.rs +99 -0
  161. package/crates/enact-core/src/tool/git.rs +162 -0
  162. package/crates/enact-core/src/tool/http.rs +214 -0
  163. package/crates/enact-core/src/tool/mcp/client.rs +0 -0
  164. package/crates/enact-core/src/tool/mcp/mod.rs +0 -0
  165. package/crates/enact-core/src/tool/mod.rs +51 -0
  166. package/crates/enact-core/src/tool/reasoning/debugging.rs +0 -0
  167. package/crates/enact-core/src/tool/reasoning/mcts.rs +0 -0
  168. package/crates/enact-core/src/tool/reasoning/mod.rs +0 -0
  169. package/crates/enact-core/src/tool/reasoning/sequential.rs +0 -0
  170. package/crates/enact-core/src/tool/sandbox/dagger.rs +0 -0
  171. package/crates/enact-core/src/tool/sandbox/mod.rs +0 -0
  172. package/crates/enact-core/src/tool/shell.rs +147 -0
  173. package/crates/enact-core/src/tool/trait.rs +33 -0
  174. package/crates/enact-core/src/tool/web_search.rs +277 -0
  175. package/crates/enact-core/src/util/config.rs +0 -0
  176. package/crates/enact-core/src/util/errors.rs +0 -0
  177. package/crates/enact-core/src/util/mod.rs +6 -0
  178. package/crates/enact-core/tests/airgapped_e2e_test.rs +291 -0
  179. package/crates/enact-core/tests/e2e_agentic_loop.rs +119 -0
  180. package/crates/enact-core/tests/e2e_test.rs +259 -0
  181. package/crates/enact-core/tests/graph_test.rs +130 -0
  182. package/crates/enact-core/tests/stream_event_id_validation.rs +435 -0
  183. package/crates/enact-cron/Cargo.toml +28 -0
  184. package/crates/enact-cron/src/lib.rs +44 -0
  185. package/crates/enact-cron/src/schedule.rs +156 -0
  186. package/crates/enact-cron/src/store.rs +589 -0
  187. package/crates/enact-cron/src/types.rs +148 -0
  188. package/crates/enact-gateway/Cargo.toml +31 -0
  189. package/crates/enact-gateway/README.md +30 -0
  190. package/crates/enact-gateway/examples/whatsapp-gateway-runner-mock.rs +59 -0
  191. package/crates/enact-gateway/examples/whatsapp-gateway.rs +42 -0
  192. package/crates/enact-gateway/src/lib.rs +582 -0
  193. package/crates/enact-mcp/Cargo.toml +24 -0
  194. package/crates/enact-mcp/src/lib.rs +178 -0
  195. package/crates/enact-memory/Cargo.toml +25 -0
  196. package/crates/enact-memory/src/backend.rs +20 -0
  197. package/crates/enact-memory/src/chunker.rs +230 -0
  198. package/crates/enact-memory/src/embeddings.rs +221 -0
  199. package/crates/enact-memory/src/lib.rs +67 -0
  200. package/crates/enact-memory/src/markdown.rs +127 -0
  201. package/crates/enact-memory/src/none.rs +61 -0
  202. package/crates/enact-memory/src/sqlite.rs +276 -0
  203. package/crates/enact-memory/src/traits.rs +65 -0
  204. package/crates/enact-memory/src/vector.rs +198 -0
  205. package/crates/enact-oauth/Cargo.toml +27 -0
  206. package/crates/enact-oauth/src/lib.rs +584 -0
  207. package/crates/enact-observability/Cargo.toml +22 -0
  208. package/crates/enact-observability/src/lib.rs +197 -0
  209. package/crates/enact-providers/Cargo.toml +33 -0
  210. package/crates/enact-providers/examples/hello-agent.rs +33 -0
  211. package/crates/enact-providers/src/anthropic.rs +182 -0
  212. package/crates/enact-providers/src/azure.rs +96 -0
  213. package/crates/enact-providers/src/bridge.rs +221 -0
  214. package/crates/enact-providers/src/gemini.rs +227 -0
  215. package/crates/enact-providers/src/http.rs +78 -0
  216. package/crates/enact-providers/src/lib.rs +53 -0
  217. package/crates/enact-providers/src/openai_compatible.rs +167 -0
  218. package/crates/enact-providers/src/openrouter.rs +33 -0
  219. package/crates/enact-runner/Cargo.toml +24 -0
  220. package/crates/enact-runner/README.md +76 -0
  221. package/crates/enact-runner/src/compaction.rs +225 -0
  222. package/crates/enact-runner/src/config.rs +118 -0
  223. package/crates/enact-runner/src/lib.rs +63 -0
  224. package/crates/enact-runner/src/loop_driver.rs +414 -0
  225. package/crates/enact-runner/src/parser.rs +421 -0
  226. package/crates/enact-runner/src/retry.rs +262 -0
  227. package/crates/enact-runner/tests/integration.rs +278 -0
  228. package/crates/enact-security/Cargo.toml +22 -0
  229. package/crates/enact-security/src/audit.rs +375 -0
  230. package/crates/enact-security/src/lib.rs +37 -0
  231. package/crates/enact-security/src/policy.rs +406 -0
  232. package/crates/enact-skills/Cargo.toml +25 -0
  233. package/crates/enact-skills/src/lib.rs +506 -0
  234. package/crates/enact-tools/Cargo.toml +22 -0
  235. package/crates/enact-tools/src/file_read.rs +166 -0
  236. package/crates/enact-tools/src/file_write.rs +216 -0
  237. package/crates/enact-tools/src/git_operations.rs +513 -0
  238. package/crates/enact-tools/src/http_request.rs +417 -0
  239. package/crates/enact-tools/src/lib.rs +104 -0
  240. package/crates/enact-tools/src/security.rs +227 -0
  241. package/crates/enact-tools/src/shell.rs +191 -0
  242. package/crates/enact-tools/src/traits.rs +159 -0
  243. package/docs/Makefile +74 -0
  244. package/docs/config.toml +62 -0
  245. package/docs/content/_index.md +174 -0
  246. package/docs/content/a2a/_index.md +431 -0
  247. package/docs/content/api/_index.md +323 -0
  248. package/docs/content/channels/_index.md +160 -0
  249. package/docs/content/channels/teams.md +205 -0
  250. package/docs/content/channels/telegram.md +182 -0
  251. package/docs/content/channels/webhook.md +423 -0
  252. package/docs/content/channels/whatsapp.md +240 -0
  253. package/docs/content/cli/_index.md +261 -0
  254. package/docs/content/concepts/_index.md +273 -0
  255. package/docs/content/configuration/_index.md +241 -0
  256. package/docs/content/cron/_index.md +248 -0
  257. package/docs/content/developers/_index.md +278 -0
  258. package/docs/content/getting-started/_index.md +180 -0
  259. package/docs/content/installation/_index.md +186 -0
  260. package/docs/content/installation/uninstall.md +101 -0
  261. package/docs/content/installation/updating.md +120 -0
  262. package/docs/content/mcp/_index.md +215 -0
  263. package/docs/content/memory/_index.md +163 -0
  264. package/docs/content/oauth/_index.md +515 -0
  265. package/docs/content/providers/_index.md +206 -0
  266. package/docs/content/roadmap/_index.md +199 -0
  267. package/docs/content/security/_index.md +219 -0
  268. package/docs/content/skills/_index.md +228 -0
  269. package/docs/content/tools/_index.md +485 -0
  270. package/docs/content/troubleshooting/_index.md +259 -0
  271. package/docs/content/yaml-schema/_index.md +294 -0
  272. package/docs/static/giallo-dark.css +91 -0
  273. package/docs/static/giallo-light.css +91 -0
  274. package/docs/themes/tanuki/.github/workflows/deploy.yml +44 -0
  275. package/docs/themes/tanuki/LICENSE +21 -0
  276. package/docs/themes/tanuki/README.md +166 -0
  277. package/docs/themes/tanuki/examples/blog/config.toml +58 -0
  278. package/docs/themes/tanuki/examples/blog/content/_index.md +4 -0
  279. package/docs/themes/tanuki/examples/blog/content/about.md +33 -0
  280. package/docs/themes/tanuki/examples/blog/content/blog/_index.md +7 -0
  281. package/docs/themes/tanuki/examples/blog/content/blog/api-design-best-practices.md +245 -0
  282. package/docs/themes/tanuki/examples/blog/content/blog/building-accessible-websites.md +147 -0
  283. package/docs/themes/tanuki/examples/blog/content/blog/css-grid-vs-flexbox.md +165 -0
  284. package/docs/themes/tanuki/examples/blog/content/blog/customizing-catppuccin-colors.md +137 -0
  285. package/docs/themes/tanuki/examples/blog/content/blog/dark-mode-best-practices.md +82 -0
  286. package/docs/themes/tanuki/examples/blog/content/blog/docker-essentials.md +301 -0
  287. package/docs/themes/tanuki/examples/blog/content/blog/getting-started-with-zola.md +129 -0
  288. package/docs/themes/tanuki/examples/blog/content/blog/git-workflow-for-content.md +112 -0
  289. package/docs/themes/tanuki/examples/blog/content/blog/introduction-to-webassembly.md +183 -0
  290. package/docs/themes/tanuki/examples/blog/content/blog/modern-javascript-features.md +234 -0
  291. package/docs/themes/tanuki/examples/blog/content/blog/testing-strategies.md +311 -0
  292. package/docs/themes/tanuki/examples/blog/content/blog/typography-for-developers.md +104 -0
  293. package/docs/themes/tanuki/examples/blog/content/blog/welcome-to-tanuki.md +67 -0
  294. package/docs/themes/tanuki/examples/blog/content/blog/why-static-sites.md +85 -0
  295. package/docs/themes/tanuki/examples/blog/content/projects.md +64 -0
  296. package/docs/themes/tanuki/examples/book/config.toml +17 -0
  297. package/docs/themes/tanuki/examples/book/content/_index.md +12 -0
  298. package/docs/themes/tanuki/examples/book/content/chapter-1.md +90 -0
  299. package/docs/themes/tanuki/examples/book/content/chapter-2.md +143 -0
  300. package/docs/themes/tanuki/examples/book/content/chapter-3.md +217 -0
  301. package/docs/themes/tanuki/examples/book/content/chapter-4.md +224 -0
  302. package/docs/themes/tanuki/examples/book/content/chapter-5.md +297 -0
  303. package/docs/themes/tanuki/examples/book/content/print.md +6 -0
  304. package/docs/themes/tanuki/examples/docs/config.toml +28 -0
  305. package/docs/themes/tanuki/examples/docs/content/_index.md +20 -0
  306. package/docs/themes/tanuki/examples/docs/content/components.md +156 -0
  307. package/docs/themes/tanuki/examples/docs/content/configuration.md +94 -0
  308. package/docs/themes/tanuki/examples/docs/content/customization.md +202 -0
  309. package/docs/themes/tanuki/examples/docs/content/deployment.md +204 -0
  310. package/docs/themes/tanuki/examples/docs/content/installation.md +59 -0
  311. package/docs/themes/tanuki/examples/docs/content/print.md +6 -0
  312. package/docs/themes/tanuki/examples/docs/static/img/tanuki-icon.avif +0 -0
  313. package/docs/themes/tanuki/examples/index.html +2104 -0
  314. package/docs/themes/tanuki/mise.toml +108 -0
  315. package/docs/themes/tanuki/sass/base/_catppuccin.scss +164 -0
  316. package/docs/themes/tanuki/sass/base/_fonts.scss +64 -0
  317. package/docs/themes/tanuki/sass/base/_reset.scss +152 -0
  318. package/docs/themes/tanuki/sass/base/_typography.scss +523 -0
  319. package/docs/themes/tanuki/sass/components/_buttons.scss +209 -0
  320. package/docs/themes/tanuki/sass/components/_code.scss +457 -0
  321. package/docs/themes/tanuki/sass/components/_landing.scss +633 -0
  322. package/docs/themes/tanuki/sass/components/_layout.scss +294 -0
  323. package/docs/themes/tanuki/sass/components/_navigation.scss +1200 -0
  324. package/docs/themes/tanuki/sass/components/_print.scss +237 -0
  325. package/docs/themes/tanuki/sass/components/_search.scss +224 -0
  326. package/docs/themes/tanuki/sass/components/_sidebar.scss +473 -0
  327. package/docs/themes/tanuki/sass/components/_theme-toggle.scss +186 -0
  328. package/docs/themes/tanuki/sass/modes/_blog.scss +366 -0
  329. package/docs/themes/tanuki/sass/modes/_product.scss +875 -0
  330. package/docs/themes/tanuki/sass/modes/_raskell.scss +1696 -0
  331. package/docs/themes/tanuki/sass/patterns/_buttons.scss +183 -0
  332. package/docs/themes/tanuki/sass/patterns/_cards.scss +144 -0
  333. package/docs/themes/tanuki/sass/patterns/_index.scss +9 -0
  334. package/docs/themes/tanuki/sass/patterns/_lists.scss +259 -0
  335. package/docs/themes/tanuki/sass/patterns/_sections.scss +243 -0
  336. package/docs/themes/tanuki/sass/style.scss +47 -0
  337. package/docs/themes/tanuki/sass/tokens/_colors.scss +139 -0
  338. package/docs/themes/tanuki/sass/tokens/_spacing.scss +100 -0
  339. package/docs/themes/tanuki/sass/tokens/_typography.scss +186 -0
  340. package/docs/themes/tanuki/screenshot.png +0 -0
  341. package/docs/themes/tanuki/sentinel.kdl +59 -0
  342. package/docs/themes/tanuki/static/elasticlunr.min.js +10 -0
  343. package/docs/themes/tanuki/static/fonts/GEIST-LICENSE.txt +92 -0
  344. package/docs/themes/tanuki/static/fonts/Geist-Variable.woff2 +0 -0
  345. package/docs/themes/tanuki/static/fonts/GeistMono-Variable.woff2 +0 -0
  346. package/docs/themes/tanuki/static/img/tanuki-icon.avif +0 -0
  347. package/docs/themes/tanuki/static/img/tanuki-icon.png +0 -0
  348. package/docs/themes/tanuki/static/js/anchors.js +18 -0
  349. package/docs/themes/tanuki/static/js/app.js +274 -0
  350. package/docs/themes/tanuki/static/js/code.js +394 -0
  351. package/docs/themes/tanuki/static/js/navigation.js +778 -0
  352. package/docs/themes/tanuki/static/js/scroll-to-top.js +33 -0
  353. package/docs/themes/tanuki/static/js/search-raskell.js +240 -0
  354. package/docs/themes/tanuki/static/js/search.js +215 -0
  355. package/docs/themes/tanuki/static/js/theme.js +169 -0
  356. package/docs/themes/tanuki/static/syntax-dark.css +151 -0
  357. package/docs/themes/tanuki/static/syntax-light.css +151 -0
  358. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm.js +486 -0
  359. package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm_bg.wasm +0 -0
  360. package/docs/themes/tanuki/templates/404.html +52 -0
  361. package/docs/themes/tanuki/templates/base.html +428 -0
  362. package/docs/themes/tanuki/templates/blog.html +66 -0
  363. package/docs/themes/tanuki/templates/home.html +108 -0
  364. package/docs/themes/tanuki/templates/index.html +178 -0
  365. package/docs/themes/tanuki/templates/landing.html +168 -0
  366. package/docs/themes/tanuki/templates/macros/nav.html +128 -0
  367. package/docs/themes/tanuki/templates/macros/posts.html +101 -0
  368. package/docs/themes/tanuki/templates/macros/ui.html +159 -0
  369. package/docs/themes/tanuki/templates/page.html +135 -0
  370. package/docs/themes/tanuki/templates/partials/footer.html +38 -0
  371. package/docs/themes/tanuki/templates/partials/header.html +366 -0
  372. package/docs/themes/tanuki/templates/partials/nav-buttons.html +55 -0
  373. package/docs/themes/tanuki/templates/partials/nav-overlay.html +81 -0
  374. package/docs/themes/tanuki/templates/partials/page-toc-panel.html +43 -0
  375. package/docs/themes/tanuki/templates/partials/search.html +52 -0
  376. package/docs/themes/tanuki/templates/partials/sidebar.html +107 -0
  377. package/docs/themes/tanuki/templates/partials/theme-toggle.html +35 -0
  378. package/docs/themes/tanuki/templates/partials/toc-overlay.html +146 -0
  379. package/docs/themes/tanuki/templates/partials/version-picker.html +38 -0
  380. package/docs/themes/tanuki/templates/print.html +244 -0
  381. package/docs/themes/tanuki/templates/section.html +186 -0
  382. package/docs/themes/tanuki/templates/taxonomy_list.html +18 -0
  383. package/docs/themes/tanuki/templates/taxonomy_single.html +31 -0
  384. package/docs/themes/tanuki/theme.toml +58 -0
  385. package/examples/hello-agent.rs +55 -0
  386. package/package.json +36 -0
  387. package/proto/config.proto +60 -0
  388. package/proto/events.proto +0 -0
  389. package/proto/runtime.proto +215 -0
@@ -0,0 +1,589 @@
1
+ //! SQLite-based storage for cron jobs
2
+
3
+ use crate::schedule::{next_run_for_schedule, schedule_cron_expression, validate_schedule};
4
+ use crate::types::{
5
+ CronJob, CronJobPatch, CronRun, DeliveryConfig, JobType, Schedule, SessionTarget,
6
+ };
7
+ use anyhow::{Context, Result};
8
+ use chrono::{DateTime, Utc};
9
+ use rusqlite::{params, Connection};
10
+ use std::path::Path;
11
+ use uuid::Uuid;
12
+
13
+ const MAX_CRON_OUTPUT_BYTES: usize = 16 * 1024;
14
+ const TRUNCATED_OUTPUT_MARKER: &str = "\n...[truncated]";
15
+
16
+ /// Cron job store
17
+ pub struct CronStore {
18
+ db_path: std::path::PathBuf,
19
+ max_run_history: u32,
20
+ max_tasks: usize,
21
+ }
22
+
23
+ impl CronStore {
24
+ /// Create a new cron store
25
+ pub fn new(workspace_dir: &Path, max_run_history: u32, max_tasks: usize) -> Result<Self> {
26
+ let db_path = workspace_dir.join("cron").join("jobs.db");
27
+ if let Some(parent) = db_path.parent() {
28
+ std::fs::create_dir_all(parent)
29
+ .with_context(|| format!("Failed to create cron directory: {}", parent.display()))?;
30
+ }
31
+
32
+ let store = Self {
33
+ db_path,
34
+ max_run_history,
35
+ max_tasks,
36
+ };
37
+
38
+ // Initialize schema
39
+ store.with_connection(|_| Ok(()))?;
40
+
41
+ Ok(store)
42
+ }
43
+
44
+ /// Add a simple shell job with cron expression
45
+ pub fn add_job(&self, expression: &str, command: &str) -> Result<CronJob> {
46
+ let schedule = Schedule::Cron {
47
+ expr: expression.to_string(),
48
+ tz: None,
49
+ };
50
+ self.add_shell_job(None, schedule, command)
51
+ }
52
+
53
+ /// Add a shell job with a custom schedule
54
+ pub fn add_shell_job(
55
+ &self,
56
+ name: Option<String>,
57
+ schedule: Schedule,
58
+ command: &str,
59
+ ) -> Result<CronJob> {
60
+ let now = Utc::now();
61
+ validate_schedule(&schedule, now)?;
62
+ let next_run = next_run_for_schedule(&schedule, now)?;
63
+ let id = Uuid::new_v4().to_string();
64
+ let expression = schedule_cron_expression(&schedule).unwrap_or_default();
65
+ let schedule_json = serde_json::to_string(&schedule)?;
66
+
67
+ self.with_connection(|conn| {
68
+ conn.execute(
69
+ "INSERT INTO cron_jobs (
70
+ id, expression, command, schedule, job_type, prompt, name, session_target, model,
71
+ enabled, delivery, delete_after_run, created_at, next_run
72
+ ) VALUES (?1, ?2, ?3, ?4, 'shell', NULL, ?5, 'isolated', NULL, 1, ?6, 0, ?7, ?8)",
73
+ params![
74
+ id,
75
+ expression,
76
+ command,
77
+ schedule_json,
78
+ name,
79
+ serde_json::to_string(&DeliveryConfig::default())?,
80
+ now.to_rfc3339(),
81
+ next_run.to_rfc3339(),
82
+ ],
83
+ )
84
+ .context("Failed to insert cron shell job")?;
85
+ Ok(())
86
+ })?;
87
+
88
+ self.get_job(&id)
89
+ }
90
+
91
+ /// Add an agent job
92
+ #[allow(clippy::too_many_arguments)]
93
+ pub fn add_agent_job(
94
+ &self,
95
+ name: Option<String>,
96
+ schedule: Schedule,
97
+ prompt: &str,
98
+ session_target: SessionTarget,
99
+ model: Option<String>,
100
+ delivery: Option<DeliveryConfig>,
101
+ delete_after_run: bool,
102
+ ) -> Result<CronJob> {
103
+ let now = Utc::now();
104
+ validate_schedule(&schedule, now)?;
105
+ let next_run = next_run_for_schedule(&schedule, now)?;
106
+ let id = Uuid::new_v4().to_string();
107
+ let expression = schedule_cron_expression(&schedule).unwrap_or_default();
108
+ let schedule_json = serde_json::to_string(&schedule)?;
109
+ let delivery = delivery.unwrap_or_default();
110
+
111
+ self.with_connection(|conn| {
112
+ conn.execute(
113
+ "INSERT INTO cron_jobs (
114
+ id, expression, command, schedule, job_type, prompt, name, session_target, model,
115
+ enabled, delivery, delete_after_run, created_at, next_run
116
+ ) VALUES (?1, ?2, '', ?3, 'agent', ?4, ?5, ?6, ?7, 1, ?8, ?9, ?10, ?11)",
117
+ params![
118
+ id,
119
+ expression,
120
+ schedule_json,
121
+ prompt,
122
+ name,
123
+ session_target.as_str(),
124
+ model,
125
+ serde_json::to_string(&delivery)?,
126
+ if delete_after_run { 1 } else { 0 },
127
+ now.to_rfc3339(),
128
+ next_run.to_rfc3339(),
129
+ ],
130
+ )
131
+ .context("Failed to insert cron agent job")?;
132
+ Ok(())
133
+ })?;
134
+
135
+ self.get_job(&id)
136
+ }
137
+
138
+ /// List all jobs
139
+ pub fn list_jobs(&self) -> Result<Vec<CronJob>> {
140
+ self.with_connection(|conn| {
141
+ let mut stmt = conn.prepare(
142
+ "SELECT id, expression, command, schedule, job_type, prompt, name, session_target, model,
143
+ enabled, delivery, delete_after_run, created_at, next_run, last_run, last_status, last_output
144
+ FROM cron_jobs ORDER BY next_run ASC",
145
+ )?;
146
+
147
+ let rows = stmt.query_map([], map_cron_job_row)?;
148
+
149
+ let mut jobs = Vec::new();
150
+ for row in rows {
151
+ jobs.push(row?);
152
+ }
153
+ Ok(jobs)
154
+ })
155
+ }
156
+
157
+ /// Get a job by ID
158
+ pub fn get_job(&self, job_id: &str) -> Result<CronJob> {
159
+ self.with_connection(|conn| {
160
+ let mut stmt = conn.prepare(
161
+ "SELECT id, expression, command, schedule, job_type, prompt, name, session_target, model,
162
+ enabled, delivery, delete_after_run, created_at, next_run, last_run, last_status, last_output
163
+ FROM cron_jobs WHERE id = ?1",
164
+ )?;
165
+
166
+ let mut rows = stmt.query(params![job_id])?;
167
+ if let Some(row) = rows.next()? {
168
+ map_cron_job_row(row).map_err(Into::into)
169
+ } else {
170
+ anyhow::bail!("Cron job '{job_id}' not found")
171
+ }
172
+ })
173
+ }
174
+
175
+ /// Remove a job
176
+ pub fn remove_job(&self, id: &str) -> Result<()> {
177
+ let changed = self.with_connection(|conn| {
178
+ conn.execute("DELETE FROM cron_jobs WHERE id = ?1", params![id])
179
+ .context("Failed to delete cron job")
180
+ })?;
181
+
182
+ if changed == 0 {
183
+ anyhow::bail!("Cron job '{id}' not found");
184
+ }
185
+
186
+ Ok(())
187
+ }
188
+
189
+ /// Get jobs that are due to run
190
+ pub fn due_jobs(&self, now: DateTime<Utc>) -> Result<Vec<CronJob>> {
191
+ let lim = i64::try_from(self.max_tasks.max(1))
192
+ .context("max_tasks overflows i64")?;
193
+ self.with_connection(|conn| {
194
+ let mut stmt = conn.prepare(
195
+ "SELECT id, expression, command, schedule, job_type, prompt, name, session_target, model,
196
+ enabled, delivery, delete_after_run, created_at, next_run, last_run, last_status, last_output
197
+ FROM cron_jobs
198
+ WHERE enabled = 1 AND next_run <= ?1
199
+ ORDER BY next_run ASC
200
+ LIMIT ?2",
201
+ )?;
202
+
203
+ let rows = stmt.query_map(params![now.to_rfc3339(), lim], map_cron_job_row)?;
204
+
205
+ let mut jobs = Vec::new();
206
+ for row in rows {
207
+ jobs.push(row?);
208
+ }
209
+ Ok(jobs)
210
+ })
211
+ }
212
+
213
+ /// Update a job
214
+ pub fn update_job(&self, job_id: &str, patch: CronJobPatch) -> Result<CronJob> {
215
+ let mut job = self.get_job(job_id)?;
216
+ let mut schedule_changed = false;
217
+
218
+ if let Some(schedule) = patch.schedule {
219
+ validate_schedule(&schedule, Utc::now())?;
220
+ job.schedule = schedule;
221
+ job.expression = schedule_cron_expression(&job.schedule).unwrap_or_default();
222
+ schedule_changed = true;
223
+ }
224
+ if let Some(command) = patch.command {
225
+ job.command = command;
226
+ }
227
+ if let Some(prompt) = patch.prompt {
228
+ job.prompt = Some(prompt);
229
+ }
230
+ if let Some(name) = patch.name {
231
+ job.name = Some(name);
232
+ }
233
+ if let Some(enabled) = patch.enabled {
234
+ job.enabled = enabled;
235
+ }
236
+ if let Some(delivery) = patch.delivery {
237
+ job.delivery = delivery;
238
+ }
239
+ if let Some(model) = patch.model {
240
+ job.model = Some(model);
241
+ }
242
+ if let Some(target) = patch.session_target {
243
+ job.session_target = target;
244
+ }
245
+ if let Some(delete_after_run) = patch.delete_after_run {
246
+ job.delete_after_run = delete_after_run;
247
+ }
248
+
249
+ if schedule_changed {
250
+ job.next_run = next_run_for_schedule(&job.schedule, Utc::now())?;
251
+ }
252
+
253
+ self.with_connection(|conn| {
254
+ conn.execute(
255
+ "UPDATE cron_jobs
256
+ SET expression = ?1, command = ?2, schedule = ?3, job_type = ?4, prompt = ?5, name = ?6,
257
+ session_target = ?7, model = ?8, enabled = ?9, delivery = ?10, delete_after_run = ?11,
258
+ next_run = ?12
259
+ WHERE id = ?13",
260
+ params![
261
+ job.expression,
262
+ job.command,
263
+ serde_json::to_string(&job.schedule)?,
264
+ job.job_type.as_str(),
265
+ job.prompt,
266
+ job.name,
267
+ job.session_target.as_str(),
268
+ job.model,
269
+ if job.enabled { 1 } else { 0 },
270
+ serde_json::to_string(&job.delivery)?,
271
+ if job.delete_after_run { 1 } else { 0 },
272
+ job.next_run.to_rfc3339(),
273
+ job.id,
274
+ ],
275
+ )
276
+ .context("Failed to update cron job")?;
277
+ Ok(())
278
+ })?;
279
+
280
+ self.get_job(job_id)
281
+ }
282
+
283
+ /// Record the last run status
284
+ pub fn record_last_run(
285
+ &self,
286
+ job_id: &str,
287
+ finished_at: DateTime<Utc>,
288
+ success: bool,
289
+ output: &str,
290
+ ) -> Result<()> {
291
+ let status = if success { "ok" } else { "error" };
292
+ let bounded_output = truncate_cron_output(output);
293
+ self.with_connection(|conn| {
294
+ conn.execute(
295
+ "UPDATE cron_jobs
296
+ SET last_run = ?1, last_status = ?2, last_output = ?3
297
+ WHERE id = ?4",
298
+ params![finished_at.to_rfc3339(), status, bounded_output, job_id],
299
+ )
300
+ .context("Failed to update cron last run fields")?;
301
+ Ok(())
302
+ })
303
+ }
304
+
305
+ /// Reschedule a job after a run
306
+ pub fn reschedule_after_run(&self, job: &CronJob, success: bool, output: &str) -> Result<()> {
307
+ let now = Utc::now();
308
+ let next_run = next_run_for_schedule(&job.schedule, now)?;
309
+ let status = if success { "ok" } else { "error" };
310
+ let bounded_output = truncate_cron_output(output);
311
+
312
+ self.with_connection(|conn| {
313
+ conn.execute(
314
+ "UPDATE cron_jobs
315
+ SET next_run = ?1, last_run = ?2, last_status = ?3, last_output = ?4
316
+ WHERE id = ?5",
317
+ params![
318
+ next_run.to_rfc3339(),
319
+ now.to_rfc3339(),
320
+ status,
321
+ bounded_output,
322
+ job.id
323
+ ],
324
+ )
325
+ .context("Failed to update cron job run state")?;
326
+ Ok(())
327
+ })
328
+ }
329
+
330
+ /// Record a job run in the history
331
+ pub fn record_run(
332
+ &self,
333
+ job_id: &str,
334
+ started_at: DateTime<Utc>,
335
+ finished_at: DateTime<Utc>,
336
+ status: &str,
337
+ output: Option<&str>,
338
+ duration_ms: i64,
339
+ ) -> Result<()> {
340
+ let bounded_output = output.map(truncate_cron_output);
341
+ let max_run_history = self.max_run_history;
342
+
343
+ self.with_connection(|conn| {
344
+ let tx = conn.unchecked_transaction()?;
345
+
346
+ tx.execute(
347
+ "INSERT INTO cron_runs (job_id, started_at, finished_at, status, output, duration_ms)
348
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
349
+ params![
350
+ job_id,
351
+ started_at.to_rfc3339(),
352
+ finished_at.to_rfc3339(),
353
+ status,
354
+ bounded_output.as_deref(),
355
+ duration_ms,
356
+ ],
357
+ )
358
+ .context("Failed to insert cron run")?;
359
+
360
+ let keep = i64::from(max_run_history.max(1));
361
+ tx.execute(
362
+ "DELETE FROM cron_runs
363
+ WHERE job_id = ?1
364
+ AND id NOT IN (
365
+ SELECT id FROM cron_runs
366
+ WHERE job_id = ?1
367
+ ORDER BY started_at DESC, id DESC
368
+ LIMIT ?2
369
+ )",
370
+ params![job_id, keep],
371
+ )
372
+ .context("Failed to prune cron run history")?;
373
+
374
+ tx.commit()
375
+ .context("Failed to commit cron run transaction")?;
376
+ Ok(())
377
+ })
378
+ }
379
+
380
+ /// List runs for a job
381
+ pub fn list_runs(&self, job_id: &str, limit: usize) -> Result<Vec<CronRun>> {
382
+ self.with_connection(|conn| {
383
+ let lim = i64::try_from(limit.max(1)).context("Run history limit overflow")?;
384
+ let mut stmt = conn.prepare(
385
+ "SELECT id, job_id, started_at, finished_at, status, output, duration_ms
386
+ FROM cron_runs
387
+ WHERE job_id = ?1
388
+ ORDER BY started_at DESC, id DESC
389
+ LIMIT ?2",
390
+ )?;
391
+
392
+ let rows = stmt.query_map(params![job_id, lim], |row| {
393
+ Ok(CronRun {
394
+ id: row.get(0)?,
395
+ job_id: row.get(1)?,
396
+ started_at: parse_rfc3339(&row.get::<_, String>(2)?)
397
+ .map_err(sql_conversion_error)?,
398
+ finished_at: parse_rfc3339(&row.get::<_, String>(3)?)
399
+ .map_err(sql_conversion_error)?,
400
+ status: row.get(4)?,
401
+ output: row.get(5)?,
402
+ duration_ms: row.get(6)?,
403
+ })
404
+ })?;
405
+
406
+ let mut runs = Vec::new();
407
+ for row in rows {
408
+ runs.push(row?);
409
+ }
410
+ Ok(runs)
411
+ })
412
+ }
413
+
414
+ fn with_connection<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
415
+ let conn = Connection::open(&self.db_path)
416
+ .with_context(|| format!("Failed to open cron DB: {}", self.db_path.display()))?;
417
+
418
+ conn.execute_batch(
419
+ "PRAGMA foreign_keys = ON;
420
+ CREATE TABLE IF NOT EXISTS cron_jobs (
421
+ id TEXT PRIMARY KEY,
422
+ expression TEXT NOT NULL,
423
+ command TEXT NOT NULL,
424
+ schedule TEXT,
425
+ job_type TEXT NOT NULL DEFAULT 'shell',
426
+ prompt TEXT,
427
+ name TEXT,
428
+ session_target TEXT NOT NULL DEFAULT 'isolated',
429
+ model TEXT,
430
+ enabled INTEGER NOT NULL DEFAULT 1,
431
+ delivery TEXT,
432
+ delete_after_run INTEGER NOT NULL DEFAULT 0,
433
+ created_at TEXT NOT NULL,
434
+ next_run TEXT NOT NULL,
435
+ last_run TEXT,
436
+ last_status TEXT,
437
+ last_output TEXT
438
+ );
439
+ CREATE INDEX IF NOT EXISTS idx_cron_jobs_next_run ON cron_jobs(next_run);
440
+
441
+ CREATE TABLE IF NOT EXISTS cron_runs (
442
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
443
+ job_id TEXT NOT NULL,
444
+ started_at TEXT NOT NULL,
445
+ finished_at TEXT NOT NULL,
446
+ status TEXT NOT NULL,
447
+ output TEXT,
448
+ duration_ms INTEGER,
449
+ FOREIGN KEY (job_id) REFERENCES cron_jobs(id) ON DELETE CASCADE
450
+ );
451
+ CREATE INDEX IF NOT EXISTS idx_cron_runs_job_id ON cron_runs(job_id);
452
+ CREATE INDEX IF NOT EXISTS idx_cron_runs_started_at ON cron_runs(started_at);
453
+ CREATE INDEX IF NOT EXISTS idx_cron_runs_job_started ON cron_runs(job_id, started_at);",
454
+ )
455
+ .context("Failed to initialize cron schema")?;
456
+
457
+ f(&conn)
458
+ }
459
+ }
460
+
461
+ fn truncate_cron_output(output: &str) -> String {
462
+ if output.len() <= MAX_CRON_OUTPUT_BYTES {
463
+ return output.to_string();
464
+ }
465
+
466
+ if MAX_CRON_OUTPUT_BYTES <= TRUNCATED_OUTPUT_MARKER.len() {
467
+ return TRUNCATED_OUTPUT_MARKER.to_string();
468
+ }
469
+
470
+ let mut cutoff = MAX_CRON_OUTPUT_BYTES - TRUNCATED_OUTPUT_MARKER.len();
471
+ while cutoff > 0 && !output.is_char_boundary(cutoff) {
472
+ cutoff -= 1;
473
+ }
474
+
475
+ let mut truncated = output[..cutoff].to_string();
476
+ truncated.push_str(TRUNCATED_OUTPUT_MARKER);
477
+ truncated
478
+ }
479
+
480
+ fn parse_rfc3339(raw: &str) -> Result<DateTime<Utc>> {
481
+ let parsed = DateTime::parse_from_rfc3339(raw)
482
+ .with_context(|| format!("Invalid RFC3339 timestamp in cron DB: {raw}"))?;
483
+ Ok(parsed.with_timezone(&Utc))
484
+ }
485
+
486
+ fn sql_conversion_error(err: anyhow::Error) -> rusqlite::Error {
487
+ rusqlite::Error::ToSqlConversionFailure(err.into())
488
+ }
489
+
490
+ fn map_cron_job_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<CronJob> {
491
+ let expression: String = row.get(1)?;
492
+ let schedule_raw: Option<String> = row.get(3)?;
493
+ let schedule = decode_schedule(schedule_raw.as_deref(), &expression).map_err(sql_conversion_error)?;
494
+
495
+ let delivery_raw: Option<String> = row.get(10)?;
496
+ let delivery = decode_delivery(delivery_raw.as_deref()).map_err(sql_conversion_error)?;
497
+
498
+ let next_run_raw: String = row.get(13)?;
499
+ let last_run_raw: Option<String> = row.get(14)?;
500
+ let created_at_raw: String = row.get(12)?;
501
+
502
+ Ok(CronJob {
503
+ id: row.get(0)?,
504
+ expression,
505
+ schedule,
506
+ command: row.get(2)?,
507
+ job_type: JobType::parse(&row.get::<_, String>(4)?),
508
+ prompt: row.get(5)?,
509
+ name: row.get(6)?,
510
+ session_target: SessionTarget::parse(&row.get::<_, String>(7)?),
511
+ model: row.get(8)?,
512
+ enabled: row.get::<_, i64>(9)? != 0,
513
+ delivery,
514
+ delete_after_run: row.get::<_, i64>(11)? != 0,
515
+ created_at: parse_rfc3339(&created_at_raw).map_err(sql_conversion_error)?,
516
+ next_run: parse_rfc3339(&next_run_raw).map_err(sql_conversion_error)?,
517
+ last_run: match last_run_raw {
518
+ Some(raw) => Some(parse_rfc3339(&raw).map_err(sql_conversion_error)?),
519
+ None => None,
520
+ },
521
+ last_status: row.get(15)?,
522
+ last_output: row.get(16)?,
523
+ })
524
+ }
525
+
526
+ fn decode_schedule(schedule_raw: Option<&str>, expression: &str) -> Result<Schedule> {
527
+ if let Some(raw) = schedule_raw {
528
+ let trimmed = raw.trim();
529
+ if !trimmed.is_empty() {
530
+ return serde_json::from_str(trimmed)
531
+ .with_context(|| format!("Failed to parse cron schedule JSON: {trimmed}"));
532
+ }
533
+ }
534
+
535
+ if expression.trim().is_empty() {
536
+ anyhow::bail!("Missing schedule and legacy expression for cron job")
537
+ }
538
+
539
+ Ok(Schedule::Cron {
540
+ expr: expression.to_string(),
541
+ tz: None,
542
+ })
543
+ }
544
+
545
+ fn decode_delivery(delivery_raw: Option<&str>) -> Result<DeliveryConfig> {
546
+ if let Some(raw) = delivery_raw {
547
+ let trimmed = raw.trim();
548
+ if !trimmed.is_empty() {
549
+ return serde_json::from_str(trimmed)
550
+ .with_context(|| format!("Failed to parse cron delivery JSON: {trimmed}"));
551
+ }
552
+ }
553
+ Ok(DeliveryConfig::default())
554
+ }
555
+
556
+ #[cfg(test)]
557
+ mod tests {
558
+ use super::*;
559
+ use tempfile::TempDir;
560
+
561
+ fn test_store(tmp: &TempDir) -> CronStore {
562
+ CronStore::new(tmp.path(), 100, 10).unwrap()
563
+ }
564
+
565
+ #[test]
566
+ fn add_job_accepts_five_field_expression() {
567
+ let tmp = TempDir::new().unwrap();
568
+ let store = test_store(&tmp);
569
+
570
+ let job = store.add_job("*/5 * * * *", "echo ok").unwrap();
571
+ assert_eq!(job.expression, "*/5 * * * *");
572
+ assert_eq!(job.command, "echo ok");
573
+ assert!(matches!(job.schedule, Schedule::Cron { .. }));
574
+ }
575
+
576
+ #[test]
577
+ fn add_list_remove_roundtrip() {
578
+ let tmp = TempDir::new().unwrap();
579
+ let store = test_store(&tmp);
580
+
581
+ let job = store.add_job("*/10 * * * *", "echo roundtrip").unwrap();
582
+ let listed = store.list_jobs().unwrap();
583
+ assert_eq!(listed.len(), 1);
584
+ assert_eq!(listed[0].id, job.id);
585
+
586
+ store.remove_job(&job.id).unwrap();
587
+ assert!(store.list_jobs().unwrap().is_empty());
588
+ }
589
+ }