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,163 @@
|
|
|
1
|
+
+++
|
|
2
|
+
title = "Memory"
|
|
3
|
+
weight = 9
|
|
4
|
+
+++
|
|
5
|
+
|
|
6
|
+
# Memory System
|
|
7
|
+
|
|
8
|
+
The `enact-memory` crate provides local-first memory backends for agents with semantic search capabilities.
|
|
9
|
+
|
|
10
|
+
## Backends
|
|
11
|
+
|
|
12
|
+
| Backend | Description | Use Case |
|
|
13
|
+
|---------|-------------|----------|
|
|
14
|
+
| `sqlite` | SQLite with FTS5 full-text search | Production, persistent storage |
|
|
15
|
+
| `markdown` | Markdown files in workspace | Development, version control |
|
|
16
|
+
| `none` | No persistence | Testing, stateless agents |
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
### Core Memory Operations
|
|
21
|
+
|
|
22
|
+
```rust
|
|
23
|
+
use enact_memory::{create_memory, MemoryCategory};
|
|
24
|
+
|
|
25
|
+
let memory = create_memory("sqlite", workspace_path)?;
|
|
26
|
+
|
|
27
|
+
// Store a memory
|
|
28
|
+
memory.store("user_preference", "prefers dark mode", MemoryCategory::Core, None).await?;
|
|
29
|
+
|
|
30
|
+
// Recall memories (keyword search)
|
|
31
|
+
let results = memory.recall("dark mode", 10, None).await?;
|
|
32
|
+
|
|
33
|
+
// Get specific memory
|
|
34
|
+
let entry = memory.get("user_preference").await?;
|
|
35
|
+
|
|
36
|
+
// List all memories in a category
|
|
37
|
+
let all_core = memory.list(Some(&MemoryCategory::Core), None).await?;
|
|
38
|
+
|
|
39
|
+
// Forget a memory
|
|
40
|
+
memory.forget("user_preference").await?;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Memory Categories
|
|
44
|
+
|
|
45
|
+
| Category | Purpose |
|
|
46
|
+
|----------|---------|
|
|
47
|
+
| `Core` | Long-term facts, preferences, decisions |
|
|
48
|
+
| `Daily` | Session logs, daily context |
|
|
49
|
+
| `Conversation` | Current conversation context |
|
|
50
|
+
| `Custom(name)` | User-defined categories |
|
|
51
|
+
|
|
52
|
+
### Embeddings for Semantic Search
|
|
53
|
+
|
|
54
|
+
The memory system supports embeddings for semantic similarity search:
|
|
55
|
+
|
|
56
|
+
```rust
|
|
57
|
+
use enact_memory::{create_embedding_provider, cosine_similarity};
|
|
58
|
+
|
|
59
|
+
// Create an embedding provider
|
|
60
|
+
let provider = create_embedding_provider(
|
|
61
|
+
"openai",
|
|
62
|
+
Some(&api_key),
|
|
63
|
+
"text-embedding-3-small",
|
|
64
|
+
1536
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Embed text
|
|
68
|
+
let embedding = provider.embed_one("Hello world").await?;
|
|
69
|
+
|
|
70
|
+
// Compare embeddings
|
|
71
|
+
let similarity = cosine_similarity(&embedding_a, &embedding_b);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Supported Providers
|
|
75
|
+
|
|
76
|
+
| Provider | Model | Dimensions |
|
|
77
|
+
|----------|-------|------------|
|
|
78
|
+
| `openai` | text-embedding-3-small | 1536 |
|
|
79
|
+
| `openai` | text-embedding-3-large | 3072 |
|
|
80
|
+
| `custom:URL` | Any OpenAI-compatible | Configurable |
|
|
81
|
+
| `none` | N/A (keyword-only) | 0 |
|
|
82
|
+
|
|
83
|
+
### Text Chunking
|
|
84
|
+
|
|
85
|
+
For large documents, use the chunker to split into semantic chunks:
|
|
86
|
+
|
|
87
|
+
```rust
|
|
88
|
+
use enact_memory::chunk_markdown;
|
|
89
|
+
|
|
90
|
+
let chunks = chunk_markdown(document_text, 512); // 512 tokens per chunk
|
|
91
|
+
|
|
92
|
+
for chunk in chunks {
|
|
93
|
+
println!("Chunk {}: {}", chunk.index, chunk.content);
|
|
94
|
+
if let Some(heading) = chunk.heading {
|
|
95
|
+
println!(" Under heading: {}", heading);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The chunker:
|
|
101
|
+
- Splits on markdown headings (`#`, `##`, `###`)
|
|
102
|
+
- Falls back to paragraph boundaries
|
|
103
|
+
- Then to line boundaries
|
|
104
|
+
- Preserves heading context in each chunk
|
|
105
|
+
|
|
106
|
+
### Hybrid Search (Vector + Keyword)
|
|
107
|
+
|
|
108
|
+
Combine semantic and keyword search for best results:
|
|
109
|
+
|
|
110
|
+
```rust
|
|
111
|
+
use enact_memory::hybrid_merge;
|
|
112
|
+
|
|
113
|
+
let vector_results = vec![("doc1".into(), 0.9), ("doc2".into(), 0.7)];
|
|
114
|
+
let keyword_results = vec![("doc2".into(), 10.0), ("doc3".into(), 5.0)];
|
|
115
|
+
|
|
116
|
+
let merged = hybrid_merge(
|
|
117
|
+
&vector_results,
|
|
118
|
+
&keyword_results,
|
|
119
|
+
0.7, // vector weight
|
|
120
|
+
0.3, // keyword weight
|
|
121
|
+
10 // limit
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for result in merged {
|
|
125
|
+
println!("{}: final_score={}", result.id, result.final_score);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Vector Operations
|
|
130
|
+
|
|
131
|
+
```rust
|
|
132
|
+
use enact_memory::{vec_to_bytes, bytes_to_vec};
|
|
133
|
+
|
|
134
|
+
// Serialize for storage
|
|
135
|
+
let bytes = vec_to_bytes(&embedding);
|
|
136
|
+
|
|
137
|
+
// Deserialize
|
|
138
|
+
let restored = bytes_to_vec(&bytes);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
# In agent config
|
|
145
|
+
memory:
|
|
146
|
+
backend: sqlite # sqlite, markdown, none
|
|
147
|
+
embeddings:
|
|
148
|
+
provider: openai
|
|
149
|
+
model: text-embedding-3-small
|
|
150
|
+
dimensions: 1536
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Session Scoping
|
|
154
|
+
|
|
155
|
+
Memories can be scoped to sessions for conversation context:
|
|
156
|
+
|
|
157
|
+
```rust
|
|
158
|
+
// Store with session
|
|
159
|
+
memory.store("topic", "discussing rust", MemoryCategory::Conversation, Some("session-123")).await?;
|
|
160
|
+
|
|
161
|
+
// Recall within session
|
|
162
|
+
let results = memory.recall("rust", 10, Some("session-123")).await?;
|
|
163
|
+
```
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
+++
|
|
2
|
+
title = "OAuth Authentication"
|
|
3
|
+
weight = 16
|
|
4
|
+
+++
|
|
5
|
+
|
|
6
|
+
# OAuth Authentication
|
|
7
|
+
|
|
8
|
+
The `enact-oauth` crate provides OAuth 2.0 authentication with device-code flow, enabling secure authentication with OpenAI and other providers without storing API keys in environment variables.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
OAuth support includes:
|
|
13
|
+
|
|
14
|
+
- **Device-code flow** - User-friendly authentication via browser
|
|
15
|
+
- **PKCE support** - Secure code exchange
|
|
16
|
+
- **Token refresh** - Automatic access token renewal
|
|
17
|
+
- **Local storage** - Secure token storage on disk
|
|
18
|
+
- **Account ID extraction** - JWT token parsing
|
|
19
|
+
|
|
20
|
+
## Supported Providers
|
|
21
|
+
|
|
22
|
+
| Provider | Flow | Status |
|
|
23
|
+
|----------|------|--------|
|
|
24
|
+
| OpenAI | Device-code | ✅ Complete |
|
|
25
|
+
| Anthropic | OAuth 2.0 | ✅ Supported via generic flow |
|
|
26
|
+
| Custom | OAuth 2.0 | ✅ Supported |
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
### Device-Code Flow (Recommended)
|
|
31
|
+
|
|
32
|
+
```rust
|
|
33
|
+
use enact_oauth::{start_device_code_flow, poll_device_code_tokens};
|
|
34
|
+
use reqwest::Client;
|
|
35
|
+
|
|
36
|
+
#[tokio::main]
|
|
37
|
+
async fn main() -> anyhow::Result<()> {
|
|
38
|
+
let client = Client::new();
|
|
39
|
+
|
|
40
|
+
// Step 1: Start device-code flow
|
|
41
|
+
let device = start_device_code_flow(&client).await?;
|
|
42
|
+
|
|
43
|
+
println!("Go to: {}", device.verification_uri);
|
|
44
|
+
println!("Enter code: {}", device.user_code);
|
|
45
|
+
println!("Expires in: {} seconds", device.expires_in);
|
|
46
|
+
|
|
47
|
+
// Step 2: Poll for tokens
|
|
48
|
+
// User must visit URL and enter code in their browser
|
|
49
|
+
let tokens = poll_device_code_tokens(&client, &device).await?;
|
|
50
|
+
|
|
51
|
+
println!("Access token: {}", tokens.access_token);
|
|
52
|
+
println!("Refresh token: {:?}", tokens.refresh_token);
|
|
53
|
+
println!("Expires at: {:?}", tokens.expires_at);
|
|
54
|
+
|
|
55
|
+
// Step 3: Save tokens securely
|
|
56
|
+
// See Token Storage section below
|
|
57
|
+
|
|
58
|
+
Ok(())
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### User Experience
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
$ enact login
|
|
66
|
+
|
|
67
|
+
To authenticate with OpenAI:
|
|
68
|
+
|
|
69
|
+
1. Visit: https://auth.openai.com/device
|
|
70
|
+
2. Enter code: ABCD-EFGH
|
|
71
|
+
|
|
72
|
+
Waiting for authentication...
|
|
73
|
+
✓ Successfully authenticated!
|
|
74
|
+
Account ID: user-abc123
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Token Types
|
|
78
|
+
|
|
79
|
+
### TokenSet
|
|
80
|
+
|
|
81
|
+
```rust
|
|
82
|
+
use enact_oauth::TokenSet;
|
|
83
|
+
|
|
84
|
+
let tokens = TokenSet {
|
|
85
|
+
access_token: "eyJhbG...".to_string(),
|
|
86
|
+
refresh_token: Some("dGhpcyBpcyBh...".to_string()),
|
|
87
|
+
id_token: Some("eyJhbG...".to_string()),
|
|
88
|
+
expires_at: Some(chrono::Utc::now() + chrono::Duration::hours(1)),
|
|
89
|
+
token_type: Some("Bearer".to_string()),
|
|
90
|
+
scope: Some("openid profile".to_string()),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Check if token is expiring soon
|
|
94
|
+
let expiring = tokens.is_expiring_within(Duration::from_secs(300));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Token Refresh
|
|
98
|
+
|
|
99
|
+
Automatically refresh access tokens:
|
|
100
|
+
|
|
101
|
+
```rust
|
|
102
|
+
use enact_oauth::refresh_access_token;
|
|
103
|
+
|
|
104
|
+
async fn get_valid_token(client: &Client) -> anyhow::Result<String> {
|
|
105
|
+
let tokens = load_stored_tokens()?; // Your storage function
|
|
106
|
+
|
|
107
|
+
// Check if token needs refresh
|
|
108
|
+
if tokens.is_expiring_within(Duration::from_secs(300)) {
|
|
109
|
+
if let Some(refresh_token) = &tokens.refresh_token {
|
|
110
|
+
let new_tokens = refresh_access_token(client, refresh_token).await?;
|
|
111
|
+
|
|
112
|
+
// Save new tokens
|
|
113
|
+
save_tokens(&new_tokens)?;
|
|
114
|
+
|
|
115
|
+
return Ok(new_tokens.access_token);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Ok(tokens.access_token)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Token Storage
|
|
124
|
+
|
|
125
|
+
### Local File Storage
|
|
126
|
+
|
|
127
|
+
```rust
|
|
128
|
+
use enact_oauth::TokenStorage;
|
|
129
|
+
use std::path::Path;
|
|
130
|
+
|
|
131
|
+
// Create storage
|
|
132
|
+
let storage = TokenStorage::new(Path::new("~/.config/enact/tokens"));
|
|
133
|
+
|
|
134
|
+
// Save tokens
|
|
135
|
+
storage.save_token(
|
|
136
|
+
"openai", // provider
|
|
137
|
+
"default", // profile name
|
|
138
|
+
&tokens
|
|
139
|
+
)?;
|
|
140
|
+
|
|
141
|
+
// Load tokens
|
|
142
|
+
if let Some(tokens) = storage.load_token("openai", "default")? {
|
|
143
|
+
println!("Access token: {}", tokens.access_token);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Secure Storage (Recommended)
|
|
148
|
+
|
|
149
|
+
```rust
|
|
150
|
+
use enact_oauth::TokenStorage;
|
|
151
|
+
use std::path::PathBuf;
|
|
152
|
+
|
|
153
|
+
fn get_token_dir() -> PathBuf {
|
|
154
|
+
dirs::config_dir()
|
|
155
|
+
.expect("Could not find config directory")
|
|
156
|
+
.join("enact")
|
|
157
|
+
.join("tokens")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let storage = TokenStorage::new(&get_token_dir());
|
|
161
|
+
|
|
162
|
+
// Tokens are stored as JSON files:
|
|
163
|
+
// ~/.config/enact/tokens/openai_default.json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## PKCE (Proof Key for Code Exchange)
|
|
167
|
+
|
|
168
|
+
PKCE adds security to OAuth flows:
|
|
169
|
+
|
|
170
|
+
```rust
|
|
171
|
+
use enact_oauth::{generate_pkce_state, build_authorize_url};
|
|
172
|
+
|
|
173
|
+
// Generate PKCE parameters
|
|
174
|
+
let pkce = generate_pkce_state();
|
|
175
|
+
|
|
176
|
+
println!("Code verifier: {}", pkce.code_verifier);
|
|
177
|
+
println!("Code challenge: {}", pkce.code_challenge);
|
|
178
|
+
println!("State: {}", pkce.state);
|
|
179
|
+
|
|
180
|
+
// Build authorization URL
|
|
181
|
+
let auth_url = build_authorize_url(&pkce);
|
|
182
|
+
println!("Open this URL: {}", auth_url);
|
|
183
|
+
|
|
184
|
+
// After user authorizes, exchange code for tokens
|
|
185
|
+
let tokens = exchange_code_for_tokens(&client, &code, &pkce
|
|
186
|
+
).await?;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Complete Example
|
|
190
|
+
|
|
191
|
+
```rust
|
|
192
|
+
use enact_oauth::*;
|
|
193
|
+
use reqwest::Client;
|
|
194
|
+
use std::path::Path;
|
|
195
|
+
|
|
196
|
+
#[tokio::main]
|
|
197
|
+
async fn main() -> anyhow::Result<()> {
|
|
198
|
+
let client = Client::new();
|
|
199
|
+
let storage = TokenStorage::new(Path::new("~/.config/enact/tokens"));
|
|
200
|
+
|
|
201
|
+
// Try to load existing token
|
|
202
|
+
if let Some(tokens) = storage.load_token("openai", "default")? {
|
|
203
|
+
if !tokens.is_expiring_within(Duration::from_secs(300)) {
|
|
204
|
+
println!("Using cached token");
|
|
205
|
+
return Ok(());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try to refresh
|
|
209
|
+
if let Some(refresh) = &tokens.refresh_token {
|
|
210
|
+
match refresh_access_token(&client, refresh).await {
|
|
211
|
+
Ok(new_tokens) => {
|
|
212
|
+
storage.save_token("openai", "default", &new_tokens)?;
|
|
213
|
+
println!("Token refreshed successfully");
|
|
214
|
+
return Ok(());
|
|
215
|
+
}
|
|
216
|
+
Err(e) => {
|
|
217
|
+
eprintln!("Refresh failed: {}, starting new auth flow", e);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Start new device-code flow
|
|
224
|
+
let device = start_device_code_flow(&client).await?;
|
|
225
|
+
|
|
226
|
+
println!("\n=== Authentication Required ===");
|
|
227
|
+
println!("Visit: {}", device.verification_uri);
|
|
228
|
+
println!("Code: {}", device.user_code);
|
|
229
|
+
println!("Expires in {} seconds\n", device.expires_in);
|
|
230
|
+
|
|
231
|
+
// Poll for completion
|
|
232
|
+
let tokens = poll_device_code_tokens(&client, &device).await?;
|
|
233
|
+
|
|
234
|
+
// Extract account info
|
|
235
|
+
if let Some(account_id) = extract_account_id_from_jwt(&tokens.access_token) {
|
|
236
|
+
println!("Authenticated as: {}", account_id);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Save tokens
|
|
240
|
+
storage.save_token("openai", "default", &tokens)?;
|
|
241
|
+
println!("Tokens saved successfully");
|
|
242
|
+
|
|
243
|
+
Ok(())
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## CLI Integration
|
|
248
|
+
|
|
249
|
+
```rust
|
|
250
|
+
// In your CLI
|
|
251
|
+
#[derive(Subcommand)]
|
|
252
|
+
enum Commands {
|
|
253
|
+
/// Authenticate with a provider
|
|
254
|
+
Login {
|
|
255
|
+
/// Provider name (openai, anthropic)
|
|
256
|
+
#[arg(default_value = "openai")]
|
|
257
|
+
provider: String,
|
|
258
|
+
|
|
259
|
+
/// Profile name
|
|
260
|
+
#[arg(default_value = "default")]
|
|
261
|
+
profile: String,
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
/// List authenticated profiles
|
|
265
|
+
Profiles,
|
|
266
|
+
|
|
267
|
+
/// Remove authentication
|
|
268
|
+
Logout {
|
|
269
|
+
provider: String,
|
|
270
|
+
profile: String,
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async fn login(provider: &str, profile: &str) -> anyhow::Result<()> {
|
|
275
|
+
let client = Client::new();
|
|
276
|
+
let storage = TokenStorage::new(&get_token_dir());
|
|
277
|
+
|
|
278
|
+
match provider {
|
|
279
|
+
"openai" => {
|
|
280
|
+
let device = start_device_code_flow(&client).await?;
|
|
281
|
+
|
|
282
|
+
println!("Go to: {}", device.verification_uri);
|
|
283
|
+
println!("Enter code: {}", device.user_code);
|
|
284
|
+
|
|
285
|
+
let tokens = poll_device_code_tokens(&client, &device).await?;
|
|
286
|
+
storage.save_token(provider, profile, &tokens)?;
|
|
287
|
+
|
|
288
|
+
println!("✓ Successfully authenticated with OpenAI");
|
|
289
|
+
}
|
|
290
|
+
_ => anyhow::bail!("Unsupported provider: {}", provider),
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
Ok(())
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Configuration
|
|
298
|
+
|
|
299
|
+
### Environment Variables
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Token storage directory
|
|
303
|
+
export ENACT_TOKEN_DIR="$HOME/.config/enact/tokens"
|
|
304
|
+
|
|
305
|
+
# Default provider
|
|
306
|
+
export ENACT_DEFAULT_PROVIDER="openai"
|
|
307
|
+
|
|
308
|
+
# Provider-specific settings
|
|
309
|
+
export ENACT_OPENAI_CLIENT_ID="app_EMoamEEZ73f0CkXaXp7hrann"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### YAML Configuration
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
# config.yaml
|
|
316
|
+
oauth:
|
|
317
|
+
default_provider: openai
|
|
318
|
+
|
|
319
|
+
providers:
|
|
320
|
+
openai:
|
|
321
|
+
client_id: "app_EMoamEEZ73f0CkXaXp7hrann"
|
|
322
|
+
authorize_url: "https://auth.openai.com/oauth/authorize"
|
|
323
|
+
token_url: "https://auth.openai.com/oauth/token"
|
|
324
|
+
device_code_url: "https://auth.openai.com/oauth/device/code"
|
|
325
|
+
|
|
326
|
+
anthropic:
|
|
327
|
+
client_id: "your-client-id"
|
|
328
|
+
authorize_url: "https://auth.anthropic.com/oauth/authorize"
|
|
329
|
+
token_url: "https://auth.anthropic.com/oauth/token"
|
|
330
|
+
|
|
331
|
+
storage:
|
|
332
|
+
type: file
|
|
333
|
+
path: "~/.config/enact/tokens"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Security Considerations
|
|
337
|
+
|
|
338
|
+
### Token Storage
|
|
339
|
+
|
|
340
|
+
- ✅ Tokens stored in user's home directory
|
|
341
|
+
- ✅ File permissions should be `600` (user read/write only)
|
|
342
|
+
- ✅ Never commit tokens to version control
|
|
343
|
+
- ✅ Use OS keychain when available (future enhancement)
|
|
344
|
+
|
|
345
|
+
```rust
|
|
346
|
+
// Set restrictive permissions (Unix)
|
|
347
|
+
#[cfg(unix)]
|
|
348
|
+
use std::os::unix::fs::PermissionsExt;
|
|
349
|
+
|
|
350
|
+
let mut perms = std::fs::metadata(&token_path)?.permissions();
|
|
351
|
+
perms.set_mode(0o600);
|
|
352
|
+
std::fs::set_permissions(&token_path, perms)?;
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Token Lifecycle
|
|
356
|
+
|
|
357
|
+
1. **Short-lived access tokens** (typically 1 hour)
|
|
358
|
+
2. **Long-lived refresh tokens** (can be revoked)
|
|
359
|
+
3. **Automatic refresh** before expiration
|
|
360
|
+
4. **Secure storage** with proper permissions
|
|
361
|
+
|
|
362
|
+
### Best Practices
|
|
363
|
+
|
|
364
|
+
```rust
|
|
365
|
+
// 1. Always check token expiration
|
|
366
|
+
if tokens.is_expiring_within(Duration::from_secs(300)) {
|
|
367
|
+
// Refresh token
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 2. Handle refresh failures gracefully
|
|
371
|
+
match refresh_access_token(&client, refresh_token).await {
|
|
372
|
+
Ok(new_tokens) => { /* use new tokens */ }
|
|
373
|
+
Err(_) => {
|
|
374
|
+
// Start new auth flow
|
|
375
|
+
initiate_device_code_flow().await?;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3. Never log tokens
|
|
380
|
+
// ❌ BAD
|
|
381
|
+
println!("Token: {}", tokens.access_token);
|
|
382
|
+
|
|
383
|
+
// ✅ GOOD
|
|
384
|
+
println!("Token obtained successfully");
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Testing
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
# Run OAuth tests
|
|
391
|
+
cargo test -p enact-oauth
|
|
392
|
+
|
|
393
|
+
# Run with output
|
|
394
|
+
cargo test -p enact-oauth -- --nocapture
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Mock Testing
|
|
398
|
+
|
|
399
|
+
```rust
|
|
400
|
+
#[cfg(test)]
|
|
401
|
+
mod tests {
|
|
402
|
+
use super::*;
|
|
403
|
+
|
|
404
|
+
#[test]
|
|
405
|
+
fn test_pkce_generation() {
|
|
406
|
+
let pkce = generate_pkce_state();
|
|
407
|
+
assert!(!pkce.code_verifier.is_empty());
|
|
408
|
+
assert!(!pkce.code_challenge.is_empty());
|
|
409
|
+
assert!(!pkce.state.is_empty());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
#[test]
|
|
413
|
+
fn test_parse_redirect_url() {
|
|
414
|
+
let code = parse_code_from_redirect(
|
|
415
|
+
"http://localhost:1455/auth/callback?code=abc123&state=xyz",
|
|
416
|
+
Some("xyz"),
|
|
417
|
+
).unwrap();
|
|
418
|
+
assert_eq!(code, "abc123");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
#[test]
|
|
422
|
+
fn test_extract_account_from_jwt() {
|
|
423
|
+
let jwt = "eyJhbG..."; // Valid JWT with account_id claim
|
|
424
|
+
let account = extract_account_id_from_jwt(jwt);
|
|
425
|
+
assert!(account.is_some());
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Troubleshooting
|
|
431
|
+
|
|
432
|
+
### "Device code expired"
|
|
433
|
+
|
|
434
|
+
The user didn't complete authentication in time:
|
|
435
|
+
|
|
436
|
+
```rust
|
|
437
|
+
match poll_device_code_tokens(&client, &device).await {
|
|
438
|
+
Ok(tokens) => { /* success */ }
|
|
439
|
+
Err(e) if e.to_string().contains("expired") => {
|
|
440
|
+
println!("Authentication timed out. Please try again.");
|
|
441
|
+
}
|
|
442
|
+
Err(e) => {
|
|
443
|
+
eprintln!("Authentication failed: {}", e);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### "Access denied"
|
|
449
|
+
|
|
450
|
+
User denied the authorization request:
|
|
451
|
+
|
|
452
|
+
```rust
|
|
453
|
+
Err(e) if e.to_string().contains("access_denied") => {
|
|
454
|
+
println!("Authorization was denied by user.");
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Token refresh failing
|
|
459
|
+
|
|
460
|
+
Refresh token may be revoked or expired:
|
|
461
|
+
|
|
462
|
+
```rust
|
|
463
|
+
match refresh_access_token(&client, refresh_token).await {
|
|
464
|
+
Ok(tokens) => { /* success */ }
|
|
465
|
+
Err(_) => {
|
|
466
|
+
// Clear stored tokens and re-authenticate
|
|
467
|
+
storage.remove_token("openai", "default")?;
|
|
468
|
+
println!("Please run 'enact login' again.");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Provider-Specific Notes
|
|
474
|
+
|
|
475
|
+
### OpenAI
|
|
476
|
+
|
|
477
|
+
- Client ID: `app_EMoamEEZ73f0CkXaXp7hrann` (fixed)
|
|
478
|
+
- Device-code flow: Supported ✅
|
|
479
|
+
- PKCE: Supported ✅
|
|
480
|
+
- Scopes: `openid profile email offline_access`
|
|
481
|
+
- Account ID: Extractable from JWT claims
|
|
482
|
+
|
|
483
|
+
### Anthropic
|
|
484
|
+
|
|
485
|
+
- Requires separate OAuth application registration
|
|
486
|
+
- Client ID provided after registration
|
|
487
|
+
- Same flow as OpenAI with different endpoints
|
|
488
|
+
|
|
489
|
+
## Comparison: OAuth vs API Keys
|
|
490
|
+
|
|
491
|
+
| Feature | OAuth | API Keys |
|
|
492
|
+
|---------|-------|----------|
|
|
493
|
+
| **Security** | ✅ Token rotation, limited scope | ⚠️ Single key, full access |
|
|
494
|
+
| **User Experience** | 🔐 Browser auth, no copy-paste | ⌨️ Manual key management |
|
|
495
|
+
| **Revocation** | ✅ Can revoke without changing code | ❌ Must regenerate and update |
|
|
496
|
+
| **Expiration** | ✅ Short-lived tokens | ❌ Never expires (risky) |
|
|
497
|
+
| **Setup Complexity** | Medium (one-time browser auth) | Low (copy-paste key) |
|
|
498
|
+
| **CI/CD** | ⚠️ Requires token storage | ✅ Simple env var |
|
|
499
|
+
|
|
500
|
+
**Recommendation**: Use OAuth for interactive usage, API keys for CI/CD.
|
|
501
|
+
|
|
502
|
+
## Future Enhancements
|
|
503
|
+
|
|
504
|
+
- [ ] OS keychain integration (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
|
505
|
+
- [ ] Browser-based OAuth flow (authorization code)
|
|
506
|
+
- [ ] Multiple profile support
|
|
507
|
+
- [ ] Token encryption at rest
|
|
508
|
+
- [ ] Automatic background refresh
|
|
509
|
+
- [ ] Provider discovery (OIDC)
|
|
510
|
+
|
|
511
|
+
## Resources
|
|
512
|
+
|
|
513
|
+
- [enact-oauth crate](/docs/crates/enact-oauth/)
|
|
514
|
+
- [OAuth 2.0 Device Authorization Grant (RFC 8628)](https://tools.ietf.org/html/rfc8628)
|
|
515
|
+
- [PKCE (RFC 7636)](https://tools.ietf.org/html/rfc7636)
|