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,347 @@
|
|
|
1
|
+
//! Tool Executor - Policy-aware tool execution
|
|
2
|
+
//!
|
|
3
|
+
//! This module ensures that EVERY tool invocation passes through policy
|
|
4
|
+
//! evaluation before execution. This is a critical security boundary.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Usage
|
|
7
|
+
//! ```ignore
|
|
8
|
+
//! let policy = ToolPolicy::new();
|
|
9
|
+
//! let executor = ToolExecutor::new(policy);
|
|
10
|
+
//!
|
|
11
|
+
//! // This will evaluate policy before executing
|
|
12
|
+
//! let result = executor.execute(&tool, args, &ctx).await?;
|
|
13
|
+
//! ```
|
|
14
|
+
|
|
15
|
+
use crate::context::TenantContext;
|
|
16
|
+
use crate::kernel::{ExecutionId, StepId};
|
|
17
|
+
use crate::policy::{PolicyAction, PolicyContext, PolicyDecision, PolicyEvaluator, ToolPolicy};
|
|
18
|
+
use crate::streaming::{EventEmitter, StreamEvent};
|
|
19
|
+
use super::Tool;
|
|
20
|
+
use serde_json::Value;
|
|
21
|
+
use std::sync::Arc;
|
|
22
|
+
|
|
23
|
+
/// Error returned when tool execution is denied by policy
|
|
24
|
+
#[derive(Debug, thiserror::Error)]
|
|
25
|
+
pub enum ToolExecutionError {
|
|
26
|
+
#[error("Tool execution denied: {reason}")]
|
|
27
|
+
PolicyDenied { reason: String },
|
|
28
|
+
|
|
29
|
+
#[error("Tool execution error: {0}")]
|
|
30
|
+
ExecutionFailed(#[from] anyhow::Error),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Context for tool execution
|
|
34
|
+
#[derive(Debug, Clone)]
|
|
35
|
+
pub struct ToolExecutionContext {
|
|
36
|
+
/// Execution ID
|
|
37
|
+
pub execution_id: ExecutionId,
|
|
38
|
+
/// Step ID
|
|
39
|
+
pub step_id: Option<StepId>,
|
|
40
|
+
/// Tenant context (REQUIRED)
|
|
41
|
+
pub tenant: TenantContext,
|
|
42
|
+
/// Additional metadata
|
|
43
|
+
pub metadata: std::collections::HashMap<String, String>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl ToolExecutionContext {
|
|
47
|
+
/// Create a new tool execution context
|
|
48
|
+
pub fn new(execution_id: ExecutionId, tenant: TenantContext) -> Self {
|
|
49
|
+
Self {
|
|
50
|
+
execution_id,
|
|
51
|
+
step_id: None,
|
|
52
|
+
tenant,
|
|
53
|
+
metadata: std::collections::HashMap::new(),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Set step ID
|
|
58
|
+
pub fn with_step(mut self, step_id: StepId) -> Self {
|
|
59
|
+
self.step_id = Some(step_id);
|
|
60
|
+
self
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Policy-aware tool executor
|
|
65
|
+
///
|
|
66
|
+
/// This executor ensures that every tool invocation passes through
|
|
67
|
+
/// ToolPolicy::evaluate() before execution. Policy decisions are emitted
|
|
68
|
+
/// as events for audit trail when an emitter is configured.
|
|
69
|
+
pub struct ToolExecutor {
|
|
70
|
+
policy: Arc<ToolPolicy>,
|
|
71
|
+
/// Optional event emitter for policy decision audit trail
|
|
72
|
+
emitter: Option<EventEmitter>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
impl ToolExecutor {
|
|
76
|
+
/// Create a new tool executor with the given policy
|
|
77
|
+
pub fn new(policy: ToolPolicy) -> Self {
|
|
78
|
+
Self {
|
|
79
|
+
policy: Arc::new(policy),
|
|
80
|
+
emitter: None,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Create with a shared policy
|
|
85
|
+
pub fn with_shared_policy(policy: Arc<ToolPolicy>) -> Self {
|
|
86
|
+
Self {
|
|
87
|
+
policy,
|
|
88
|
+
emitter: None,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Configure an event emitter for policy decision audit trail
|
|
93
|
+
pub fn with_emitter(mut self, emitter: EventEmitter) -> Self {
|
|
94
|
+
self.emitter = Some(emitter);
|
|
95
|
+
self
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Set the event emitter for policy decision audit trail
|
|
99
|
+
pub fn set_emitter(&mut self, emitter: EventEmitter) {
|
|
100
|
+
self.emitter = Some(emitter);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Execute a tool with policy enforcement
|
|
104
|
+
///
|
|
105
|
+
/// This is the ONLY way to execute tools - it ensures policy is always checked.
|
|
106
|
+
/// Policy decisions are emitted as events for audit trail when an emitter is configured.
|
|
107
|
+
pub async fn execute(
|
|
108
|
+
&self,
|
|
109
|
+
tool: &dyn Tool,
|
|
110
|
+
args: Value,
|
|
111
|
+
ctx: &ToolExecutionContext,
|
|
112
|
+
) -> Result<Value, ToolExecutionError> {
|
|
113
|
+
// Create policy context
|
|
114
|
+
let policy_ctx = PolicyContext {
|
|
115
|
+
tenant_id: Some(ctx.tenant.tenant_id().as_str().to_string()),
|
|
116
|
+
user_id: ctx.tenant.user_id().map(|u| u.as_str().to_string()),
|
|
117
|
+
action: PolicyAction::InvokeTool {
|
|
118
|
+
tool_name: tool.name().to_string(),
|
|
119
|
+
},
|
|
120
|
+
metadata: ctx.metadata.clone(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let tool_name = tool.name().to_string();
|
|
124
|
+
|
|
125
|
+
// Evaluate policy and emit decision event for audit trail
|
|
126
|
+
match self.policy.evaluate(&policy_ctx) {
|
|
127
|
+
PolicyDecision::Allow => {
|
|
128
|
+
// Emit allow decision event
|
|
129
|
+
if let Some(emitter) = &self.emitter {
|
|
130
|
+
emitter.emit(StreamEvent::policy_decision_allow(
|
|
131
|
+
&ctx.execution_id,
|
|
132
|
+
ctx.step_id.as_ref(),
|
|
133
|
+
&tool_name,
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
// Policy allows execution
|
|
137
|
+
tool.execute(args).await.map_err(ToolExecutionError::from)
|
|
138
|
+
}
|
|
139
|
+
PolicyDecision::Deny { reason } => {
|
|
140
|
+
// Emit deny decision event
|
|
141
|
+
if let Some(emitter) = &self.emitter {
|
|
142
|
+
emitter.emit(StreamEvent::policy_decision_deny(
|
|
143
|
+
&ctx.execution_id,
|
|
144
|
+
ctx.step_id.as_ref(),
|
|
145
|
+
&tool_name,
|
|
146
|
+
&reason,
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
Err(ToolExecutionError::PolicyDenied { reason })
|
|
150
|
+
}
|
|
151
|
+
PolicyDecision::Warn { message } => {
|
|
152
|
+
// Emit warn decision event
|
|
153
|
+
if let Some(emitter) = &self.emitter {
|
|
154
|
+
emitter.emit(StreamEvent::policy_decision_warn(
|
|
155
|
+
&ctx.execution_id,
|
|
156
|
+
ctx.step_id.as_ref(),
|
|
157
|
+
&tool_name,
|
|
158
|
+
&message,
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
// Policy warns but allows - log and continue
|
|
162
|
+
tracing::warn!(tool = tool.name(), message = %message, "Tool policy warning");
|
|
163
|
+
tool.execute(args).await.map_err(ToolExecutionError::from)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Execute multiple tools in sequence, stopping on first policy denial
|
|
169
|
+
pub async fn execute_sequence(
|
|
170
|
+
&self,
|
|
171
|
+
tools: &[(Arc<dyn Tool>, Value)],
|
|
172
|
+
ctx: &ToolExecutionContext,
|
|
173
|
+
) -> Result<Vec<Value>, ToolExecutionError> {
|
|
174
|
+
let mut results = Vec::new();
|
|
175
|
+
for (tool, args) in tools {
|
|
176
|
+
let result = self.execute(tool.as_ref(), args.clone(), ctx).await?;
|
|
177
|
+
results.push(result);
|
|
178
|
+
}
|
|
179
|
+
Ok(results)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// Check if a tool would be allowed by policy (without executing)
|
|
183
|
+
pub fn is_allowed(&self, tool_name: &str, ctx: &ToolExecutionContext) -> bool {
|
|
184
|
+
let policy_ctx = PolicyContext {
|
|
185
|
+
tenant_id: Some(ctx.tenant.tenant_id().as_str().to_string()),
|
|
186
|
+
user_id: ctx.tenant.user_id().map(|u| u.as_str().to_string()),
|
|
187
|
+
action: PolicyAction::InvokeTool {
|
|
188
|
+
tool_name: tool_name.to_string(),
|
|
189
|
+
},
|
|
190
|
+
metadata: std::collections::HashMap::new(),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
matches!(
|
|
194
|
+
self.policy.evaluate(&policy_ctx),
|
|
195
|
+
PolicyDecision::Allow | PolicyDecision::Warn { .. }
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Get the permissions for a tool
|
|
200
|
+
pub fn get_permissions(&self, tool_name: &str) -> &crate::policy::ToolPermissions {
|
|
201
|
+
self.policy.get_permissions(tool_name)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Get the policy reference
|
|
205
|
+
pub fn policy(&self) -> &ToolPolicy {
|
|
206
|
+
&self.policy
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
impl Default for ToolExecutor {
|
|
211
|
+
fn default() -> Self {
|
|
212
|
+
Self::new(ToolPolicy::default())
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(test)]
|
|
217
|
+
mod tests {
|
|
218
|
+
use super::*;
|
|
219
|
+
use crate::kernel::TenantId;
|
|
220
|
+
use async_trait::async_trait;
|
|
221
|
+
|
|
222
|
+
struct MockTool {
|
|
223
|
+
name: String,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[async_trait]
|
|
227
|
+
impl Tool for MockTool {
|
|
228
|
+
fn name(&self) -> &str {
|
|
229
|
+
&self.name
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn description(&self) -> &str {
|
|
233
|
+
"Mock tool for testing"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async fn execute(&self, args: Value) -> anyhow::Result<Value> {
|
|
237
|
+
Ok(args)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[tokio::test]
|
|
242
|
+
async fn test_tool_execution_allowed() {
|
|
243
|
+
let policy = ToolPolicy::new();
|
|
244
|
+
let executor = ToolExecutor::new(policy);
|
|
245
|
+
|
|
246
|
+
let tool = MockTool {
|
|
247
|
+
name: "test_tool".to_string(),
|
|
248
|
+
};
|
|
249
|
+
let ctx = ToolExecutionContext::new(
|
|
250
|
+
ExecutionId::new(),
|
|
251
|
+
TenantContext::new(TenantId::from("tenant_123")),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
let result = executor.execute(&tool, Value::String("test".into()), &ctx).await;
|
|
255
|
+
assert!(result.is_ok());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[tokio::test]
|
|
259
|
+
async fn test_tool_execution_blocked() {
|
|
260
|
+
let policy = ToolPolicy::new().block_tool("blocked_tool");
|
|
261
|
+
let executor = ToolExecutor::new(policy);
|
|
262
|
+
|
|
263
|
+
let tool = MockTool {
|
|
264
|
+
name: "blocked_tool".to_string(),
|
|
265
|
+
};
|
|
266
|
+
let ctx = ToolExecutionContext::new(
|
|
267
|
+
ExecutionId::new(),
|
|
268
|
+
TenantContext::new(TenantId::from("tenant_123")),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
let result = executor.execute(&tool, Value::Null, &ctx).await;
|
|
272
|
+
assert!(matches!(result, Err(ToolExecutionError::PolicyDenied { .. })));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[tokio::test]
|
|
276
|
+
async fn test_is_allowed() {
|
|
277
|
+
let policy = ToolPolicy::new().block_tool("blocked_tool");
|
|
278
|
+
let executor = ToolExecutor::new(policy);
|
|
279
|
+
|
|
280
|
+
let ctx = ToolExecutionContext::new(
|
|
281
|
+
ExecutionId::new(),
|
|
282
|
+
TenantContext::new(TenantId::from("tenant_123")),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert!(executor.is_allowed("allowed_tool", &ctx));
|
|
286
|
+
assert!(!executor.is_allowed("blocked_tool", &ctx));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[tokio::test]
|
|
290
|
+
async fn test_policy_decision_event_emission_allowed() {
|
|
291
|
+
let policy = ToolPolicy::new();
|
|
292
|
+
let emitter = EventEmitter::new();
|
|
293
|
+
let executor = ToolExecutor::new(policy).with_emitter(emitter.clone());
|
|
294
|
+
|
|
295
|
+
let tool = MockTool {
|
|
296
|
+
name: "test_tool".to_string(),
|
|
297
|
+
};
|
|
298
|
+
let ctx = ToolExecutionContext::new(
|
|
299
|
+
ExecutionId::new(),
|
|
300
|
+
TenantContext::new(TenantId::from("tenant_123")),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
let result = executor.execute(&tool, Value::Null, &ctx).await;
|
|
304
|
+
assert!(result.is_ok());
|
|
305
|
+
|
|
306
|
+
// Check that allow event was emitted
|
|
307
|
+
let events = emitter.drain();
|
|
308
|
+
assert_eq!(events.len(), 1);
|
|
309
|
+
match &events[0] {
|
|
310
|
+
StreamEvent::PolicyDecision { decision, tool_name, .. } => {
|
|
311
|
+
assert_eq!(decision, "allow");
|
|
312
|
+
assert_eq!(tool_name, "test_tool");
|
|
313
|
+
}
|
|
314
|
+
_ => panic!("Expected PolicyDecision event"),
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#[tokio::test]
|
|
319
|
+
async fn test_policy_decision_event_emission_denied() {
|
|
320
|
+
let policy = ToolPolicy::new().block_tool("blocked_tool");
|
|
321
|
+
let emitter = EventEmitter::new();
|
|
322
|
+
let executor = ToolExecutor::new(policy).with_emitter(emitter.clone());
|
|
323
|
+
|
|
324
|
+
let tool = MockTool {
|
|
325
|
+
name: "blocked_tool".to_string(),
|
|
326
|
+
};
|
|
327
|
+
let ctx = ToolExecutionContext::new(
|
|
328
|
+
ExecutionId::new(),
|
|
329
|
+
TenantContext::new(TenantId::from("tenant_123")),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
let result = executor.execute(&tool, Value::Null, &ctx).await;
|
|
333
|
+
assert!(matches!(result, Err(ToolExecutionError::PolicyDenied { .. })));
|
|
334
|
+
|
|
335
|
+
// Check that deny event was emitted
|
|
336
|
+
let events = emitter.drain();
|
|
337
|
+
assert_eq!(events.len(), 1);
|
|
338
|
+
match &events[0] {
|
|
339
|
+
StreamEvent::PolicyDecision { decision, tool_name, reason, .. } => {
|
|
340
|
+
assert_eq!(decision, "deny");
|
|
341
|
+
assert_eq!(tool_name, "blocked_tool");
|
|
342
|
+
assert!(reason.is_some());
|
|
343
|
+
}
|
|
344
|
+
_ => panic!("Expected PolicyDecision event"),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//! File system tools for reading and writing files
|
|
2
|
+
|
|
3
|
+
use crate::tool::Tool;
|
|
4
|
+
use async_trait::async_trait;
|
|
5
|
+
use serde_json::json;
|
|
6
|
+
use std::path::Path;
|
|
7
|
+
|
|
8
|
+
const MAX_FILE_SIZE_BYTES: u64 = 10 * 1024 * 1024; // 10MB
|
|
9
|
+
|
|
10
|
+
/// Read file contents from workspace
|
|
11
|
+
pub struct FileReadTool;
|
|
12
|
+
|
|
13
|
+
impl FileReadTool {
|
|
14
|
+
pub fn new() -> Self {
|
|
15
|
+
Self
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Default for FileReadTool {
|
|
20
|
+
fn default() -> Self {
|
|
21
|
+
Self::new()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[async_trait]
|
|
26
|
+
impl Tool for FileReadTool {
|
|
27
|
+
fn name(&self) -> &str {
|
|
28
|
+
"file_read"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn description(&self) -> &str {
|
|
32
|
+
"Read the contents of a file in the workspace"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn parameters_schema(&self) -> serde_json::Value {
|
|
36
|
+
json!({
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"path": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "Relative path to the file within the workspace"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"required": ["path"]
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn requires_network(&self) -> bool {
|
|
49
|
+
false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
|
|
53
|
+
let path = args
|
|
54
|
+
.get("path")
|
|
55
|
+
.and_then(|v| v.as_str())
|
|
56
|
+
.ok_or_else(|| anyhow::anyhow!("Missing 'path' parameter"))?;
|
|
57
|
+
|
|
58
|
+
let path = Path::new(path);
|
|
59
|
+
|
|
60
|
+
// Security: Prevent directory traversal
|
|
61
|
+
if path.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
|
|
62
|
+
anyhow::bail!("Path cannot contain '..' (directory traversal not allowed)");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check file exists
|
|
66
|
+
if !path.exists() {
|
|
67
|
+
anyhow::bail!("File not found: {}", path.display());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check it's a file
|
|
71
|
+
if !path.is_file() {
|
|
72
|
+
anyhow::bail!("Path is not a file: {}", path.display());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check file size
|
|
76
|
+
let metadata = tokio::fs::metadata(path).await?;
|
|
77
|
+
if metadata.len() > MAX_FILE_SIZE_BYTES {
|
|
78
|
+
anyhow::bail!(
|
|
79
|
+
"File too large: {} bytes (max: {} bytes)",
|
|
80
|
+
metadata.len(),
|
|
81
|
+
MAX_FILE_SIZE_BYTES
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let content = tokio::fs::read_to_string(path).await?;
|
|
86
|
+
|
|
87
|
+
Ok(json!({
|
|
88
|
+
"success": true,
|
|
89
|
+
"content": content,
|
|
90
|
+
"path": path.to_string_lossy().to_string(),
|
|
91
|
+
"size": metadata.len()
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Write file contents to workspace
|
|
97
|
+
pub struct FileWriteTool;
|
|
98
|
+
|
|
99
|
+
impl FileWriteTool {
|
|
100
|
+
pub fn new() -> Self {
|
|
101
|
+
Self
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
impl Default for FileWriteTool {
|
|
106
|
+
fn default() -> Self {
|
|
107
|
+
Self::new()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[async_trait]
|
|
112
|
+
impl Tool for FileWriteTool {
|
|
113
|
+
fn name(&self) -> &str {
|
|
114
|
+
"file_write"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn description(&self) -> &str {
|
|
118
|
+
"Write content to a file in the workspace (creates or overwrites)"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn parameters_schema(&self) -> serde_json::Value {
|
|
122
|
+
json!({
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"path": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Relative path to the file within the workspace"
|
|
128
|
+
},
|
|
129
|
+
"content": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Content to write to the file"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"required": ["path", "content"]
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn requires_network(&self) -> bool {
|
|
139
|
+
false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
|
|
143
|
+
let path = args
|
|
144
|
+
.get("path")
|
|
145
|
+
.and_then(|v| v.as_str())
|
|
146
|
+
.ok_or_else(|| anyhow::anyhow!("Missing 'path' parameter"))?;
|
|
147
|
+
|
|
148
|
+
let content = args
|
|
149
|
+
.get("content")
|
|
150
|
+
.and_then(|v| v.as_str())
|
|
151
|
+
.ok_or_else(|| anyhow::anyhow!("Missing 'content' parameter"))?;
|
|
152
|
+
|
|
153
|
+
let path = Path::new(path);
|
|
154
|
+
|
|
155
|
+
// Security: Prevent directory traversal
|
|
156
|
+
if path.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
|
|
157
|
+
anyhow::bail!("Path cannot contain '..' (directory traversal not allowed)");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create parent directories if needed
|
|
161
|
+
if let Some(parent) = path.parent() {
|
|
162
|
+
tokio::fs::create_dir_all(parent).await?;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
tokio::fs::write(path, content).await?;
|
|
166
|
+
|
|
167
|
+
let metadata = tokio::fs::metadata(path).await?;
|
|
168
|
+
|
|
169
|
+
Ok(json!({
|
|
170
|
+
"success": true,
|
|
171
|
+
"path": path.to_string_lossy().to_string(),
|
|
172
|
+
"size": metadata.len(),
|
|
173
|
+
"message": "File written successfully"
|
|
174
|
+
}))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[cfg(test)]
|
|
179
|
+
mod tests {
|
|
180
|
+
use super::*;
|
|
181
|
+
|
|
182
|
+
#[tokio::test]
|
|
183
|
+
async fn test_file_read_success() {
|
|
184
|
+
let tool = FileReadTool::new();
|
|
185
|
+
|
|
186
|
+
// Create a test file
|
|
187
|
+
let test_content = "Hello, World!";
|
|
188
|
+
tokio::fs::write("/tmp/test_read.txt", test_content).await.unwrap();
|
|
189
|
+
|
|
190
|
+
let result = tool.execute(json!({"path": "/tmp/test_read.txt"})).await.unwrap();
|
|
191
|
+
assert_eq!(result["success"], true);
|
|
192
|
+
assert_eq!(result["content"], test_content);
|
|
193
|
+
|
|
194
|
+
// Cleanup
|
|
195
|
+
tokio::fs::remove_file("/tmp/test_read.txt").await.ok();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#[tokio::test]
|
|
199
|
+
async fn test_file_write_success() {
|
|
200
|
+
let tool = FileWriteTool::new();
|
|
201
|
+
|
|
202
|
+
let result = tool.execute(json!({
|
|
203
|
+
"path": "/tmp/test_write.txt",
|
|
204
|
+
"content": "Test content"
|
|
205
|
+
})).await.unwrap();
|
|
206
|
+
|
|
207
|
+
assert_eq!(result["success"], true);
|
|
208
|
+
|
|
209
|
+
// Verify file was written
|
|
210
|
+
let content = tokio::fs::read_to_string("/tmp/test_write.txt").await.unwrap();
|
|
211
|
+
assert_eq!(content, "Test content");
|
|
212
|
+
|
|
213
|
+
// Cleanup
|
|
214
|
+
tokio::fs::remove_file("/tmp/test_write.txt").await.ok();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[tokio::test]
|
|
218
|
+
async fn test_file_read_not_found() {
|
|
219
|
+
let tool = FileReadTool::new();
|
|
220
|
+
let result = tool.execute(json!({"path": "/tmp/nonexistent_file_xyz.txt"})).await;
|
|
221
|
+
assert!(result.is_err());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#[tokio::test]
|
|
225
|
+
async fn test_file_read_traversal_prevention() {
|
|
226
|
+
let tool = FileReadTool::new();
|
|
227
|
+
let result = tool.execute(json!({"path": "../etc/passwd"})).await;
|
|
228
|
+
assert!(result.is_err());
|
|
229
|
+
assert!(result.unwrap_err().to_string().contains("directory traversal"));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//! Function tool - wrap any async function as a tool
|
|
2
|
+
|
|
3
|
+
use super::Tool;
|
|
4
|
+
use async_trait::async_trait;
|
|
5
|
+
use serde_json::Value;
|
|
6
|
+
use std::future::Future;
|
|
7
|
+
use std::pin::Pin;
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
/// Type alias for async tool functions
|
|
11
|
+
pub type ToolFn = Arc<
|
|
12
|
+
dyn Fn(Value) -> Pin<Box<dyn Future<Output = anyhow::Result<Value>> + Send>>
|
|
13
|
+
+ Send
|
|
14
|
+
+ Sync,
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/// FunctionTool - wraps an async closure as a tool
|
|
18
|
+
pub struct FunctionTool {
|
|
19
|
+
name: String,
|
|
20
|
+
description: String,
|
|
21
|
+
parameters: Value,
|
|
22
|
+
func: ToolFn,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl FunctionTool {
|
|
26
|
+
/// Create a new function tool
|
|
27
|
+
pub fn new<F, Fut>(
|
|
28
|
+
name: impl Into<String>,
|
|
29
|
+
description: impl Into<String>,
|
|
30
|
+
parameters: Value,
|
|
31
|
+
func: F,
|
|
32
|
+
) -> Self
|
|
33
|
+
where
|
|
34
|
+
F: Fn(Value) -> Fut + Send + Sync + 'static,
|
|
35
|
+
Fut: Future<Output = anyhow::Result<Value>> + Send + 'static,
|
|
36
|
+
{
|
|
37
|
+
let func = Arc::new(move |args: Value| {
|
|
38
|
+
let fut = func(args);
|
|
39
|
+
Box::pin(fut) as Pin<Box<dyn Future<Output = anyhow::Result<Value>> + Send>>
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
Self {
|
|
43
|
+
name: name.into(),
|
|
44
|
+
description: description.into(),
|
|
45
|
+
parameters,
|
|
46
|
+
func,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Create a simple tool with no parameters
|
|
51
|
+
pub fn simple<F, Fut>(name: impl Into<String>, description: impl Into<String>, func: F) -> Self
|
|
52
|
+
where
|
|
53
|
+
F: Fn(Value) -> Fut + Send + Sync + 'static,
|
|
54
|
+
Fut: Future<Output = anyhow::Result<Value>> + Send + 'static,
|
|
55
|
+
{
|
|
56
|
+
Self::new(
|
|
57
|
+
name,
|
|
58
|
+
description,
|
|
59
|
+
serde_json::json!({
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {},
|
|
62
|
+
"required": []
|
|
63
|
+
}),
|
|
64
|
+
func,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[async_trait]
|
|
70
|
+
impl Tool for FunctionTool {
|
|
71
|
+
fn name(&self) -> &str {
|
|
72
|
+
&self.name
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn description(&self) -> &str {
|
|
76
|
+
&self.description
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn parameters_schema(&self) -> Value {
|
|
80
|
+
self.parameters.clone()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async fn execute(&self, args: Value) -> anyhow::Result<Value> {
|
|
84
|
+
(self.func)(args).await
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Macro to create a FunctionTool more easily
|
|
89
|
+
#[macro_export]
|
|
90
|
+
macro_rules! tool {
|
|
91
|
+
($name:expr, $desc:expr, |$args:ident| $body:expr) => {
|
|
92
|
+
FunctionTool::new(
|
|
93
|
+
$name,
|
|
94
|
+
$desc,
|
|
95
|
+
serde_json::json!({"type": "object", "properties": {}}),
|
|
96
|
+
|$args| async move { $body },
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
}
|