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,298 @@
1
+ //! Enact Configuration Management
2
+ //!
3
+ //! Unified configuration management for Enact with:
4
+ //! - Environment variable support for secrets (checks `ENACT_*` env vars first)
5
+ //! - OS keychain for secrets (API keys, tokens, credentials) with fallback support
6
+ //! - Encrypted file storage for settings (feature flags, timeouts, preferences)
7
+ //! - Cloud sync for authenticated users (respects air-gapped mode)
8
+ //!
9
+ //! # Example
10
+ //!
11
+ //! ```rust,no_run
12
+ //! use enact_config::{ConfigManager, RuntimeMode, default_config_path};
13
+ //!
14
+ //! # async fn example() -> anyhow::Result<()> {
15
+ //! let config_path = default_config_path()?;
16
+ //! let manager = ConfigManager::new(config_path).await?;
17
+ //!
18
+ //! // Load configuration
19
+ //! let config = manager.load().await?;
20
+ //!
21
+ //! // Set a secret (stored in keychain)
22
+ //! manager.set_secret("providers.azure.apiKey", "your-api-key").await?;
23
+ //!
24
+ //! // Set a setting (stored in encrypted file)
25
+ //! let mut config = manager.load().await?;
26
+ //! config.runtime.mode = RuntimeMode::Local;
27
+ //! manager.save(&config).await?;
28
+ //!
29
+ //! // Save configuration
30
+ //! manager.save(&config).await?;
31
+ //! # Ok(())
32
+ //! # }
33
+ //! ```
34
+
35
+ pub mod config;
36
+ pub mod encrypted_store;
37
+ pub mod secrets;
38
+ pub mod sync;
39
+
40
+ pub use config::*;
41
+ pub use encrypted_store::{default_config_path, EncryptedStore};
42
+ pub use secrets::SecretManager;
43
+ pub use sync::{SyncManager, SyncStatus};
44
+
45
+ use anyhow::{Context, Result};
46
+ use serde_json;
47
+ use std::path::PathBuf;
48
+ use tracing::{debug, info};
49
+
50
+ /// Main configuration manager
51
+ pub struct ConfigManager {
52
+ encrypted_store: EncryptedStore,
53
+ secrets: SecretManager,
54
+ sync_manager: Option<SyncManager>,
55
+ }
56
+
57
+ impl ConfigManager {
58
+ /// Create a new configuration manager
59
+ ///
60
+ /// # Arguments
61
+ /// * `config_path` - Path to the encrypted config file
62
+ pub async fn new(config_path: impl Into<PathBuf>) -> Result<Self> {
63
+ let config_path = config_path.into();
64
+
65
+ // In test mode, we might want mock store
66
+ if std::env::var("ENACT_USE_MOCK_SECRET_STORE").is_ok()
67
+ || std::env::var("CARGO_TARGET_TMPDIR").is_ok()
68
+ {
69
+ return Self::new_with_mock_secrets(config_path).await;
70
+ }
71
+
72
+ #[cfg(test)]
73
+ {
74
+ return Self::new_with_mock_secrets(config_path).await;
75
+ }
76
+
77
+ #[cfg(not(test))]
78
+ {
79
+ // Always use SecretManager which handles env vars and .env files
80
+ // No more OS keychain prompts!
81
+ let secrets = SecretManager::new();
82
+ // For EncryptedStore, we need to pass the secret manager if it uses it for encryption keys?
83
+ // EncryptedStore used to take KeychainManager to store the master key.
84
+ // Now that we don't have keychain, where does EncryptedStore get the master key?
85
+ // Usually it generates one and stores it in keychain.
86
+ // If keychain is gone, we must store the master key in .env? ENACT_MASTER_KEY?
87
+
88
+ // I need to check EncryptedStore implementation. It likely calls `keychain.get("enact.master.key")`.
89
+ // With SecretManager, it will look for ENACT_ENACT_MASTER_KEY in env.
90
+ // If not found, EncryptedStore usually generates it. But it can't save it back to env.
91
+ // So EncryptedStore setup might fail or generate a new key every time if not in .env.
92
+ // This effectively means configuration is ephemeral unless ENACT_MASTER_KEY is set.
93
+
94
+ // I'll proceed with updating ConfigManager, and then I MUST check EncryptedStore.
95
+
96
+ let encrypted_store =
97
+ EncryptedStore::new(&config_path).context("Failed to create encrypted store")?;
98
+
99
+ Ok(Self {
100
+ encrypted_store,
101
+ secrets,
102
+ sync_manager: None,
103
+ })
104
+ }
105
+ }
106
+
107
+ /// Create a new configuration manager with a mock secrets for testing
108
+ pub async fn new_with_mock_secrets(config_path: impl Into<PathBuf>) -> Result<Self> {
109
+ let config_path = config_path.into();
110
+ let mock_secrets = SecretManager::new_mock();
111
+ // EncryptedStore needs to be updated to take SecretManager
112
+ // For now, assume EncryptedStore still expects KeychainManager?
113
+ // I need to update EncryptedStore too!
114
+
115
+ // This tool call only updates lib.rs. I'll need to update encrypted_store.rs next.
116
+ // I will temporarily comment out EncryptedStore usage here or assume it's updated.
117
+ // But `EncryptedStore::with_keychain` signature will change.
118
+
119
+ // Let's assume I will rename `with_keychain` to `with_secrets`.
120
+ let encrypted_store = EncryptedStore::with_secrets(&config_path, mock_secrets.clone())
121
+ .context("Failed to create encrypted store")?;
122
+
123
+ Ok(Self {
124
+ encrypted_store,
125
+ secrets: mock_secrets,
126
+ sync_manager: None,
127
+ })
128
+ }
129
+
130
+ /// Create a configuration manager with cloud sync enabled
131
+ pub async fn with_sync(
132
+ config_path: impl Into<PathBuf>,
133
+ api_url: Option<String>,
134
+ tenant_id: Option<String>,
135
+ auto_sync: bool,
136
+ runtime_mode: RuntimeMode,
137
+ ) -> Result<Self> {
138
+ let mut manager = Self::new(config_path).await?;
139
+ manager.sync_manager = Some(SyncManager::new(
140
+ api_url,
141
+ tenant_id,
142
+ auto_sync,
143
+ runtime_mode,
144
+ ));
145
+
146
+ let mut config = manager.load().await?;
147
+ let mut changed = false;
148
+ if config.runtime.mode != runtime_mode {
149
+ config.runtime.mode = runtime_mode;
150
+ changed = true;
151
+ }
152
+
153
+ if matches!(runtime_mode, RuntimeMode::AirGapped) && config.runtime.allow_network {
154
+ config.runtime.allow_network = false;
155
+ changed = true;
156
+ }
157
+
158
+ if changed {
159
+ manager.save(&config).await?;
160
+ }
161
+
162
+ Ok(manager)
163
+ }
164
+
165
+ /// Load configuration from encrypted file
166
+ pub async fn load(&self) -> Result<Config> {
167
+ match self.encrypted_store.load()? {
168
+ Some(json) => {
169
+ let config: Config =
170
+ serde_json::from_str(&json).context("Failed to parse configuration")?;
171
+ debug!("Loaded configuration from encrypted store");
172
+ Ok(config)
173
+ }
174
+ None => {
175
+ debug!("No configuration found, using defaults");
176
+ Ok(Config::default())
177
+ }
178
+ }
179
+ }
180
+
181
+ /// Save configuration to encrypted file
182
+ pub async fn save(&self, config: &Config) -> Result<()> {
183
+ let json =
184
+ serde_json::to_string_pretty(config).context("Failed to serialize configuration")?;
185
+
186
+ self.encrypted_store.save(&json)?;
187
+
188
+ // Auto-sync to cloud if enabled
189
+ if let Some(ref sync_manager) = self.sync_manager {
190
+ if sync_manager.is_enabled() {
191
+ info!("Auto-syncing configuration to cloud");
192
+ if let Err(e) = sync_manager.sync_to_cloud(config).await {
193
+ tracing::warn!("Failed to sync configuration to cloud: {}", e);
194
+ }
195
+ }
196
+ }
197
+
198
+ Ok(())
199
+ }
200
+
201
+ /// Set a secret value
202
+ /// Note: With SecretManager, this might fail if not using mock store.
203
+ /// Users should set secrets in .env.
204
+ pub async fn set_secret(&self, key: &str, value: &str) -> Result<()> {
205
+ self.secrets.set(key, value)?;
206
+ debug!("Set secret: {}", key);
207
+ Ok(())
208
+ }
209
+
210
+ /// Get a secret value
211
+ pub async fn get_secret(&self, key: &str) -> Result<Option<String>> {
212
+ self.secrets.get(key)
213
+ }
214
+
215
+ /// Delete a secret
216
+ pub async fn delete_secret(&self, key: &str) -> Result<()> {
217
+ self.secrets.delete(key)?;
218
+ debug!("Deleted secret: {}", key);
219
+ Ok(())
220
+ }
221
+
222
+ /// Sync configuration from cloud
223
+ pub async fn sync_from_cloud(&self) -> Result<Option<Config>> {
224
+ if let Some(ref sync_manager) = self.sync_manager {
225
+ sync_manager.sync_from_cloud().await
226
+ } else {
227
+ Ok(None)
228
+ }
229
+ }
230
+
231
+ /// Sync configuration to cloud
232
+ pub async fn sync_to_cloud(&self, config: &Config) -> Result<Option<sync::SyncResponse>> {
233
+ if let Some(ref sync_manager) = self.sync_manager {
234
+ sync_manager.sync_to_cloud(config).await
235
+ } else {
236
+ Ok(None)
237
+ }
238
+ }
239
+
240
+ /// Get sync status
241
+ pub fn sync_status(&self) -> SyncStatus {
242
+ self.sync_manager
243
+ .as_ref()
244
+ .map(|m| m.status())
245
+ .unwrap_or(SyncStatus::Disabled)
246
+ }
247
+
248
+ /// Get the config file path
249
+ pub fn config_path(&self) -> &std::path::Path {
250
+ self.encrypted_store.config_path()
251
+ }
252
+ }
253
+
254
+ #[cfg(test)]
255
+ mod tests {
256
+ use super::*;
257
+ use tempfile::TempDir;
258
+
259
+ #[tokio::test]
260
+ async fn test_config_manager() {
261
+ let temp_dir = TempDir::new().unwrap();
262
+ let config_path = temp_dir.path().join("test_config.encrypted");
263
+ let manager = ConfigManager::new_with_mock_secrets(&config_path)
264
+ .await
265
+ .unwrap();
266
+
267
+ // Test default config
268
+ let config = manager.load().await.unwrap();
269
+ assert_eq!(config.runtime.mode, RuntimeMode::Local);
270
+
271
+ // Test save and load
272
+ let mut config = Config::default();
273
+ config.runtime.mode = RuntimeMode::AirGapped;
274
+ manager.save(&config).await.unwrap();
275
+
276
+ let loaded = manager.load().await.unwrap();
277
+ assert_eq!(loaded.runtime.mode, RuntimeMode::AirGapped);
278
+ }
279
+
280
+ #[tokio::test]
281
+ async fn test_secret_management() {
282
+ let temp_dir = TempDir::new().unwrap();
283
+ let config_path = temp_dir.path().join("test_config.encrypted");
284
+ let manager = ConfigManager::new_with_mock_secrets(&config_path)
285
+ .await
286
+ .unwrap();
287
+
288
+ // Test set and get secret
289
+ manager.set_secret("test.key", "test_value").await.unwrap();
290
+ let value = manager.get_secret("test.key").await.unwrap();
291
+ assert_eq!(value, Some("test_value".to_string()));
292
+
293
+ // Test delete
294
+ manager.delete_secret("test.key").await.unwrap();
295
+ let value = manager.get_secret("test.key").await.unwrap();
296
+ assert_eq!(value, None);
297
+ }
298
+ }
@@ -0,0 +1,149 @@
1
+ //! Secrets Management - Environment variable based storage for secrets
2
+ //!
3
+ //! Replaces OS keychain with .env file support as requested by user.
4
+ //! Secrets are read from:
5
+ //! 1. Environment variables directly
6
+ //! 2. .env file loaded at startup
7
+ //!
8
+ //! Setting secrets via API is no longer supported (must be set in .env or environment).
9
+
10
+ use anyhow::Result;
11
+ use dotenv::dotenv;
12
+ use std::collections::HashMap;
13
+ use std::sync::{Arc, Mutex};
14
+ use tracing::{debug, warn};
15
+
16
+ /// Convert a key identifier to an environment variable name
17
+ ///
18
+ /// Converts keys like `providers.azure.apiKey` to `ENACT_PROVIDERS_AZURE_APIKEY`
19
+ fn key_to_env_var_name(key: &str) -> String {
20
+ let env_name = key.to_uppercase().replace('.', "_");
21
+ format!("ENACT_{}", env_name)
22
+ }
23
+
24
+ /// Get legacy environment variable names for a given config key
25
+ fn get_legacy_env_vars(key: &str) -> Option<Vec<&'static str>> {
26
+ match key {
27
+ "providers.azure.apiKey" => Some(vec!["AZURE_OPENAI_API_KEY", "AZURE_API_KEY"]),
28
+ "providers.azure.endpoint" => {
29
+ Some(vec!["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_API_ENDPOINT"])
30
+ }
31
+ "providers.azure.deploymentName" => Some(vec![
32
+ "AZURE_OPENAI_DEPLOYMENT",
33
+ "AZURE_OPENAI_DEPLOYMENT_NAME",
34
+ ]),
35
+ "providers.azure.apiVersion" => Some(vec!["AZURE_OPENAI_API_VERSION"]),
36
+ "providers.anthropic.apiKey" => Some(vec!["ANTHROPIC_API_KEY"]),
37
+ "providers.openai.apiKey" => Some(vec!["OPENAI_API_KEY"]),
38
+ "providers.google.apiKey" => Some(vec!["GOOGLE_AI_API_KEY", "GOOGLE_API_KEY"]),
39
+ "providers.ollama.baseUrl" => Some(vec!["OLLAMA_BASE_URL"]),
40
+ "storage.eventStore.dsn" => Some(vec!["DATABASE_URL"]),
41
+ "storage.cache.url" => Some(vec!["REDIS_URL"]),
42
+ _ => None,
43
+ }
44
+ }
45
+
46
+ /// Secret manager for retrieving secrets from environment
47
+ #[derive(Clone)]
48
+ pub struct SecretManager {
49
+ mock_store: Option<Arc<Mutex<HashMap<String, String>>>>,
50
+ }
51
+
52
+ impl SecretManager {
53
+ /// Create a new secret manager
54
+ pub fn new() -> Self {
55
+ // Load .env file if present
56
+ if let Err(e) = dotenv() {
57
+ debug!("No .env file found or error loading it: {}", e);
58
+ } else {
59
+ debug!("Loaded .env file");
60
+ }
61
+
62
+ Self { mock_store: None }
63
+ }
64
+
65
+ /// Create a new secret manager with in-memory mock storage for testing
66
+ pub fn new_mock() -> Self {
67
+ Self {
68
+ mock_store: Some(Arc::new(Mutex::new(HashMap::new()))),
69
+ }
70
+ }
71
+
72
+ /// Set a secret (only supported in mock mode)
73
+ ///
74
+ /// In production, secrets must be set via environment variables.
75
+ pub fn set(&self, key: &str, value: &str) -> Result<()> {
76
+ if let Some(ref store) = self.mock_store {
77
+ let mut map = store.lock().unwrap();
78
+ map.insert(key.to_string(), value.to_string());
79
+ debug!("Stored secret in mock store: {}", key);
80
+ return Ok(());
81
+ }
82
+
83
+ // We cannot write to environment variables persistently from here.
84
+ // Warn the user or return error?
85
+ // Returning error is safer to indicate this operation is not supported.
86
+ warn!("Setting secrets programmatically is not supported with .env auth. Please update your .env file or environment manually.");
87
+ Err(anyhow::anyhow!(
88
+ "Setting secrets is not supported in .env mode"
89
+ ))
90
+ }
91
+
92
+ /// Retrieve a secret
93
+ ///
94
+ /// Checks mock store first (if enabled), then environment variables.
95
+ pub fn get(&self, key: &str) -> Result<Option<String>> {
96
+ // Check mock store first if enabled
97
+ if let Some(ref store) = self.mock_store {
98
+ let map = store.lock().unwrap();
99
+ let value = map.get(key).cloned();
100
+ return Ok(value);
101
+ }
102
+
103
+ // Check for environment variable (standard enactment format)
104
+ let env_var_name = key_to_env_var_name(key);
105
+ if let Ok(value) = std::env::var(&env_var_name) {
106
+ debug!(
107
+ "Retrieved secret from environment variable {}: {}",
108
+ env_var_name, key
109
+ );
110
+ return Ok(Some(value));
111
+ }
112
+
113
+ // Check for legacy/alternative environment variables
114
+ // This allows standard .env files to work without renaming keys
115
+ if let Some(legacy_vars) = get_legacy_env_vars(key) {
116
+ for legacy_var in legacy_vars {
117
+ if let Ok(value) = std::env::var(legacy_var) {
118
+ debug!(
119
+ "Retrieved secret from legacy environment variable {}: {}",
120
+ legacy_var, key
121
+ );
122
+ return Ok(Some(value));
123
+ }
124
+ }
125
+ }
126
+
127
+ // Not found
128
+ Ok(None)
129
+ }
130
+
131
+ /// Delete a secret (only supported in mock mode)
132
+ pub fn delete(&self, key: &str) -> Result<()> {
133
+ if let Some(ref store) = self.mock_store {
134
+ let mut map = store.lock().unwrap();
135
+ map.remove(key);
136
+ return Ok(());
137
+ }
138
+
139
+ Err(anyhow::anyhow!(
140
+ "Deleting secrets is not supported in .env mode"
141
+ ))
142
+ }
143
+ }
144
+
145
+ impl Default for SecretManager {
146
+ fn default() -> Self {
147
+ Self::new()
148
+ }
149
+ }
@@ -0,0 +1,260 @@
1
+ //! Cloud Sync - Automatic configuration synchronization
2
+ //!
3
+ //! Syncs configuration between local and cloud for authenticated users.
4
+ //! Respects air-gapped mode (no sync when runtime.mode === "airgapped").
5
+
6
+ use anyhow::{Context, Result};
7
+ use serde::{Deserialize, Serialize};
8
+ use tracing::{debug, warn};
9
+
10
+ use crate::config::{Config, RuntimeMode};
11
+
12
+ /// Sync status
13
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
14
+ pub enum SyncStatus {
15
+ /// Sync is enabled and active
16
+ Enabled,
17
+ /// Sync is disabled (air-gapped mode or not authenticated)
18
+ Disabled,
19
+ /// Sync is in progress
20
+ Syncing,
21
+ /// Sync failed
22
+ Failed,
23
+ }
24
+
25
+ /// Cloud sync manager
26
+ pub struct SyncManager {
27
+ api_url: Option<String>,
28
+ tenant_id: Option<String>,
29
+ auto_sync: bool,
30
+ runtime_mode: RuntimeMode,
31
+ }
32
+
33
+ #[derive(Debug, Serialize, Deserialize)]
34
+ struct SyncRequest {
35
+ tenant_id: String,
36
+ config: Config,
37
+ timestamp: i64,
38
+ }
39
+
40
+ #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41
+ pub struct SyncResponse {
42
+ pub config: Config,
43
+ pub timestamp: i64,
44
+ pub conflicts: Vec<String>,
45
+ }
46
+
47
+ impl SyncManager {
48
+ /// Create a new sync manager
49
+ pub fn new(
50
+ api_url: Option<String>,
51
+ tenant_id: Option<String>,
52
+ auto_sync: bool,
53
+ runtime_mode: RuntimeMode,
54
+ ) -> Self {
55
+ Self {
56
+ api_url,
57
+ tenant_id,
58
+ auto_sync,
59
+ runtime_mode,
60
+ }
61
+ }
62
+
63
+ /// Check if sync is enabled
64
+ ///
65
+ /// Sync is enabled only if:
66
+ /// - auto_sync is true
67
+ /// - runtime_mode is not "airgapped"
68
+ /// - api_url and tenant_id are set
69
+ pub fn is_enabled(&self) -> bool {
70
+ if !self.auto_sync {
71
+ return false;
72
+ }
73
+
74
+ if matches!(self.runtime_mode, RuntimeMode::AirGapped) {
75
+ debug!("Sync disabled: air-gapped mode");
76
+ return false;
77
+ }
78
+
79
+ if self.api_url.is_none() || self.tenant_id.is_none() {
80
+ debug!("Sync disabled: missing API URL or tenant ID");
81
+ return false;
82
+ }
83
+
84
+ true
85
+ }
86
+
87
+ /// Get sync status
88
+ pub fn status(&self) -> SyncStatus {
89
+ if self.is_enabled() {
90
+ SyncStatus::Enabled
91
+ } else {
92
+ SyncStatus::Disabled
93
+ }
94
+ }
95
+
96
+ /// Sync configuration to cloud
97
+ ///
98
+ /// # Arguments
99
+ /// * `config` - The configuration to sync
100
+ ///
101
+ /// # Returns
102
+ /// * `Ok(Some(response))` if sync was successful
103
+ /// * `Ok(None)` if sync is disabled
104
+ /// * `Err` if there was an error syncing
105
+ pub async fn sync_to_cloud(&self, config: &Config) -> Result<Option<SyncResponse>> {
106
+ if !self.is_enabled() {
107
+ return Ok(None);
108
+ }
109
+
110
+ let api_url = self.api_url.as_ref().unwrap();
111
+ let tenant_id = self.tenant_id.as_ref().unwrap();
112
+
113
+ let sync_request = SyncRequest {
114
+ tenant_id: tenant_id.clone(),
115
+ config: config.clone(),
116
+ timestamp: chrono::Utc::now().timestamp(),
117
+ };
118
+
119
+ let client = reqwest::Client::new();
120
+ let url = format!("{}/api/v1/config/sync", api_url);
121
+
122
+ debug!("Syncing configuration to cloud: {}", url);
123
+
124
+ let response = client
125
+ .post(&url)
126
+ .json(&sync_request)
127
+ .send()
128
+ .await
129
+ .context("Failed to send sync request")?;
130
+
131
+ if !response.status().is_success() {
132
+ let status = response.status();
133
+ let error_text = response.text().await.unwrap_or_default();
134
+ return Err(anyhow::anyhow!(
135
+ "Sync failed with status {}: {}",
136
+ status,
137
+ error_text
138
+ ));
139
+ }
140
+
141
+ let sync_response: SyncResponse = response
142
+ .json()
143
+ .await
144
+ .context("Failed to parse sync response")?;
145
+
146
+ if !sync_response.conflicts.is_empty() {
147
+ warn!(
148
+ "Configuration conflicts detected: {:?}",
149
+ sync_response.conflicts
150
+ );
151
+ }
152
+
153
+ debug!("Configuration synced successfully");
154
+ Ok(Some(sync_response))
155
+ }
156
+
157
+ /// Sync configuration from cloud
158
+ ///
159
+ /// # Returns
160
+ /// * `Ok(Some(config))` if sync was successful
161
+ /// * `Ok(None)` if sync is disabled
162
+ /// * `Err` if there was an error syncing
163
+ pub async fn sync_from_cloud(&self) -> Result<Option<Config>> {
164
+ if !self.is_enabled() {
165
+ return Ok(None);
166
+ }
167
+
168
+ let api_url = self.api_url.as_ref().unwrap();
169
+ let tenant_id = self.tenant_id.as_ref().unwrap();
170
+
171
+ let client = reqwest::Client::new();
172
+ let url = format!("{}/api/v1/config/sync?tenant_id={}", api_url, tenant_id);
173
+
174
+ debug!("Syncing configuration from cloud: {}", url);
175
+
176
+ let response = client
177
+ .get(&url)
178
+ .send()
179
+ .await
180
+ .context("Failed to send sync request")?;
181
+
182
+ if !response.status().is_success() {
183
+ let status = response.status();
184
+ let error_text = response.text().await.unwrap_or_default();
185
+ return Err(anyhow::anyhow!(
186
+ "Sync failed with status {}: {}",
187
+ status,
188
+ error_text
189
+ ));
190
+ }
191
+
192
+ let sync_response: SyncResponse = response
193
+ .json()
194
+ .await
195
+ .context("Failed to parse sync response")?;
196
+
197
+ debug!("Configuration synced from cloud successfully");
198
+ Ok(Some(sync_response.config))
199
+ }
200
+
201
+ /// Resolve conflicts using last-write-wins strategy
202
+ ///
203
+ /// # Arguments
204
+ /// * `local_config` - Local configuration
205
+ /// * `cloud_config` - Cloud configuration
206
+ /// * `conflicts` - List of conflicting keys
207
+ ///
208
+ /// # Returns
209
+ /// * Merged configuration with conflicts resolved
210
+ pub fn resolve_conflicts(
211
+ &self,
212
+ _local_config: &Config,
213
+ cloud_config: &Config,
214
+ conflicts: &[String],
215
+ ) -> Config {
216
+ // For now, use last-write-wins (cloud wins)
217
+ // In the future, this could be more sophisticated
218
+ warn!(
219
+ "Resolving {} conflicts using last-write-wins (cloud wins)",
220
+ conflicts.len()
221
+ );
222
+ cloud_config.clone()
223
+ }
224
+ }
225
+
226
+ #[cfg(test)]
227
+ mod tests {
228
+ use super::*;
229
+
230
+ #[test]
231
+ fn test_sync_enabled() {
232
+ let manager = SyncManager::new(
233
+ Some("https://api.example.com".to_string()),
234
+ Some("tenant-123".to_string()),
235
+ true,
236
+ RuntimeMode::Local,
237
+ );
238
+ assert!(manager.is_enabled());
239
+ assert_eq!(manager.status(), SyncStatus::Enabled);
240
+ }
241
+
242
+ #[test]
243
+ fn test_sync_disabled_airgapped() {
244
+ let manager = SyncManager::new(
245
+ Some("https://api.example.com".to_string()),
246
+ Some("tenant-123".to_string()),
247
+ true,
248
+ RuntimeMode::AirGapped,
249
+ );
250
+ assert!(!manager.is_enabled());
251
+ assert_eq!(manager.status(), SyncStatus::Disabled);
252
+ }
253
+
254
+ #[test]
255
+ fn test_sync_disabled_no_auth() {
256
+ let manager = SyncManager::new(None, None, true, RuntimeMode::Local);
257
+ assert!(!manager.is_enabled());
258
+ assert_eq!(manager.status(), SyncStatus::Disabled);
259
+ }
260
+ }