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,1315 @@
1
+ //! Enforcement - Kernel-owned limits, quotas, and rate limiting
2
+ //!
3
+ //! This module provides the enforcement layer that ensures executions
4
+ //! respect their resource boundaries. All limit enforcement happens
5
+ //! in the kernel, not in providers.
6
+ //!
7
+ //! ## Design Principles
8
+ //!
9
+ //! 1. **Kernel Owns Enforcement**: Providers are dumb adapters
10
+ //! 2. **Hard Limits**: Quota exceeded = execution halts immediately
11
+ //! 3. **Deterministic**: Same limits → same enforcement behavior
12
+ //! 4. **Observable**: All enforcement decisions are logged/events
13
+ //!
14
+ //! ## Key Components
15
+ //!
16
+ //! - `UsageTracker`: Tracks resource consumption per execution
17
+ //! - `EnforcementPolicy`: Defines limits and enforcement rules
18
+ //! - `EnforcementResult`: Outcome of limit checks
19
+ //!
20
+ //! @see docs/feat-03-limits-quotas.md
21
+
22
+ use super::error::{ExecutionError, ExecutionErrorCategory};
23
+ use super::ids::{ExecutionId, StepId, TenantId};
24
+ use crate::context::ResourceLimits;
25
+ use serde::{Deserialize, Serialize};
26
+ use std::collections::HashMap;
27
+ use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
28
+ use std::sync::Arc;
29
+ use std::time::{Duration, Instant};
30
+ use tokio::sync::RwLock;
31
+
32
+ // =============================================================================
33
+ // Usage Tracking
34
+ // =============================================================================
35
+
36
+ /// Tracks resource usage for a single execution
37
+ #[derive(Debug)]
38
+ pub struct ExecutionUsage {
39
+ /// Execution ID
40
+ pub execution_id: ExecutionId,
41
+ /// Tenant ID
42
+ pub tenant_id: TenantId,
43
+ /// Number of steps executed
44
+ pub steps: AtomicU32,
45
+ /// Total input tokens consumed
46
+ pub input_tokens: AtomicU32,
47
+ /// Total output tokens consumed
48
+ pub output_tokens: AtomicU32,
49
+ /// Wall clock start time
50
+ pub started_at: Instant,
51
+ /// Last activity timestamp
52
+ pub last_activity: RwLock<Instant>,
53
+ // === Long-running execution tracking ===
54
+ /// Number of dynamically discovered steps (StepSource::Discovered)
55
+ pub discovered_steps: AtomicU32,
56
+ /// Current discovery chain depth (how deep in the discovery tree)
57
+ pub discovery_depth: AtomicU32,
58
+ /// Maximum discovery depth reached during execution
59
+ pub max_discovery_depth_reached: AtomicU32,
60
+ /// Cumulative cost in cents (USD * 100 for integer precision)
61
+ pub cost_cents: AtomicU64,
62
+ }
63
+
64
+ impl ExecutionUsage {
65
+ /// Create a new usage tracker for an execution
66
+ pub fn new(execution_id: ExecutionId, tenant_id: TenantId) -> Self {
67
+ let now = Instant::now();
68
+ Self {
69
+ execution_id,
70
+ tenant_id,
71
+ steps: AtomicU32::new(0),
72
+ input_tokens: AtomicU32::new(0),
73
+ output_tokens: AtomicU32::new(0),
74
+ started_at: now,
75
+ last_activity: RwLock::new(now),
76
+ discovered_steps: AtomicU32::new(0),
77
+ discovery_depth: AtomicU32::new(0),
78
+ max_discovery_depth_reached: AtomicU32::new(0),
79
+ cost_cents: AtomicU64::new(0),
80
+ }
81
+ }
82
+
83
+ /// Record step execution
84
+ pub fn record_step(&self) {
85
+ self.steps.fetch_add(1, Ordering::SeqCst);
86
+ }
87
+
88
+ /// Record a discovered step (dynamically added to DAG)
89
+ pub fn record_discovered_step(&self) {
90
+ self.discovered_steps.fetch_add(1, Ordering::SeqCst);
91
+ }
92
+
93
+ /// Record token usage
94
+ pub fn record_tokens(&self, input: u32, output: u32) {
95
+ self.input_tokens.fetch_add(input, Ordering::SeqCst);
96
+ self.output_tokens.fetch_add(output, Ordering::SeqCst);
97
+ }
98
+
99
+ /// Record cost in USD (converted to cents for storage)
100
+ pub fn record_cost_usd(&self, cost_usd: f64) {
101
+ let cents = (cost_usd * 100.0) as u64;
102
+ self.cost_cents.fetch_add(cents, Ordering::SeqCst);
103
+ }
104
+
105
+ /// Push discovery depth (entering a discovered step)
106
+ pub fn push_discovery_depth(&self) {
107
+ let new_depth = self.discovery_depth.fetch_add(1, Ordering::SeqCst) + 1;
108
+ // Update max if this is deeper than before
109
+ let current_max = self.max_discovery_depth_reached.load(Ordering::SeqCst);
110
+ if new_depth > current_max {
111
+ self.max_discovery_depth_reached.store(new_depth, Ordering::SeqCst);
112
+ }
113
+ }
114
+
115
+ /// Pop discovery depth (exiting a discovered step)
116
+ pub fn pop_discovery_depth(&self) {
117
+ self.discovery_depth.fetch_sub(1, Ordering::SeqCst);
118
+ }
119
+
120
+ /// Update last activity timestamp
121
+ pub async fn touch(&self) {
122
+ let mut last = self.last_activity.write().await;
123
+ *last = Instant::now();
124
+ }
125
+
126
+ /// Get current step count
127
+ pub fn step_count(&self) -> u32 {
128
+ self.steps.load(Ordering::SeqCst)
129
+ }
130
+
131
+ /// Get discovered step count
132
+ pub fn discovered_step_count(&self) -> u32 {
133
+ self.discovered_steps.load(Ordering::SeqCst)
134
+ }
135
+
136
+ /// Get current discovery depth
137
+ pub fn current_discovery_depth(&self) -> u32 {
138
+ self.discovery_depth.load(Ordering::SeqCst)
139
+ }
140
+
141
+ /// Get total token count
142
+ pub fn total_tokens(&self) -> u32 {
143
+ self.input_tokens.load(Ordering::SeqCst) + self.output_tokens.load(Ordering::SeqCst)
144
+ }
145
+
146
+ /// Get cumulative cost in USD
147
+ pub fn cost_usd(&self) -> f64 {
148
+ self.cost_cents.load(Ordering::SeqCst) as f64 / 100.0
149
+ }
150
+
151
+ /// Get wall clock duration
152
+ pub fn wall_time(&self) -> Duration {
153
+ self.started_at.elapsed()
154
+ }
155
+
156
+ /// Get wall time in milliseconds
157
+ pub fn wall_time_ms(&self) -> u64 {
158
+ self.wall_time().as_millis() as u64
159
+ }
160
+
161
+ /// Get idle duration (time since last activity)
162
+ pub async fn idle_duration(&self) -> Duration {
163
+ let last = self.last_activity.read().await;
164
+ last.elapsed()
165
+ }
166
+
167
+ /// Get idle duration in seconds
168
+ pub async fn idle_seconds(&self) -> u64 {
169
+ self.idle_duration().await.as_secs()
170
+ }
171
+ }
172
+
173
+ /// Serializable snapshot of execution usage
174
+ #[derive(Debug, Clone, Serialize, Deserialize)]
175
+ pub struct UsageSnapshot {
176
+ pub execution_id: String,
177
+ pub tenant_id: String,
178
+ pub steps: u32,
179
+ pub input_tokens: u32,
180
+ pub output_tokens: u32,
181
+ pub total_tokens: u32,
182
+ pub wall_time_ms: u64,
183
+ // Long-running execution metrics
184
+ pub discovered_steps: u32,
185
+ pub discovery_depth: u32,
186
+ pub max_discovery_depth: u32,
187
+ pub cost_usd: f64,
188
+ }
189
+
190
+ impl From<&ExecutionUsage> for UsageSnapshot {
191
+ fn from(usage: &ExecutionUsage) -> Self {
192
+ let input = usage.input_tokens.load(Ordering::SeqCst);
193
+ let output = usage.output_tokens.load(Ordering::SeqCst);
194
+ Self {
195
+ execution_id: usage.execution_id.as_str().to_string(),
196
+ tenant_id: usage.tenant_id.as_str().to_string(),
197
+ steps: usage.steps.load(Ordering::SeqCst),
198
+ input_tokens: input,
199
+ output_tokens: output,
200
+ total_tokens: input + output,
201
+ wall_time_ms: usage.wall_time_ms(),
202
+ discovered_steps: usage.discovered_steps.load(Ordering::SeqCst),
203
+ discovery_depth: usage.discovery_depth.load(Ordering::SeqCst),
204
+ max_discovery_depth: usage.max_discovery_depth_reached.load(Ordering::SeqCst),
205
+ cost_usd: usage.cost_usd(),
206
+ }
207
+ }
208
+ }
209
+
210
+ // =============================================================================
211
+ // Enforcement Results
212
+ // =============================================================================
213
+
214
+ /// Result of an enforcement check
215
+ #[derive(Debug, Clone, PartialEq, Eq)]
216
+ pub enum EnforcementResult {
217
+ /// Operation is allowed to proceed
218
+ Allowed,
219
+ /// Operation is blocked due to limit exceeded
220
+ Blocked(EnforcementViolation),
221
+ /// Operation is allowed but near limit (warning)
222
+ Warning(EnforcementWarning),
223
+ }
224
+
225
+ impl EnforcementResult {
226
+ /// Check if the result allows the operation
227
+ pub fn is_allowed(&self) -> bool {
228
+ matches!(self, Self::Allowed | Self::Warning(_))
229
+ }
230
+
231
+ /// Check if the result blocks the operation
232
+ pub fn is_blocked(&self) -> bool {
233
+ matches!(self, Self::Blocked(_))
234
+ }
235
+
236
+ /// Convert to an ExecutionError if blocked
237
+ pub fn to_error(&self) -> Option<ExecutionError> {
238
+ match self {
239
+ Self::Blocked(violation) => Some(violation.to_error()),
240
+ _ => None,
241
+ }
242
+ }
243
+ }
244
+
245
+ /// Type of enforcement violation
246
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247
+ pub enum ViolationType {
248
+ /// Maximum steps exceeded
249
+ StepLimit,
250
+ /// Maximum tokens exceeded
251
+ TokenLimit,
252
+ /// Wall clock timeout exceeded
253
+ WallTimeLimit,
254
+ /// Memory limit exceeded
255
+ MemoryLimit,
256
+ /// Concurrent execution limit exceeded
257
+ ConcurrencyLimit,
258
+ /// Rate limit exceeded
259
+ RateLimit,
260
+ /// Network access denied in air-gapped mode
261
+ NetworkViolation,
262
+ // === Long-running execution controls ===
263
+ /// Maximum discovered steps exceeded (agentic DAG)
264
+ DiscoveredStepLimit,
265
+ /// Discovery chain depth exceeded (prevents infinite discovery)
266
+ DiscoveryDepthLimit,
267
+ /// Cost threshold exceeded (USD-based alerting)
268
+ CostThreshold,
269
+ /// No activity for too long (idle timeout)
270
+ IdleTimeout,
271
+ /// Agent repeating same methodology (semantic loop)
272
+ SameStepLoop,
273
+ }
274
+
275
+ impl std::fmt::Display for ViolationType {
276
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277
+ match self {
278
+ Self::StepLimit => write!(f, "step_limit"),
279
+ Self::TokenLimit => write!(f, "token_limit"),
280
+ Self::WallTimeLimit => write!(f, "wall_time_limit"),
281
+ Self::MemoryLimit => write!(f, "memory_limit"),
282
+ Self::ConcurrencyLimit => write!(f, "concurrency_limit"),
283
+ Self::RateLimit => write!(f, "rate_limit"),
284
+ Self::NetworkViolation => write!(f, "network_violation"),
285
+ Self::DiscoveredStepLimit => write!(f, "discovered_step_limit"),
286
+ Self::DiscoveryDepthLimit => write!(f, "discovery_depth_limit"),
287
+ Self::CostThreshold => write!(f, "cost_threshold"),
288
+ Self::IdleTimeout => write!(f, "idle_timeout"),
289
+ Self::SameStepLoop => write!(f, "same_step_loop"),
290
+ }
291
+ }
292
+ }
293
+
294
+ /// Details of an enforcement violation
295
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296
+ pub struct EnforcementViolation {
297
+ /// Type of violation
298
+ pub violation_type: ViolationType,
299
+ /// Current value
300
+ pub current: u64,
301
+ /// Limit value
302
+ pub limit: u64,
303
+ /// Human-readable message
304
+ pub message: String,
305
+ }
306
+
307
+ impl EnforcementViolation {
308
+ /// Create a new violation
309
+ pub fn new(violation_type: ViolationType, current: u64, limit: u64) -> Self {
310
+ let message = format!(
311
+ "{} exceeded: {} / {} ({}%)",
312
+ violation_type,
313
+ current,
314
+ limit,
315
+ (current as f64 / limit as f64 * 100.0) as u32
316
+ );
317
+ Self {
318
+ violation_type,
319
+ current,
320
+ limit,
321
+ message,
322
+ }
323
+ }
324
+
325
+ /// Convert to an ExecutionError
326
+ pub fn to_error(&self) -> ExecutionError {
327
+ let category = match self.violation_type {
328
+ ViolationType::WallTimeLimit => ExecutionErrorCategory::Timeout,
329
+ ViolationType::RateLimit => ExecutionErrorCategory::LlmError, // Rate limits are retryable
330
+ ViolationType::NetworkViolation => ExecutionErrorCategory::PolicyViolation, // Non-retryable policy
331
+ _ => ExecutionErrorCategory::QuotaExceeded,
332
+ };
333
+
334
+ ExecutionError::new(category, self.message.clone())
335
+ .with_code(self.violation_type.to_string())
336
+ .with_details(serde_json::json!({
337
+ "current": self.current,
338
+ "limit": self.limit,
339
+ "violation_type": self.violation_type.to_string(),
340
+ }))
341
+ }
342
+ }
343
+
344
+ /// Warning about approaching limits
345
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346
+ pub struct EnforcementWarning {
347
+ /// Type of limit being approached
348
+ pub warning_type: ViolationType,
349
+ /// Current usage percentage (0-100)
350
+ pub usage_percent: u32,
351
+ /// Human-readable message
352
+ pub message: String,
353
+ }
354
+
355
+ impl EnforcementWarning {
356
+ /// Create a new warning
357
+ pub fn new(warning_type: ViolationType, current: u64, limit: u64) -> Self {
358
+ let percent = (current as f64 / limit as f64 * 100.0) as u32;
359
+ let message = format!("{} at {}%: {} / {}", warning_type, percent, current, limit);
360
+ Self {
361
+ warning_type,
362
+ usage_percent: percent,
363
+ message,
364
+ }
365
+ }
366
+ }
367
+
368
+ // =============================================================================
369
+ // Enforcement Policy
370
+ // =============================================================================
371
+
372
+ /// Configuration for enforcement behavior
373
+ #[derive(Debug, Clone, Serialize, Deserialize)]
374
+ pub struct EnforcementPolicy {
375
+ /// Warning threshold (percentage of limit, 0-100)
376
+ pub warning_threshold: u32,
377
+ /// Whether to emit events on warnings
378
+ pub emit_warning_events: bool,
379
+ /// Whether to emit events on blocks
380
+ pub emit_block_events: bool,
381
+ /// Grace period for timeouts (milliseconds)
382
+ pub timeout_grace_ms: u64,
383
+ }
384
+
385
+ impl Default for EnforcementPolicy {
386
+ fn default() -> Self {
387
+ Self {
388
+ warning_threshold: 80, // Warn at 80% usage
389
+ emit_warning_events: true,
390
+ emit_block_events: true,
391
+ timeout_grace_ms: 1000, // 1 second grace period
392
+ }
393
+ }
394
+ }
395
+
396
+ // =============================================================================
397
+ // Long-Running Execution Policy
398
+ // =============================================================================
399
+
400
+ /// Policy configuration for long-running agentic executions
401
+ ///
402
+ /// These controls prevent runaway costs, infinite discovery loops, and idle
403
+ /// executions from consuming resources in the Agentic DAG model.
404
+ #[derive(Debug, Clone, Serialize, Deserialize)]
405
+ pub struct LongRunningExecutionPolicy {
406
+ /// Maximum number of dynamically discovered steps before intervention
407
+ /// (Steps with StepSource::Discovered)
408
+ pub max_discovered_steps: Option<u32>,
409
+ /// Maximum depth of discovery chains (prevents infinite discovery)
410
+ /// e.g., agent discovers step A, which discovers step B, which discovers C...
411
+ pub max_discovery_depth: Option<u32>,
412
+ /// Alert threshold for cumulative cost in USD
413
+ /// When exceeded, execution pauses for approval
414
+ pub cost_alert_threshold_usd: Option<f64>,
415
+ /// Maximum time without activity before idle timeout (seconds)
416
+ pub idle_timeout_seconds: Option<u64>,
417
+ /// Maximum repetitions of same methodology before loop detection (default: 3)
418
+ pub max_same_step_repetitions: Option<u32>,
419
+ }
420
+
421
+ impl Default for LongRunningExecutionPolicy {
422
+ fn default() -> Self {
423
+ Self::standard()
424
+ }
425
+ }
426
+
427
+ impl LongRunningExecutionPolicy {
428
+ /// Standard preset - balanced limits for typical long-running executions
429
+ /// - Max duration: ~30 minutes (via idle timeout)
430
+ /// - Discovered steps: 50
431
+ /// - Discovery depth: 5
432
+ /// - Cost alert: $5.00 USD
433
+ pub fn standard() -> Self {
434
+ Self {
435
+ max_discovered_steps: Some(50),
436
+ max_discovery_depth: Some(5),
437
+ cost_alert_threshold_usd: Some(5.0),
438
+ idle_timeout_seconds: Some(1800), // 30 minutes
439
+ max_same_step_repetitions: Some(3),
440
+ }
441
+ }
442
+
443
+ /// Extended preset - higher limits for complex, supervised workflows
444
+ /// - Max duration: ~4 hours
445
+ /// - Discovered steps: 300
446
+ /// - Discovery depth: 10
447
+ /// - Cost alert: $50.00 USD
448
+ pub fn extended() -> Self {
449
+ Self {
450
+ max_discovered_steps: Some(300),
451
+ max_discovery_depth: Some(10),
452
+ cost_alert_threshold_usd: Some(50.0),
453
+ idle_timeout_seconds: Some(14400), // 4 hours
454
+ max_same_step_repetitions: Some(5),
455
+ }
456
+ }
457
+
458
+ /// Unlimited preset - no discovery limits, but requires cost monitoring
459
+ /// - No step/depth limits
460
+ /// - Cost alert: $100.00 USD (mandatory safety net)
461
+ /// - Idle timeout: 24 hours
462
+ pub fn unlimited() -> Self {
463
+ Self {
464
+ max_discovered_steps: None,
465
+ max_discovery_depth: None,
466
+ cost_alert_threshold_usd: Some(100.0), // Required safety net
467
+ idle_timeout_seconds: Some(86400), // 24 hours
468
+ max_same_step_repetitions: None,
469
+ }
470
+ }
471
+
472
+ /// Disabled - no long-running controls (use with caution)
473
+ pub fn disabled() -> Self {
474
+ Self {
475
+ max_discovered_steps: None,
476
+ max_discovery_depth: None,
477
+ cost_alert_threshold_usd: None,
478
+ idle_timeout_seconds: None,
479
+ max_same_step_repetitions: None,
480
+ }
481
+ }
482
+ }
483
+
484
+ // =============================================================================
485
+ // Enforcement Middleware
486
+ // =============================================================================
487
+
488
+ /// Enforcement middleware for checking limits before operations
489
+ #[derive(Debug)]
490
+ pub struct EnforcementMiddleware {
491
+ /// Active executions and their usage
492
+ executions: RwLock<HashMap<ExecutionId, Arc<ExecutionUsage>>>,
493
+ /// Active execution count per tenant
494
+ tenant_executions: RwLock<HashMap<TenantId, AtomicU32>>,
495
+ /// Global rate limiter state
496
+ #[allow(dead_code)]
497
+ rate_limiter: RwLock<RateLimiterState>,
498
+ /// Enforcement policy
499
+ policy: EnforcementPolicy,
500
+ }
501
+
502
+ impl EnforcementMiddleware {
503
+ /// Create a new enforcement middleware
504
+ pub fn new() -> Self {
505
+ Self::with_policy(EnforcementPolicy::default())
506
+ }
507
+
508
+ /// Create with custom policy
509
+ pub fn with_policy(policy: EnforcementPolicy) -> Self {
510
+ Self {
511
+ executions: RwLock::new(HashMap::new()),
512
+ tenant_executions: RwLock::new(HashMap::new()),
513
+ rate_limiter: RwLock::new(RateLimiterState::new()),
514
+ policy,
515
+ }
516
+ }
517
+
518
+ /// Whether warning events should be emitted when limits are near
519
+ pub fn emit_warning_events_enabled(&self) -> bool {
520
+ self.policy.emit_warning_events
521
+ }
522
+
523
+ /// Register a new execution
524
+ pub async fn register_execution(
525
+ &self,
526
+ execution_id: ExecutionId,
527
+ tenant_id: TenantId,
528
+ ) -> Arc<ExecutionUsage> {
529
+ let usage = Arc::new(ExecutionUsage::new(execution_id.clone(), tenant_id.clone()));
530
+
531
+ // Register in executions map
532
+ {
533
+ let mut executions = self.executions.write().await;
534
+ executions.insert(execution_id, Arc::clone(&usage));
535
+ }
536
+
537
+ // Increment tenant execution count
538
+ {
539
+ let mut tenant_execs = self.tenant_executions.write().await;
540
+ tenant_execs
541
+ .entry(tenant_id)
542
+ .or_insert_with(|| AtomicU32::new(0))
543
+ .fetch_add(1, Ordering::SeqCst);
544
+ }
545
+
546
+ usage
547
+ }
548
+
549
+ /// Unregister an execution
550
+ pub async fn unregister_execution(&self, execution_id: &ExecutionId) {
551
+ let tenant_id = {
552
+ let mut executions = self.executions.write().await;
553
+ executions.remove(execution_id).map(|u| u.tenant_id.clone())
554
+ };
555
+
556
+ // Decrement tenant execution count
557
+ if let Some(tenant_id) = tenant_id {
558
+ let tenant_execs = self.tenant_executions.read().await;
559
+ if let Some(count) = tenant_execs.get(&tenant_id) {
560
+ count.fetch_sub(1, Ordering::SeqCst);
561
+ }
562
+ }
563
+ }
564
+
565
+ /// Get usage for an execution
566
+ pub async fn get_usage(&self, execution_id: &ExecutionId) -> Option<Arc<ExecutionUsage>> {
567
+ let executions = self.executions.read().await;
568
+ executions.get(execution_id).cloned()
569
+ }
570
+
571
+ /// Get usage snapshot for an execution
572
+ pub async fn get_usage_snapshot(&self, execution_id: &ExecutionId) -> Option<UsageSnapshot> {
573
+ self.get_usage(execution_id)
574
+ .await
575
+ .map(|u| UsageSnapshot::from(u.as_ref()))
576
+ }
577
+
578
+ /// Check if a new step can be started
579
+ pub async fn check_step_allowed(
580
+ &self,
581
+ execution_id: &ExecutionId,
582
+ limits: &ResourceLimits,
583
+ ) -> EnforcementResult {
584
+ let usage = match self.get_usage(execution_id).await {
585
+ Some(u) => u,
586
+ None => return EnforcementResult::Allowed, // No tracking = allowed
587
+ };
588
+
589
+ let current = usage.step_count() as u64 + 1; // +1 for the step we're about to start
590
+ let limit = limits.max_steps as u64;
591
+
592
+ if current > limit {
593
+ return EnforcementResult::Blocked(EnforcementViolation::new(
594
+ ViolationType::StepLimit,
595
+ current,
596
+ limit,
597
+ ));
598
+ }
599
+
600
+ let percent = (current as f64 / limit as f64 * 100.0) as u32;
601
+ if percent >= self.policy.warning_threshold {
602
+ return EnforcementResult::Warning(EnforcementWarning::new(
603
+ ViolationType::StepLimit,
604
+ current,
605
+ limit,
606
+ ));
607
+ }
608
+
609
+ EnforcementResult::Allowed
610
+ }
611
+
612
+ /// Check if token usage is within limits
613
+ pub async fn check_tokens_allowed(
614
+ &self,
615
+ execution_id: &ExecutionId,
616
+ limits: &ResourceLimits,
617
+ additional_tokens: u32,
618
+ ) -> EnforcementResult {
619
+ let usage = match self.get_usage(execution_id).await {
620
+ Some(u) => u,
621
+ None => return EnforcementResult::Allowed,
622
+ };
623
+
624
+ let current = usage.total_tokens() as u64 + additional_tokens as u64;
625
+ let limit = limits.max_tokens as u64;
626
+
627
+ if current > limit {
628
+ return EnforcementResult::Blocked(EnforcementViolation::new(
629
+ ViolationType::TokenLimit,
630
+ current,
631
+ limit,
632
+ ));
633
+ }
634
+
635
+ let percent = (current as f64 / limit as f64 * 100.0) as u32;
636
+ if percent >= self.policy.warning_threshold {
637
+ return EnforcementResult::Warning(EnforcementWarning::new(
638
+ ViolationType::TokenLimit,
639
+ current,
640
+ limit,
641
+ ));
642
+ }
643
+
644
+ EnforcementResult::Allowed
645
+ }
646
+
647
+ /// Check if wall time is within limits
648
+ pub async fn check_wall_time_allowed(
649
+ &self,
650
+ execution_id: &ExecutionId,
651
+ limits: &ResourceLimits,
652
+ ) -> EnforcementResult {
653
+ let usage = match self.get_usage(execution_id).await {
654
+ Some(u) => u,
655
+ None => return EnforcementResult::Allowed,
656
+ };
657
+
658
+ let current = usage.wall_time_ms();
659
+ let limit = limits.max_wall_time_ms;
660
+
661
+ // Add grace period
662
+ let effective_limit = limit + self.policy.timeout_grace_ms;
663
+
664
+ if current > effective_limit {
665
+ return EnforcementResult::Blocked(EnforcementViolation::new(
666
+ ViolationType::WallTimeLimit,
667
+ current,
668
+ limit,
669
+ ));
670
+ }
671
+
672
+ let percent = (current as f64 / limit as f64 * 100.0) as u32;
673
+ if percent >= self.policy.warning_threshold {
674
+ return EnforcementResult::Warning(EnforcementWarning::new(
675
+ ViolationType::WallTimeLimit,
676
+ current,
677
+ limit,
678
+ ));
679
+ }
680
+
681
+ EnforcementResult::Allowed
682
+ }
683
+
684
+ /// Check if concurrent execution limit is respected
685
+ pub async fn check_concurrency_allowed(
686
+ &self,
687
+ tenant_id: &TenantId,
688
+ limits: &ResourceLimits,
689
+ ) -> EnforcementResult {
690
+ let max_concurrent = match limits.max_concurrent_executions {
691
+ Some(max) => max,
692
+ None => return EnforcementResult::Allowed, // No limit set
693
+ };
694
+
695
+ let current = {
696
+ let tenant_execs = self.tenant_executions.read().await;
697
+ tenant_execs
698
+ .get(tenant_id)
699
+ .map(|c| c.load(Ordering::SeqCst))
700
+ .unwrap_or(0) as u64
701
+ };
702
+
703
+ let limit = max_concurrent as u64;
704
+
705
+ if current >= limit {
706
+ return EnforcementResult::Blocked(EnforcementViolation::new(
707
+ ViolationType::ConcurrencyLimit,
708
+ current + 1, // +1 for the execution we're about to start
709
+ limit,
710
+ ));
711
+ }
712
+
713
+ EnforcementResult::Allowed
714
+ }
715
+
716
+ /// Perform all limit checks before starting a step
717
+ pub async fn check_all_limits(
718
+ &self,
719
+ execution_id: &ExecutionId,
720
+ limits: &ResourceLimits,
721
+ ) -> EnforcementResult {
722
+ // Check wall time first (most likely to timeout)
723
+ let wall_check = self.check_wall_time_allowed(execution_id, limits).await;
724
+ if wall_check.is_blocked() {
725
+ return wall_check;
726
+ }
727
+
728
+ // Check step limit
729
+ let step_check = self.check_step_allowed(execution_id, limits).await;
730
+ if step_check.is_blocked() {
731
+ return step_check;
732
+ }
733
+
734
+ // Check token limit
735
+ let token_check = self.check_tokens_allowed(execution_id, limits, 0).await;
736
+ if token_check.is_blocked() {
737
+ return token_check;
738
+ }
739
+
740
+ // Return warnings if any
741
+ if let EnforcementResult::Warning(w) = wall_check {
742
+ return EnforcementResult::Warning(w);
743
+ }
744
+ if let EnforcementResult::Warning(w) = step_check {
745
+ return EnforcementResult::Warning(w);
746
+ }
747
+ if let EnforcementResult::Warning(w) = token_check {
748
+ return EnforcementResult::Warning(w);
749
+ }
750
+
751
+ EnforcementResult::Allowed
752
+ }
753
+
754
+ /// Record step completion and update usage
755
+ pub async fn record_step(&self, execution_id: &ExecutionId) {
756
+ if let Some(usage) = self.get_usage(execution_id).await {
757
+ usage.record_step();
758
+ usage.touch().await;
759
+ }
760
+ }
761
+
762
+ /// Record token usage
763
+ pub async fn record_tokens(&self, execution_id: &ExecutionId, input: u32, output: u32) {
764
+ if let Some(usage) = self.get_usage(execution_id).await {
765
+ usage.record_tokens(input, output);
766
+ usage.touch().await;
767
+ }
768
+ }
769
+
770
+ /// Record a discovered step and update usage
771
+ pub async fn record_discovered_step(&self, execution_id: &ExecutionId) {
772
+ if let Some(usage) = self.get_usage(execution_id).await {
773
+ usage.record_discovered_step();
774
+ usage.touch().await;
775
+ }
776
+ }
777
+
778
+ /// Record cost in USD
779
+ pub async fn record_cost(&self, execution_id: &ExecutionId, cost_usd: f64) {
780
+ if let Some(usage) = self.get_usage(execution_id).await {
781
+ usage.record_cost_usd(cost_usd);
782
+ usage.touch().await;
783
+ }
784
+ }
785
+
786
+ /// Push discovery depth (entering a discovered step's sub-execution)
787
+ pub async fn push_discovery_depth(&self, execution_id: &ExecutionId) {
788
+ if let Some(usage) = self.get_usage(execution_id).await {
789
+ usage.push_discovery_depth();
790
+ }
791
+ }
792
+
793
+ /// Pop discovery depth (exiting a discovered step's sub-execution)
794
+ pub async fn pop_discovery_depth(&self, execution_id: &ExecutionId) {
795
+ if let Some(usage) = self.get_usage(execution_id).await {
796
+ usage.pop_discovery_depth();
797
+ }
798
+ }
799
+
800
+ // =========================================================================
801
+ // Long-Running Execution Checks
802
+ // =========================================================================
803
+
804
+ /// Check if discovered step limit is within bounds
805
+ pub async fn check_discovered_step_limit(
806
+ &self,
807
+ execution_id: &ExecutionId,
808
+ policy: &LongRunningExecutionPolicy,
809
+ ) -> EnforcementResult {
810
+ let max_discovered = match policy.max_discovered_steps {
811
+ Some(max) => max,
812
+ None => return EnforcementResult::Allowed,
813
+ };
814
+
815
+ let usage = match self.get_usage(execution_id).await {
816
+ Some(u) => u,
817
+ None => return EnforcementResult::Allowed,
818
+ };
819
+
820
+ let current = usage.discovered_step_count() as u64 + 1; // +1 for step we're about to discover
821
+ let limit = max_discovered as u64;
822
+
823
+ if current > limit {
824
+ return EnforcementResult::Blocked(EnforcementViolation::new(
825
+ ViolationType::DiscoveredStepLimit,
826
+ current,
827
+ limit,
828
+ ));
829
+ }
830
+
831
+ let percent = (current as f64 / limit as f64 * 100.0) as u32;
832
+ if percent >= self.policy.warning_threshold {
833
+ return EnforcementResult::Warning(EnforcementWarning::new(
834
+ ViolationType::DiscoveredStepLimit,
835
+ current,
836
+ limit,
837
+ ));
838
+ }
839
+
840
+ EnforcementResult::Allowed
841
+ }
842
+
843
+ /// Check if discovery depth is within bounds
844
+ pub async fn check_discovery_depth_limit(
845
+ &self,
846
+ execution_id: &ExecutionId,
847
+ policy: &LongRunningExecutionPolicy,
848
+ ) -> EnforcementResult {
849
+ let max_depth = match policy.max_discovery_depth {
850
+ Some(max) => max,
851
+ None => return EnforcementResult::Allowed,
852
+ };
853
+
854
+ let usage = match self.get_usage(execution_id).await {
855
+ Some(u) => u,
856
+ None => return EnforcementResult::Allowed,
857
+ };
858
+
859
+ let current = usage.current_discovery_depth() as u64 + 1; // +1 for depth we're about to enter
860
+ let limit = max_depth as u64;
861
+
862
+ if current > limit {
863
+ return EnforcementResult::Blocked(EnforcementViolation::new(
864
+ ViolationType::DiscoveryDepthLimit,
865
+ current,
866
+ limit,
867
+ ));
868
+ }
869
+
870
+ // No warning for depth - it's either allowed or not
871
+ EnforcementResult::Allowed
872
+ }
873
+
874
+ /// Check if cost threshold has been exceeded
875
+ pub async fn check_cost_threshold(
876
+ &self,
877
+ execution_id: &ExecutionId,
878
+ policy: &LongRunningExecutionPolicy,
879
+ ) -> EnforcementResult {
880
+ let threshold = match policy.cost_alert_threshold_usd {
881
+ Some(t) => t,
882
+ None => return EnforcementResult::Allowed,
883
+ };
884
+
885
+ let usage = match self.get_usage(execution_id).await {
886
+ Some(u) => u,
887
+ None => return EnforcementResult::Allowed,
888
+ };
889
+
890
+ let current_cents = usage.cost_cents.load(Ordering::SeqCst);
891
+ let current_usd = current_cents as f64 / 100.0;
892
+ let limit_cents = (threshold * 100.0) as u64;
893
+
894
+ if current_usd >= threshold {
895
+ return EnforcementResult::Blocked(EnforcementViolation::new(
896
+ ViolationType::CostThreshold,
897
+ current_cents,
898
+ limit_cents,
899
+ ));
900
+ }
901
+
902
+ let percent = (current_usd / threshold * 100.0) as u32;
903
+ if percent >= self.policy.warning_threshold {
904
+ return EnforcementResult::Warning(EnforcementWarning::new(
905
+ ViolationType::CostThreshold,
906
+ current_cents,
907
+ limit_cents,
908
+ ));
909
+ }
910
+
911
+ EnforcementResult::Allowed
912
+ }
913
+
914
+ /// Check if idle timeout has been exceeded
915
+ pub async fn check_idle_timeout(
916
+ &self,
917
+ execution_id: &ExecutionId,
918
+ policy: &LongRunningExecutionPolicy,
919
+ ) -> EnforcementResult {
920
+ let timeout_secs = match policy.idle_timeout_seconds {
921
+ Some(t) => t,
922
+ None => return EnforcementResult::Allowed,
923
+ };
924
+
925
+ let usage = match self.get_usage(execution_id).await {
926
+ Some(u) => u,
927
+ None => return EnforcementResult::Allowed,
928
+ };
929
+
930
+ let idle_secs = usage.idle_seconds().await;
931
+
932
+ if idle_secs >= timeout_secs {
933
+ return EnforcementResult::Blocked(EnforcementViolation::new(
934
+ ViolationType::IdleTimeout,
935
+ idle_secs,
936
+ timeout_secs,
937
+ ));
938
+ }
939
+
940
+ // Warn at 80% of idle timeout
941
+ let percent = (idle_secs as f64 / timeout_secs as f64 * 100.0) as u32;
942
+ if percent >= self.policy.warning_threshold {
943
+ return EnforcementResult::Warning(EnforcementWarning::new(
944
+ ViolationType::IdleTimeout,
945
+ idle_secs,
946
+ timeout_secs,
947
+ ));
948
+ }
949
+
950
+ EnforcementResult::Allowed
951
+ }
952
+
953
+ /// Perform all long-running execution checks
954
+ pub async fn check_long_running_limits(
955
+ &self,
956
+ execution_id: &ExecutionId,
957
+ policy: &LongRunningExecutionPolicy,
958
+ ) -> EnforcementResult {
959
+ // Check cost threshold first (most critical for runaway costs)
960
+ let cost_check = self.check_cost_threshold(execution_id, policy).await;
961
+ if cost_check.is_blocked() {
962
+ return cost_check;
963
+ }
964
+
965
+ // Check discovery depth (prevents infinite discovery)
966
+ let depth_check = self.check_discovery_depth_limit(execution_id, policy).await;
967
+ if depth_check.is_blocked() {
968
+ return depth_check;
969
+ }
970
+
971
+ // Check discovered step count
972
+ let discovered_check = self.check_discovered_step_limit(execution_id, policy).await;
973
+ if discovered_check.is_blocked() {
974
+ return discovered_check;
975
+ }
976
+
977
+ // Check idle timeout
978
+ let idle_check = self.check_idle_timeout(execution_id, policy).await;
979
+ if idle_check.is_blocked() {
980
+ return idle_check;
981
+ }
982
+
983
+ // Return warnings if any
984
+ if let EnforcementResult::Warning(w) = cost_check {
985
+ return EnforcementResult::Warning(w);
986
+ }
987
+ if let EnforcementResult::Warning(w) = discovered_check {
988
+ return EnforcementResult::Warning(w);
989
+ }
990
+ if let EnforcementResult::Warning(w) = idle_check {
991
+ return EnforcementResult::Warning(w);
992
+ }
993
+
994
+ EnforcementResult::Allowed
995
+ }
996
+ }
997
+
998
+ impl Default for EnforcementMiddleware {
999
+ fn default() -> Self {
1000
+ Self::new()
1001
+ }
1002
+ }
1003
+
1004
+ // =============================================================================
1005
+ // Rate Limiter
1006
+ // =============================================================================
1007
+
1008
+ /// Rate limiter state using token bucket algorithm
1009
+ #[derive(Debug)]
1010
+ struct RateLimiterState {
1011
+ /// Tokens per provider
1012
+ #[allow(dead_code)]
1013
+ provider_tokens: HashMap<String, TokenBucket>,
1014
+ }
1015
+
1016
+ impl RateLimiterState {
1017
+ fn new() -> Self {
1018
+ Self {
1019
+ provider_tokens: HashMap::new(),
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ /// Token bucket for rate limiting
1025
+ #[derive(Debug)]
1026
+ struct TokenBucket {
1027
+ /// Current token count
1028
+ tokens: AtomicU64,
1029
+ /// Maximum tokens (bucket size)
1030
+ max_tokens: u64,
1031
+ /// Tokens added per second
1032
+ refill_rate: u64,
1033
+ /// Last refill timestamp
1034
+ last_refill: RwLock<Instant>,
1035
+ }
1036
+
1037
+ impl TokenBucket {
1038
+ /// Create a new token bucket
1039
+ #[allow(dead_code)]
1040
+ fn new(max_tokens: u64, refill_rate: u64) -> Self {
1041
+ Self {
1042
+ tokens: AtomicU64::new(max_tokens),
1043
+ max_tokens,
1044
+ refill_rate,
1045
+ last_refill: RwLock::new(Instant::now()),
1046
+ }
1047
+ }
1048
+
1049
+ /// Try to acquire tokens
1050
+ #[allow(dead_code)]
1051
+ async fn try_acquire(&self, count: u64) -> bool {
1052
+ // Refill tokens based on elapsed time
1053
+ {
1054
+ let mut last = self.last_refill.write().await;
1055
+ let elapsed = last.elapsed();
1056
+ let new_tokens = (elapsed.as_secs_f64() * self.refill_rate as f64) as u64;
1057
+ if new_tokens > 0 {
1058
+ let current = self.tokens.load(Ordering::SeqCst);
1059
+ let new_total = std::cmp::min(current + new_tokens, self.max_tokens);
1060
+ self.tokens.store(new_total, Ordering::SeqCst);
1061
+ *last = Instant::now();
1062
+ }
1063
+ }
1064
+
1065
+ // Try to acquire
1066
+ let current = self.tokens.load(Ordering::SeqCst);
1067
+ if current >= count {
1068
+ self.tokens.fetch_sub(count, Ordering::SeqCst);
1069
+ true
1070
+ } else {
1071
+ false
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ // =============================================================================
1077
+ // Step Timeout Guard
1078
+ // =============================================================================
1079
+
1080
+ /// Guard for enforcing step timeouts
1081
+ pub struct StepTimeoutGuard {
1082
+ step_id: StepId,
1083
+ timeout: Duration,
1084
+ started_at: Instant,
1085
+ }
1086
+
1087
+ impl StepTimeoutGuard {
1088
+ /// Create a new timeout guard
1089
+ pub fn new(step_id: StepId, timeout: Duration) -> Self {
1090
+ Self {
1091
+ step_id,
1092
+ timeout,
1093
+ started_at: Instant::now(),
1094
+ }
1095
+ }
1096
+
1097
+ /// Check if the timeout has been exceeded
1098
+ pub fn is_timed_out(&self) -> bool {
1099
+ self.started_at.elapsed() > self.timeout
1100
+ }
1101
+
1102
+ /// Get remaining time
1103
+ pub fn remaining(&self) -> Duration {
1104
+ self.timeout.saturating_sub(self.started_at.elapsed())
1105
+ }
1106
+
1107
+ /// Get elapsed time
1108
+ pub fn elapsed(&self) -> Duration {
1109
+ self.started_at.elapsed()
1110
+ }
1111
+
1112
+ /// Check and return an error if timed out
1113
+ pub fn check(&self) -> Result<(), ExecutionError> {
1114
+ if self.is_timed_out() {
1115
+ Err(ExecutionError::timeout(format!(
1116
+ "Step {} timed out after {:?}",
1117
+ self.step_id, self.timeout
1118
+ ))
1119
+ .with_step_id(self.step_id.clone()))
1120
+ } else {
1121
+ Ok(())
1122
+ }
1123
+ }
1124
+ }
1125
+
1126
+ // =============================================================================
1127
+ // Tests
1128
+ // =============================================================================
1129
+
1130
+ #[cfg(test)]
1131
+ mod tests {
1132
+ use super::*;
1133
+
1134
+ #[tokio::test]
1135
+ async fn test_usage_tracking() {
1136
+ let exec_id = ExecutionId::new();
1137
+ let tenant_id = TenantId::from("tenant_test123456789012345");
1138
+ let usage = ExecutionUsage::new(exec_id, tenant_id);
1139
+
1140
+ usage.record_step();
1141
+ usage.record_step();
1142
+ assert_eq!(usage.step_count(), 2);
1143
+
1144
+ usage.record_tokens(100, 50);
1145
+ assert_eq!(usage.total_tokens(), 150);
1146
+ }
1147
+
1148
+ #[tokio::test]
1149
+ async fn test_step_limit_enforcement() {
1150
+ let middleware = EnforcementMiddleware::new();
1151
+ let exec_id = ExecutionId::new();
1152
+ let tenant_id = TenantId::from("tenant_test123456789012345");
1153
+
1154
+ let limits = ResourceLimits {
1155
+ max_steps: 5,
1156
+ max_tokens: 1000,
1157
+ max_wall_time_ms: 60000,
1158
+ max_memory_mb: None,
1159
+ max_concurrent_executions: None,
1160
+ };
1161
+
1162
+ let usage = middleware
1163
+ .register_execution(exec_id.clone(), tenant_id)
1164
+ .await;
1165
+
1166
+ // First 5 steps should be allowed
1167
+ for _ in 0..5 {
1168
+ let result = middleware.check_step_allowed(&exec_id, &limits).await;
1169
+ assert!(result.is_allowed(), "Step should be allowed");
1170
+ usage.record_step();
1171
+ }
1172
+
1173
+ // 6th step should be blocked
1174
+ let result = middleware.check_step_allowed(&exec_id, &limits).await;
1175
+ assert!(result.is_blocked(), "Step should be blocked");
1176
+ }
1177
+
1178
+ #[tokio::test]
1179
+ async fn test_token_limit_enforcement() {
1180
+ let middleware = EnforcementMiddleware::new();
1181
+ let exec_id = ExecutionId::new();
1182
+ let tenant_id = TenantId::from("tenant_test123456789012345");
1183
+
1184
+ let limits = ResourceLimits {
1185
+ max_steps: 100,
1186
+ max_tokens: 100,
1187
+ max_wall_time_ms: 60000,
1188
+ max_memory_mb: None,
1189
+ max_concurrent_executions: None,
1190
+ };
1191
+
1192
+ let usage = middleware
1193
+ .register_execution(exec_id.clone(), tenant_id)
1194
+ .await;
1195
+
1196
+ // Record some tokens
1197
+ usage.record_tokens(50, 30);
1198
+
1199
+ // Check with additional tokens that would exceed
1200
+ let result = middleware.check_tokens_allowed(&exec_id, &limits, 25).await;
1201
+ assert!(
1202
+ result.is_blocked(),
1203
+ "Should be blocked when exceeding limit"
1204
+ );
1205
+
1206
+ // Check with tokens that would stay within limit
1207
+ let result = middleware.check_tokens_allowed(&exec_id, &limits, 10).await;
1208
+ assert!(result.is_allowed(), "Should be allowed within limit");
1209
+ }
1210
+
1211
+ #[tokio::test]
1212
+ async fn test_warning_threshold() {
1213
+ let policy = EnforcementPolicy {
1214
+ warning_threshold: 80,
1215
+ ..Default::default()
1216
+ };
1217
+ let middleware = EnforcementMiddleware::with_policy(policy);
1218
+ let exec_id = ExecutionId::new();
1219
+ let tenant_id = TenantId::from("tenant_test123456789012345");
1220
+
1221
+ let limits = ResourceLimits {
1222
+ max_steps: 10,
1223
+ max_tokens: 1000,
1224
+ max_wall_time_ms: 60000,
1225
+ max_memory_mb: None,
1226
+ max_concurrent_executions: None,
1227
+ };
1228
+
1229
+ let usage = middleware
1230
+ .register_execution(exec_id.clone(), tenant_id)
1231
+ .await;
1232
+
1233
+ // Record 8 steps (80% = warning threshold)
1234
+ for _ in 0..7 {
1235
+ usage.record_step();
1236
+ }
1237
+
1238
+ // 8th step should trigger warning
1239
+ let result = middleware.check_step_allowed(&exec_id, &limits).await;
1240
+ assert!(matches!(result, EnforcementResult::Warning(_)));
1241
+ }
1242
+
1243
+ #[test]
1244
+ fn test_step_timeout_guard() {
1245
+ let step_id = StepId::new();
1246
+ let guard = StepTimeoutGuard::new(step_id, Duration::from_millis(100));
1247
+
1248
+ assert!(!guard.is_timed_out());
1249
+ assert!(guard.check().is_ok());
1250
+
1251
+ // Sleep past timeout
1252
+ std::thread::sleep(Duration::from_millis(150));
1253
+
1254
+ assert!(guard.is_timed_out());
1255
+ assert!(guard.check().is_err());
1256
+ }
1257
+
1258
+ #[tokio::test]
1259
+ async fn test_concurrency_limit() {
1260
+ let middleware = EnforcementMiddleware::new();
1261
+ let tenant_id = TenantId::from("tenant_test123456789012345");
1262
+
1263
+ let limits = ResourceLimits {
1264
+ max_steps: 100,
1265
+ max_tokens: 1000,
1266
+ max_wall_time_ms: 60000,
1267
+ max_memory_mb: None,
1268
+ max_concurrent_executions: Some(2),
1269
+ };
1270
+
1271
+ // Register 2 executions
1272
+ let exec1 = ExecutionId::new();
1273
+ let exec2 = ExecutionId::new();
1274
+ middleware
1275
+ .register_execution(exec1.clone(), tenant_id.clone())
1276
+ .await;
1277
+ middleware
1278
+ .register_execution(exec2.clone(), tenant_id.clone())
1279
+ .await;
1280
+
1281
+ // Third should be blocked
1282
+ let result = middleware
1283
+ .check_concurrency_allowed(&tenant_id, &limits)
1284
+ .await;
1285
+ assert!(result.is_blocked());
1286
+
1287
+ // Unregister one
1288
+ middleware.unregister_execution(&exec1).await;
1289
+
1290
+ // Now should be allowed
1291
+ let result = middleware
1292
+ .check_concurrency_allowed(&tenant_id, &limits)
1293
+ .await;
1294
+ assert!(result.is_allowed());
1295
+ }
1296
+
1297
+ #[test]
1298
+ fn test_network_violation_type() {
1299
+ // Verify NetworkViolation exists and is non-retryable
1300
+ let violation = EnforcementViolation::new(ViolationType::NetworkViolation, 0, 0);
1301
+
1302
+ let error = violation.to_error();
1303
+ assert_eq!(error.category, ExecutionErrorCategory::PolicyViolation);
1304
+ assert!(!error.is_retryable());
1305
+ assert!(error.is_fatal());
1306
+ }
1307
+
1308
+ #[test]
1309
+ fn test_violation_type_display_network() {
1310
+ assert_eq!(
1311
+ format!("{}", ViolationType::NetworkViolation),
1312
+ "network_violation"
1313
+ );
1314
+ }
1315
+ }