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.
- package/.env.example +20 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/publish.yml +250 -0
- package/.gitmodules +3 -0
- package/Cargo.lock +3584 -0
- package/Cargo.toml +97 -0
- package/crates/enact/Cargo.toml +27 -0
- package/crates/enact/src/lib.rs +60 -0
- package/crates/enact-a2a/Cargo.toml +25 -0
- package/crates/enact-a2a/src/lib.rs +411 -0
- package/crates/enact-channels/Cargo.toml +64 -0
- package/crates/enact-channels/examples/README.md +80 -0
- package/crates/enact-channels/examples/channel_bot.rs +169 -0
- package/crates/enact-channels/examples/telegram-echo.rs +34 -0
- package/crates/enact-channels/examples/whatsapp-echo.rs +142 -0
- package/crates/enact-channels/src/config.rs +213 -0
- package/crates/enact-channels/src/lib.rs +25 -0
- package/crates/enact-channels/src/runtime.rs +237 -0
- package/crates/enact-channels/src/security/mod.rs +5 -0
- package/crates/enact-channels/src/security/pairing.rs +205 -0
- package/crates/enact-channels/src/teams.rs +601 -0
- package/crates/enact-channels/src/telegram.rs +2833 -0
- package/crates/enact-channels/src/traits.rs +200 -0
- package/crates/enact-channels/src/webhook.rs +262 -0
- package/crates/enact-channels/src/whatsapp.rs +310 -0
- package/crates/enact-cli/Cargo.toml +40 -0
- package/crates/enact-cli/src/commands/doctor.rs +62 -0
- package/crates/enact-cli/src/commands/mod.rs +3 -0
- package/crates/enact-cli/src/commands/run.rs +69 -0
- package/crates/enact-cli/src/commands/serve.rs +81 -0
- package/crates/enact-cli/src/config.rs +2 -0
- package/crates/enact-cli/src/main.rs +79 -0
- package/crates/enact-config/Cargo.toml +36 -0
- package/crates/enact-config/ENV_VAR_MAPPING.md +135 -0
- package/crates/enact-config/QUICK_REFERENCE.md +92 -0
- package/crates/enact-config/README.md +107 -0
- package/crates/enact-config/TESTING.md +161 -0
- package/crates/enact-config/examples/test-env-vars.rs +100 -0
- package/crates/enact-config/src/config.rs +399 -0
- package/crates/enact-config/src/encrypted_store.rs +211 -0
- package/crates/enact-config/src/lib.rs +298 -0
- package/crates/enact-config/src/secrets.rs +149 -0
- package/crates/enact-config/src/sync.rs +260 -0
- package/crates/enact-config/test-env-vars.sh +34 -0
- package/crates/enact-config/tests/README.md +99 -0
- package/crates/enact-config/tests/config_integration_test.rs +202 -0
- package/crates/enact-config/tests/security_test.rs +140 -0
- package/crates/enact-context/Cargo.toml +41 -0
- package/crates/enact-context/src/budget.rs +314 -0
- package/crates/enact-context/src/calibrator.rs +535 -0
- package/crates/enact-context/src/compactor.rs +392 -0
- package/crates/enact-context/src/condenser.rs +826 -0
- package/crates/enact-context/src/lib.rs +94 -0
- package/crates/enact-context/src/segment.rs +238 -0
- package/crates/enact-context/src/step_context.rs +645 -0
- package/crates/enact-context/src/token_counter.rs +148 -0
- package/crates/enact-context/src/window.rs +372 -0
- package/crates/enact-core/Cargo.toml +42 -0
- package/crates/enact-core/README.md +98 -0
- package/crates/enact-core/src/background/executor.rs +524 -0
- package/crates/enact-core/src/background/mod.rs +48 -0
- package/crates/enact-core/src/background/target_binding.rs +390 -0
- package/crates/enact-core/src/background/trigger.rs +511 -0
- package/crates/enact-core/src/callable/callable.rs +152 -0
- package/crates/enact-core/src/callable/composite.rs +817 -0
- package/crates/enact-core/src/callable/graph.rs +104 -0
- package/crates/enact-core/src/callable/llm.rs +211 -0
- package/crates/enact-core/src/callable/mod.rs +64 -0
- package/crates/enact-core/src/callable/registry.rs +206 -0
- package/crates/enact-core/src/context/execution_context.rs +757 -0
- package/crates/enact-core/src/context/invocation.rs +99 -0
- package/crates/enact-core/src/context/mod.rs +50 -0
- package/crates/enact-core/src/context/tenant.rs +175 -0
- package/crates/enact-core/src/context/trace.rs +127 -0
- package/crates/enact-core/src/flow/conditional.rs +293 -0
- package/crates/enact-core/src/flow/mod.rs +43 -0
- package/crates/enact-core/src/flow/parallel.rs +437 -0
- package/crates/enact-core/src/flow/repeat.rs +534 -0
- package/crates/enact-core/src/flow/sequential.rs +248 -0
- package/crates/enact-core/src/graph/checkpoint.rs +79 -0
- package/crates/enact-core/src/graph/checkpoint_store.rs +76 -0
- package/crates/enact-core/src/graph/compiled.rs +189 -0
- package/crates/enact-core/src/graph/edge.rs +59 -0
- package/crates/enact-core/src/graph/graph_schema.rs +218 -0
- package/crates/enact-core/src/graph/loader.rs +155 -0
- package/crates/enact-core/src/graph/mod.rs +18 -0
- package/crates/enact-core/src/graph/node/function.rs +49 -0
- package/crates/enact-core/src/graph/node/mod.rs +48 -0
- package/crates/enact-core/src/graph/schema.rs +62 -0
- package/crates/enact-core/src/inbox/message.rs +405 -0
- package/crates/enact-core/src/inbox/mod.rs +31 -0
- package/crates/enact-core/src/inbox/store.rs +355 -0
- package/crates/enact-core/src/kernel/artifact/filesystem.rs +546 -0
- package/crates/enact-core/src/kernel/artifact/metadata.rs +283 -0
- package/crates/enact-core/src/kernel/artifact/mod.rs +27 -0
- package/crates/enact-core/src/kernel/artifact/store.rs +427 -0
- package/crates/enact-core/src/kernel/enforcement.rs +1315 -0
- package/crates/enact-core/src/kernel/error.rs +1200 -0
- package/crates/enact-core/src/kernel/event.rs +1394 -0
- package/crates/enact-core/src/kernel/execution_model.rs +831 -0
- package/crates/enact-core/src/kernel/execution_state.rs +189 -0
- package/crates/enact-core/src/kernel/execution_strategy.rs +117 -0
- package/crates/enact-core/src/kernel/ids.rs +2086 -0
- package/crates/enact-core/src/kernel/interrupt.rs +125 -0
- package/crates/enact-core/src/kernel/kernel.rs +1283 -0
- package/crates/enact-core/src/kernel/mod.rs +205 -0
- package/crates/enact-core/src/kernel/persistence/event_store.rs +270 -0
- package/crates/enact-core/src/kernel/persistence/message_store.rs +908 -0
- package/crates/enact-core/src/kernel/persistence/mod.rs +102 -0
- package/crates/enact-core/src/kernel/persistence/state_store.rs +228 -0
- package/crates/enact-core/src/kernel/persistence/vector_store.rs +299 -0
- package/crates/enact-core/src/kernel/reducer.rs +808 -0
- package/crates/enact-core/src/kernel/replay.rs +153 -0
- package/crates/enact-core/src/lib.rs +413 -0
- package/crates/enact-core/src/memory/episodic.rs +0 -0
- package/crates/enact-core/src/memory/mod.rs +6 -0
- package/crates/enact-core/src/memory/semantic.rs +0 -0
- package/crates/enact-core/src/memory/trait.rs +0 -0
- package/crates/enact-core/src/memory/vector_db.rs +0 -0
- package/crates/enact-core/src/memory/working.rs +0 -0
- package/crates/enact-core/src/policy/execution_policy.rs +292 -0
- package/crates/enact-core/src/policy/filters.rs +458 -0
- package/crates/enact-core/src/policy/input_processor.rs +407 -0
- package/crates/enact-core/src/policy/long_running.rs +134 -0
- package/crates/enact-core/src/policy/mod.rs +193 -0
- package/crates/enact-core/src/policy/pii_input.rs +274 -0
- package/crates/enact-core/src/policy/tenant_policy.rs +453 -0
- package/crates/enact-core/src/policy/tool_policy.rs +407 -0
- package/crates/enact-core/src/providers/mod.rs +63 -0
- package/crates/enact-core/src/providers/trait.rs +292 -0
- package/crates/enact-core/src/runner/callbacks.rs +6 -0
- package/crates/enact-core/src/runner/execution_runner.rs +476 -0
- package/crates/enact-core/src/runner/loop.rs +117 -0
- package/crates/enact-core/src/runner/mod.rs +58 -0
- package/crates/enact-core/src/runner/protected_runner.rs +280 -0
- package/crates/enact-core/src/signal/inmemory.rs +231 -0
- package/crates/enact-core/src/signal/mod.rs +108 -0
- package/crates/enact-core/src/streaming/event_logger.rs +195 -0
- package/crates/enact-core/src/streaming/event_stream.rs +1423 -0
- package/crates/enact-core/src/streaming/mod.rs +108 -0
- package/crates/enact-core/src/streaming/pause_cancel.rs +0 -0
- package/crates/enact-core/src/streaming/protected_emitter.rs +173 -0
- package/crates/enact-core/src/streaming/protection/context.rs +136 -0
- package/crates/enact-core/src/streaming/protection/encryption.rs +289 -0
- package/crates/enact-core/src/streaming/protection/mod.rs +43 -0
- package/crates/enact-core/src/streaming/protection/pii_protection.rs +243 -0
- package/crates/enact-core/src/streaming/protection/processor.rs +166 -0
- package/crates/enact-core/src/streaming/sse.rs +0 -0
- package/crates/enact-core/src/telemetry/exporter.rs +0 -0
- package/crates/enact-core/src/telemetry/init.rs +0 -0
- package/crates/enact-core/src/telemetry/mod.rs +49 -0
- package/crates/enact-core/src/telemetry/spans.rs +245 -0
- package/crates/enact-core/src/tool/agent_tool.rs +177 -0
- package/crates/enact-core/src/tool/browser/mod.rs +0 -0
- package/crates/enact-core/src/tool/browser/webdriver.rs +0 -0
- package/crates/enact-core/src/tool/cost.rs +247 -0
- package/crates/enact-core/src/tool/discovery.rs +0 -0
- package/crates/enact-core/src/tool/dispatcher.rs +347 -0
- package/crates/enact-core/src/tool/filesystem.rs +231 -0
- package/crates/enact-core/src/tool/function.rs +99 -0
- package/crates/enact-core/src/tool/git.rs +162 -0
- package/crates/enact-core/src/tool/http.rs +214 -0
- package/crates/enact-core/src/tool/mcp/client.rs +0 -0
- package/crates/enact-core/src/tool/mcp/mod.rs +0 -0
- package/crates/enact-core/src/tool/mod.rs +51 -0
- package/crates/enact-core/src/tool/reasoning/debugging.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/mcts.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/mod.rs +0 -0
- package/crates/enact-core/src/tool/reasoning/sequential.rs +0 -0
- package/crates/enact-core/src/tool/sandbox/dagger.rs +0 -0
- package/crates/enact-core/src/tool/sandbox/mod.rs +0 -0
- package/crates/enact-core/src/tool/shell.rs +147 -0
- package/crates/enact-core/src/tool/trait.rs +33 -0
- package/crates/enact-core/src/tool/web_search.rs +277 -0
- package/crates/enact-core/src/util/config.rs +0 -0
- package/crates/enact-core/src/util/errors.rs +0 -0
- package/crates/enact-core/src/util/mod.rs +6 -0
- package/crates/enact-core/tests/airgapped_e2e_test.rs +291 -0
- package/crates/enact-core/tests/e2e_agentic_loop.rs +119 -0
- package/crates/enact-core/tests/e2e_test.rs +259 -0
- package/crates/enact-core/tests/graph_test.rs +130 -0
- package/crates/enact-core/tests/stream_event_id_validation.rs +435 -0
- package/crates/enact-cron/Cargo.toml +28 -0
- package/crates/enact-cron/src/lib.rs +44 -0
- package/crates/enact-cron/src/schedule.rs +156 -0
- package/crates/enact-cron/src/store.rs +589 -0
- package/crates/enact-cron/src/types.rs +148 -0
- package/crates/enact-gateway/Cargo.toml +31 -0
- package/crates/enact-gateway/README.md +30 -0
- package/crates/enact-gateway/examples/whatsapp-gateway-runner-mock.rs +59 -0
- package/crates/enact-gateway/examples/whatsapp-gateway.rs +42 -0
- package/crates/enact-gateway/src/lib.rs +582 -0
- package/crates/enact-mcp/Cargo.toml +24 -0
- package/crates/enact-mcp/src/lib.rs +178 -0
- package/crates/enact-memory/Cargo.toml +25 -0
- package/crates/enact-memory/src/backend.rs +20 -0
- package/crates/enact-memory/src/chunker.rs +230 -0
- package/crates/enact-memory/src/embeddings.rs +221 -0
- package/crates/enact-memory/src/lib.rs +67 -0
- package/crates/enact-memory/src/markdown.rs +127 -0
- package/crates/enact-memory/src/none.rs +61 -0
- package/crates/enact-memory/src/sqlite.rs +276 -0
- package/crates/enact-memory/src/traits.rs +65 -0
- package/crates/enact-memory/src/vector.rs +198 -0
- package/crates/enact-oauth/Cargo.toml +27 -0
- package/crates/enact-oauth/src/lib.rs +584 -0
- package/crates/enact-observability/Cargo.toml +22 -0
- package/crates/enact-observability/src/lib.rs +197 -0
- package/crates/enact-providers/Cargo.toml +33 -0
- package/crates/enact-providers/examples/hello-agent.rs +33 -0
- package/crates/enact-providers/src/anthropic.rs +182 -0
- package/crates/enact-providers/src/azure.rs +96 -0
- package/crates/enact-providers/src/bridge.rs +221 -0
- package/crates/enact-providers/src/gemini.rs +227 -0
- package/crates/enact-providers/src/http.rs +78 -0
- package/crates/enact-providers/src/lib.rs +53 -0
- package/crates/enact-providers/src/openai_compatible.rs +167 -0
- package/crates/enact-providers/src/openrouter.rs +33 -0
- package/crates/enact-runner/Cargo.toml +24 -0
- package/crates/enact-runner/README.md +76 -0
- package/crates/enact-runner/src/compaction.rs +225 -0
- package/crates/enact-runner/src/config.rs +118 -0
- package/crates/enact-runner/src/lib.rs +63 -0
- package/crates/enact-runner/src/loop_driver.rs +414 -0
- package/crates/enact-runner/src/parser.rs +421 -0
- package/crates/enact-runner/src/retry.rs +262 -0
- package/crates/enact-runner/tests/integration.rs +278 -0
- package/crates/enact-security/Cargo.toml +22 -0
- package/crates/enact-security/src/audit.rs +375 -0
- package/crates/enact-security/src/lib.rs +37 -0
- package/crates/enact-security/src/policy.rs +406 -0
- package/crates/enact-skills/Cargo.toml +25 -0
- package/crates/enact-skills/src/lib.rs +506 -0
- package/crates/enact-tools/Cargo.toml +22 -0
- package/crates/enact-tools/src/file_read.rs +166 -0
- package/crates/enact-tools/src/file_write.rs +216 -0
- package/crates/enact-tools/src/git_operations.rs +513 -0
- package/crates/enact-tools/src/http_request.rs +417 -0
- package/crates/enact-tools/src/lib.rs +104 -0
- package/crates/enact-tools/src/security.rs +227 -0
- package/crates/enact-tools/src/shell.rs +191 -0
- package/crates/enact-tools/src/traits.rs +159 -0
- package/docs/Makefile +74 -0
- package/docs/config.toml +62 -0
- package/docs/content/_index.md +174 -0
- package/docs/content/a2a/_index.md +431 -0
- package/docs/content/api/_index.md +323 -0
- package/docs/content/channels/_index.md +160 -0
- package/docs/content/channels/teams.md +205 -0
- package/docs/content/channels/telegram.md +182 -0
- package/docs/content/channels/webhook.md +423 -0
- package/docs/content/channels/whatsapp.md +240 -0
- package/docs/content/cli/_index.md +261 -0
- package/docs/content/concepts/_index.md +273 -0
- package/docs/content/configuration/_index.md +241 -0
- package/docs/content/cron/_index.md +248 -0
- package/docs/content/developers/_index.md +278 -0
- package/docs/content/getting-started/_index.md +180 -0
- package/docs/content/installation/_index.md +186 -0
- package/docs/content/installation/uninstall.md +101 -0
- package/docs/content/installation/updating.md +120 -0
- package/docs/content/mcp/_index.md +215 -0
- package/docs/content/memory/_index.md +163 -0
- package/docs/content/oauth/_index.md +515 -0
- package/docs/content/providers/_index.md +206 -0
- package/docs/content/roadmap/_index.md +199 -0
- package/docs/content/security/_index.md +219 -0
- package/docs/content/skills/_index.md +228 -0
- package/docs/content/tools/_index.md +485 -0
- package/docs/content/troubleshooting/_index.md +259 -0
- package/docs/content/yaml-schema/_index.md +294 -0
- package/docs/static/giallo-dark.css +91 -0
- package/docs/static/giallo-light.css +91 -0
- package/docs/themes/tanuki/.github/workflows/deploy.yml +44 -0
- package/docs/themes/tanuki/LICENSE +21 -0
- package/docs/themes/tanuki/README.md +166 -0
- package/docs/themes/tanuki/examples/blog/config.toml +58 -0
- package/docs/themes/tanuki/examples/blog/content/_index.md +4 -0
- package/docs/themes/tanuki/examples/blog/content/about.md +33 -0
- package/docs/themes/tanuki/examples/blog/content/blog/_index.md +7 -0
- package/docs/themes/tanuki/examples/blog/content/blog/api-design-best-practices.md +245 -0
- package/docs/themes/tanuki/examples/blog/content/blog/building-accessible-websites.md +147 -0
- package/docs/themes/tanuki/examples/blog/content/blog/css-grid-vs-flexbox.md +165 -0
- package/docs/themes/tanuki/examples/blog/content/blog/customizing-catppuccin-colors.md +137 -0
- package/docs/themes/tanuki/examples/blog/content/blog/dark-mode-best-practices.md +82 -0
- package/docs/themes/tanuki/examples/blog/content/blog/docker-essentials.md +301 -0
- package/docs/themes/tanuki/examples/blog/content/blog/getting-started-with-zola.md +129 -0
- package/docs/themes/tanuki/examples/blog/content/blog/git-workflow-for-content.md +112 -0
- package/docs/themes/tanuki/examples/blog/content/blog/introduction-to-webassembly.md +183 -0
- package/docs/themes/tanuki/examples/blog/content/blog/modern-javascript-features.md +234 -0
- package/docs/themes/tanuki/examples/blog/content/blog/testing-strategies.md +311 -0
- package/docs/themes/tanuki/examples/blog/content/blog/typography-for-developers.md +104 -0
- package/docs/themes/tanuki/examples/blog/content/blog/welcome-to-tanuki.md +67 -0
- package/docs/themes/tanuki/examples/blog/content/blog/why-static-sites.md +85 -0
- package/docs/themes/tanuki/examples/blog/content/projects.md +64 -0
- package/docs/themes/tanuki/examples/book/config.toml +17 -0
- package/docs/themes/tanuki/examples/book/content/_index.md +12 -0
- package/docs/themes/tanuki/examples/book/content/chapter-1.md +90 -0
- package/docs/themes/tanuki/examples/book/content/chapter-2.md +143 -0
- package/docs/themes/tanuki/examples/book/content/chapter-3.md +217 -0
- package/docs/themes/tanuki/examples/book/content/chapter-4.md +224 -0
- package/docs/themes/tanuki/examples/book/content/chapter-5.md +297 -0
- package/docs/themes/tanuki/examples/book/content/print.md +6 -0
- package/docs/themes/tanuki/examples/docs/config.toml +28 -0
- package/docs/themes/tanuki/examples/docs/content/_index.md +20 -0
- package/docs/themes/tanuki/examples/docs/content/components.md +156 -0
- package/docs/themes/tanuki/examples/docs/content/configuration.md +94 -0
- package/docs/themes/tanuki/examples/docs/content/customization.md +202 -0
- package/docs/themes/tanuki/examples/docs/content/deployment.md +204 -0
- package/docs/themes/tanuki/examples/docs/content/installation.md +59 -0
- package/docs/themes/tanuki/examples/docs/content/print.md +6 -0
- package/docs/themes/tanuki/examples/docs/static/img/tanuki-icon.avif +0 -0
- package/docs/themes/tanuki/examples/index.html +2104 -0
- package/docs/themes/tanuki/mise.toml +108 -0
- package/docs/themes/tanuki/sass/base/_catppuccin.scss +164 -0
- package/docs/themes/tanuki/sass/base/_fonts.scss +64 -0
- package/docs/themes/tanuki/sass/base/_reset.scss +152 -0
- package/docs/themes/tanuki/sass/base/_typography.scss +523 -0
- package/docs/themes/tanuki/sass/components/_buttons.scss +209 -0
- package/docs/themes/tanuki/sass/components/_code.scss +457 -0
- package/docs/themes/tanuki/sass/components/_landing.scss +633 -0
- package/docs/themes/tanuki/sass/components/_layout.scss +294 -0
- package/docs/themes/tanuki/sass/components/_navigation.scss +1200 -0
- package/docs/themes/tanuki/sass/components/_print.scss +237 -0
- package/docs/themes/tanuki/sass/components/_search.scss +224 -0
- package/docs/themes/tanuki/sass/components/_sidebar.scss +473 -0
- package/docs/themes/tanuki/sass/components/_theme-toggle.scss +186 -0
- package/docs/themes/tanuki/sass/modes/_blog.scss +366 -0
- package/docs/themes/tanuki/sass/modes/_product.scss +875 -0
- package/docs/themes/tanuki/sass/modes/_raskell.scss +1696 -0
- package/docs/themes/tanuki/sass/patterns/_buttons.scss +183 -0
- package/docs/themes/tanuki/sass/patterns/_cards.scss +144 -0
- package/docs/themes/tanuki/sass/patterns/_index.scss +9 -0
- package/docs/themes/tanuki/sass/patterns/_lists.scss +259 -0
- package/docs/themes/tanuki/sass/patterns/_sections.scss +243 -0
- package/docs/themes/tanuki/sass/style.scss +47 -0
- package/docs/themes/tanuki/sass/tokens/_colors.scss +139 -0
- package/docs/themes/tanuki/sass/tokens/_spacing.scss +100 -0
- package/docs/themes/tanuki/sass/tokens/_typography.scss +186 -0
- package/docs/themes/tanuki/screenshot.png +0 -0
- package/docs/themes/tanuki/sentinel.kdl +59 -0
- package/docs/themes/tanuki/static/elasticlunr.min.js +10 -0
- package/docs/themes/tanuki/static/fonts/GEIST-LICENSE.txt +92 -0
- package/docs/themes/tanuki/static/fonts/Geist-Variable.woff2 +0 -0
- package/docs/themes/tanuki/static/fonts/GeistMono-Variable.woff2 +0 -0
- package/docs/themes/tanuki/static/img/tanuki-icon.avif +0 -0
- package/docs/themes/tanuki/static/img/tanuki-icon.png +0 -0
- package/docs/themes/tanuki/static/js/anchors.js +18 -0
- package/docs/themes/tanuki/static/js/app.js +274 -0
- package/docs/themes/tanuki/static/js/code.js +394 -0
- package/docs/themes/tanuki/static/js/navigation.js +778 -0
- package/docs/themes/tanuki/static/js/scroll-to-top.js +33 -0
- package/docs/themes/tanuki/static/js/search-raskell.js +240 -0
- package/docs/themes/tanuki/static/js/search.js +215 -0
- package/docs/themes/tanuki/static/js/theme.js +169 -0
- package/docs/themes/tanuki/static/syntax-dark.css +151 -0
- package/docs/themes/tanuki/static/syntax-light.css +151 -0
- package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm.js +486 -0
- package/docs/themes/tanuki/static/wasm/sentinel_playground_wasm_bg.wasm +0 -0
- package/docs/themes/tanuki/templates/404.html +52 -0
- package/docs/themes/tanuki/templates/base.html +428 -0
- package/docs/themes/tanuki/templates/blog.html +66 -0
- package/docs/themes/tanuki/templates/home.html +108 -0
- package/docs/themes/tanuki/templates/index.html +178 -0
- package/docs/themes/tanuki/templates/landing.html +168 -0
- package/docs/themes/tanuki/templates/macros/nav.html +128 -0
- package/docs/themes/tanuki/templates/macros/posts.html +101 -0
- package/docs/themes/tanuki/templates/macros/ui.html +159 -0
- package/docs/themes/tanuki/templates/page.html +135 -0
- package/docs/themes/tanuki/templates/partials/footer.html +38 -0
- package/docs/themes/tanuki/templates/partials/header.html +366 -0
- package/docs/themes/tanuki/templates/partials/nav-buttons.html +55 -0
- package/docs/themes/tanuki/templates/partials/nav-overlay.html +81 -0
- package/docs/themes/tanuki/templates/partials/page-toc-panel.html +43 -0
- package/docs/themes/tanuki/templates/partials/search.html +52 -0
- package/docs/themes/tanuki/templates/partials/sidebar.html +107 -0
- package/docs/themes/tanuki/templates/partials/theme-toggle.html +35 -0
- package/docs/themes/tanuki/templates/partials/toc-overlay.html +146 -0
- package/docs/themes/tanuki/templates/partials/version-picker.html +38 -0
- package/docs/themes/tanuki/templates/print.html +244 -0
- package/docs/themes/tanuki/templates/section.html +186 -0
- package/docs/themes/tanuki/templates/taxonomy_list.html +18 -0
- package/docs/themes/tanuki/templates/taxonomy_single.html +31 -0
- package/docs/themes/tanuki/theme.toml +58 -0
- package/examples/hello-agent.rs +55 -0
- package/package.json +36 -0
- package/proto/config.proto +60 -0
- package/proto/events.proto +0 -0
- 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
|
+
}
|