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