diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d77be1aa..ee03e8c9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -54,7 +54,7 @@ jobs:
### Linux (x86_64)
```bash
- curl -LO https://github.com/Pama-Lee/Ordo/releases/download/${{ github.ref_name }}/ordo-x86_64-unknown-linux-gnu.tar.gz
+ curl -LO https://github.com/Ordo-Engine/Ordo/releases/download/${{ github.ref_name }}/ordo-x86_64-unknown-linux-gnu.tar.gz
tar -xzf ordo-x86_64-unknown-linux-gnu.tar.gz
chmod +x ordo-server
./ordo-server
@@ -62,7 +62,7 @@ jobs:
### macOS (Apple Silicon)
```bash
- curl -LO https://github.com/Pama-Lee/Ordo/releases/download/${{ github.ref_name }}/ordo-aarch64-apple-darwin.tar.gz
+ curl -LO https://github.com/Ordo-Engine/Ordo/releases/download/${{ github.ref_name }}/ordo-aarch64-apple-darwin.tar.gz
tar -xzf ordo-aarch64-apple-darwin.tar.gz
chmod +x ordo-server
./ordo-server
@@ -70,7 +70,7 @@ jobs:
### macOS (Intel)
```bash
- curl -LO https://github.com/Pama-Lee/Ordo/releases/download/${{ github.ref_name }}/ordo-x86_64-apple-darwin.tar.gz
+ curl -LO https://github.com/Ordo-Engine/Ordo/releases/download/${{ github.ref_name }}/ordo-x86_64-apple-darwin.tar.gz
tar -xzf ordo-x86_64-apple-darwin.tar.gz
chmod +x ordo-server
./ordo-server
@@ -220,4 +220,3 @@ jobs:
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
-
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f68def65..7660fe3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,233 +9,233 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Documentation
-- Add benchmark race GIF to README Performance section ([#42](https://github.com/Pama-Lee/Ordo/pull/42))
-([6b0c57b](https://github.com/Pama-Lee/Ordo/commit/6b0c57b23dbe9308ce5e6178dcc4d31cfbc17ee3))
-- Add benchmark visualization and competitive comparison data ([#40](https://github.com/Pama-Lee/Ordo/pull/40))
-([f5dba2e](https://github.com/Pama-Lee/Ordo/commit/f5dba2e026bfde43c157061ea156e6c38d5bb27c))
+- Add benchmark race GIF to README Performance section ([#42](https://github.com/Ordo-Engine/Ordo/pull/42))
+([6b0c57b](https://github.com/Ordo-Engine/Ordo/commit/6b0c57b23dbe9308ce5e6178dcc4d31cfbc17ee3))
+- Add benchmark visualization and competitive comparison data ([#40](https://github.com/Ordo-Engine/Ordo/pull/40))
+([f5dba2e](https://github.com/Ordo-Engine/Ordo/commit/f5dba2e026bfde43c157061ea156e6c38d5bb27c))
### Features
-- **ordo-core:** Add 23 extended built-in functions ([#52](https://github.com/Pama-Lee/Ordo/pull/52))
-([7860cfa](https://github.com/Pama-Lee/Ordo/commit/7860cfaefbfbcd30ee5f9086d6b98962b0ad83e2))
-- **ordo-core:** Add 30+ extended built-in functions ([#48](https://github.com/Pama-Lee/Ordo/pull/48))
-([b4adbbc](https://github.com/Pama-Lee/Ordo/commit/b4adbbc76375e480a10b7e3f8302f41f501ae6c7))
-- **ordo-server:** Add external reference data store ([#50](https://github.com/Pama-Lee/Ordo/pull/50))
-([d3f071f](https://github.com/Pama-Lee/Ordo/commit/d3f071f5731aea3d3ed1274da202c5a840fc5e6e))
-- **rule-composition:** Add CallRuleSet action and pipeline API ([#51](https://github.com/Pama-Lee/Ordo/pull/51))
-([47354c2](https://github.com/Pama-Lee/Ordo/commit/47354c21e5bfc434e7ba0542a0390531228c91b7))
-- **sdk:** Add Python SDK with HTTP/gRPC support ([#53](https://github.com/Pama-Lee/Ordo/pull/53))
-([3aafb56](https://github.com/Pama-Lee/Ordo/commit/3aafb56297a459153c4b2f9489cf4bd5e9f8a7f6))
-- **server:** Add store resource limits (max_rules_per_tenant, max_total_rules) ([#38](https://github.com/Pama-Lee/Ordo/pull/38))
-([bc7be7f](https://github.com/Pama-Lee/Ordo/commit/bc7be7fe7fd270b8858b25205eeb4e0dfd0d2750))
-- **server:** Add config validation, sync metrics, and instance ID improvement ([#36](https://github.com/Pama-Lee/Ordo/pull/36))
-([4725269](https://github.com/Pama-Lee/Ordo/commit/4725269e395c590a87b22898f4ed08d7e6b9c099))
-- **server:** Add NATS JetStream sync for distributed deployments ([#34](https://github.com/Pama-Lee/Ordo/pull/34))
-([d08d400](https://github.com/Pama-Lee/Ordo/commit/d08d40098d2c51806e1116fb1b3770bfa595bdc1))
+- **ordo-core:** Add 23 extended built-in functions ([#52](https://github.com/Ordo-Engine/Ordo/pull/52))
+([7860cfa](https://github.com/Ordo-Engine/Ordo/commit/7860cfaefbfbcd30ee5f9086d6b98962b0ad83e2))
+- **ordo-core:** Add 30+ extended built-in functions ([#48](https://github.com/Ordo-Engine/Ordo/pull/48))
+([b4adbbc](https://github.com/Ordo-Engine/Ordo/commit/b4adbbc76375e480a10b7e3f8302f41f501ae6c7))
+- **ordo-server:** Add external reference data store ([#50](https://github.com/Ordo-Engine/Ordo/pull/50))
+([d3f071f](https://github.com/Ordo-Engine/Ordo/commit/d3f071f5731aea3d3ed1274da202c5a840fc5e6e))
+- **rule-composition:** Add CallRuleSet action and pipeline API ([#51](https://github.com/Ordo-Engine/Ordo/pull/51))
+([47354c2](https://github.com/Ordo-Engine/Ordo/commit/47354c21e5bfc434e7ba0542a0390531228c91b7))
+- **sdk:** Add Python SDK with HTTP/gRPC support ([#53](https://github.com/Ordo-Engine/Ordo/pull/53))
+([3aafb56](https://github.com/Ordo-Engine/Ordo/commit/3aafb56297a459153c4b2f9489cf4bd5e9f8a7f6))
+- **server:** Add store resource limits (max_rules_per_tenant, max_total_rules) ([#38](https://github.com/Ordo-Engine/Ordo/pull/38))
+([bc7be7f](https://github.com/Ordo-Engine/Ordo/commit/bc7be7fe7fd270b8858b25205eeb4e0dfd0d2750))
+- **server:** Add config validation, sync metrics, and instance ID improvement ([#36](https://github.com/Ordo-Engine/Ordo/pull/36))
+([4725269](https://github.com/Ordo-Engine/Ordo/commit/4725269e395c590a87b22898f4ed08d7e6b9c099))
+- **server:** Add NATS JetStream sync for distributed deployments ([#34](https://github.com/Ordo-Engine/Ordo/pull/34))
+([d08d400](https://github.com/Ordo-Engine/Ordo/commit/d08d40098d2c51806e1116fb1b3770bfa595bdc1))
### Performance
-- **server:** Optimize HTTP serialization and reduce lock contention ([#43](https://github.com/Pama-Lee/Ordo/pull/43))
-([1e4415a](https://github.com/Pama-Lee/Ordo/commit/1e4415a29874d96c90809597af5e5f2c6e733b79))
+- **server:** Optimize HTTP serialization and reduce lock contention ([#43](https://github.com/Ordo-Engine/Ordo/pull/43))
+([1e4415a](https://github.com/Ordo-Engine/Ordo/commit/1e4415a29874d96c90809597af5e5f2c6e733b79))
## [0.3.0] - 2026-03-06
### Bug Fixes
- **ci:** Skip JIT benchmarks in GitHub Actions
-([54c9c06](https://github.com/Pama-Lee/Ordo/commit/54c9c0654d5eeef75146c3ffff19704a4b45c95f))
+([54c9c06](https://github.com/Ordo-Engine/Ordo/commit/54c9c0654d5eeef75146c3ffff19704a4b45c95f))
- **docs:** Use dynamic base path for language redirect
-([ad64f9c](https://github.com/Pama-Lee/Ordo/commit/ad64f9c2f7d55cde9b473b97c97d59153473ac1a))
+([ad64f9c](https://github.com/Ordo-Engine/Ordo/commit/ad64f9c2f7d55cde9b473b97c97d59153473ac1a))
- Improve error handling and add project skills
-([18e9832](https://github.com/Pama-Lee/Ordo/commit/18e98320e341bcbedd0e84a85571ac912dd00249))
+([18e9832](https://github.com/Ordo-Engine/Ordo/commit/18e98320e341bcbedd0e84a85571ac912dd00249))
- Make signature feature optional for WASM compatibility
-([ed676a5](https://github.com/Pama-Lee/Ordo/commit/ed676a560dd693b3851832182de1ecd4ca05793d))
+([ed676a5](https://github.com/Ordo-Engine/Ordo/commit/ed676a560dd693b3851832182de1ecd4ca05793d))
### Documentation
- Add integration guides for Nomad and Kubernetes
-([10ecea7](https://github.com/Pama-Lee/Ordo/commit/10ecea7b520b623480b7cfa73d2c43aad01cd622))
+([10ecea7](https://github.com/Ordo-Engine/Ordo/commit/10ecea7b520b623480b7cfa73d2c43aad01cd622))
- Update documentation for v0.2.0 release
-([39d1dfa](https://github.com/Pama-Lee/Ordo/commit/39d1dfa4bf2393684066b9adeee2435ca382cb7c))
+([39d1dfa](https://github.com/Ordo-Engine/Ordo/commit/39d1dfa4bf2393684066b9adeee2435ca382cb7c))
### Features
-- **grpc:** Add multi-tenancy support and batch execution ([#26](https://github.com/Pama-Lee/Ordo/pull/26))
-([445888d](https://github.com/Pama-Lee/Ordo/commit/445888df85e4da95798339043fc13dba85acebd1))
+- **grpc:** Add multi-tenancy support and batch execution ([#26](https://github.com/Ordo-Engine/Ordo/pull/26))
+([445888d](https://github.com/Ordo-Engine/Ordo/commit/445888df85e4da95798339043fc13dba85acebd1))
- **playground:** Add .ordo file import/export support
-([dcf2383](https://github.com/Pama-Lee/Ordo/commit/dcf238316555abe883ac4b595dde56ac5344f848))
-- **server:** Add Writer/Reader role deployment, file watcher, K8s health probes, and request limits ([#30](https://github.com/Pama-Lee/Ordo/pull/30))
-([1a6deef](https://github.com/Pama-Lee/Ordo/commit/1a6deef44b7aef6c9c1b4be223b28f36824afd23))
-- **server:** Graceful shutdown, panic recovery, and OpenTelemetry ([#27](https://github.com/Pama-Lee/Ordo/pull/27))
-([257878c](https://github.com/Pama-Lee/Ordo/commit/257878c326758b2ea4af560677878e52dd735e58))
+([dcf2383](https://github.com/Ordo-Engine/Ordo/commit/dcf238316555abe883ac4b595dde56ac5344f848))
+- **server:** Add Writer/Reader role deployment, file watcher, K8s health probes, and request limits ([#30](https://github.com/Ordo-Engine/Ordo/pull/30))
+([1a6deef](https://github.com/Ordo-Engine/Ordo/commit/1a6deef44b7aef6c9c1b4be223b28f36824afd23))
+- **server:** Graceful shutdown, panic recovery, and OpenTelemetry ([#27](https://github.com/Ordo-Engine/Ordo/pull/27))
+([257878c](https://github.com/Ordo-Engine/Ordo/commit/257878c326758b2ea4af560677878e52dd735e58))
- **server:** Add multi-tenancy support with namespace isolation
-([5b4cef7](https://github.com/Pama-Lee/Ordo/commit/5b4cef79197e855d6b33e08d0a31502256addc31))
-- Add expression input limits and HTTP API integration tests ([#28](https://github.com/Pama-Lee/Ordo/pull/28))
-([c5663be](https://github.com/Pama-Lee/Ordo/commit/c5663becf948b1411b76fb40cd1db2cf894ff19d))
+([5b4cef7](https://github.com/Ordo-Engine/Ordo/commit/5b4cef79197e855d6b33e08d0a31502256addc31))
+- Add expression input limits and HTTP API integration tests ([#28](https://github.com/Ordo-Engine/Ordo/pull/28))
+([c5663be](https://github.com/Ordo-Engine/Ordo/commit/c5663becf948b1411b76fb40cd1db2cf894ff19d))
- Add Ed25519 rule signature verification
-([83a13f7](https://github.com/Pama-Lee/Ordo/commit/83a13f788b3668a2d3598a5e6bc903d074d5c079))
+([83a13f7](https://github.com/Ordo-Engine/Ordo/commit/83a13f788b3668a2d3598a5e6bc903d074d5c079))
## [0.2.0] - 2026-01-18
### Bug Fixes
- **docs:** Add CUSTOM_DOMAIN env var to vercel.json
-([ecf6e37](https://github.com/Pama-Lee/Ordo/commit/ecf6e37d3d3ce7127936456bf593eac40c2ac491))
+([ecf6e37](https://github.com/Ordo-Engine/Ordo/commit/ecf6e37d3d3ce7127936456bf593eac40c2ac491))
- **docs:** Add vercel.json with correct output directory
-([f9e9657](https://github.com/Pama-Lee/Ordo/commit/f9e96575c84575902d46389eb8fc35bbb500d7da))
+([f9e9657](https://github.com/Ordo-Engine/Ordo/commit/f9e96575c84575902d46389eb8fc35bbb500d7da))
- **playground:** Use dynamic VERSION from editor-core
-([5b80f34](https://github.com/Pama-Lee/Ordo/commit/5b80f344e49df3f784e906af903f61dc7416c265))
+([5b80f34](https://github.com/Ordo-Engine/Ordo/commit/5b80f344e49df3f784e906af903f61dc7416c265))
- **wasm:** Make JIT feature optional for wasm32 target
-([8780814](https://github.com/Pama-Lee/Ordo/commit/87808145e075753f24943242c96f9f025fe29751))
+([8780814](https://github.com/Ordo-Engine/Ordo/commit/87808145e075753f24943242c96f9f025fe29751))
### Documentation
- Update README and add dual-domain deployment support
-([e204527](https://github.com/Pama-Lee/Ordo/commit/e20452764aefe8598a38aab95c736d35318bfa87))
+([e204527](https://github.com/Ordo-Engine/Ordo/commit/e20452764aefe8598a38aab95c736d35318bfa87))
- Fix dead links in quick-start.md
-([a83fecf](https://github.com/Pama-Lee/Ordo/commit/a83fecfae8d4b1f2f272e984893d8c27bd9f64c7))
+([a83fecf](https://github.com/Ordo-Engine/Ordo/commit/a83fecfae8d4b1f2f272e984893d8c27bd9f64c7))
### Features
- **expr:** Implement expression optimization techniques
-([04e2cf5](https://github.com/Pama-Lee/Ordo/commit/04e2cf5bc2e84868b7568e626af4130fc868e116))
+([04e2cf5](https://github.com/Ordo-Engine/Ordo/commit/04e2cf5bc2e84868b7568e626af4130fc868e116))
- **jit:** Implement schema-based JIT compilation system
-([d2d97f8](https://github.com/Pama-Lee/Ordo/commit/d2d97f8f07e690a9048bab1b8a7875211a606f21))
+([d2d97f8](https://github.com/Ordo-Engine/Ordo/commit/d2d97f8f07e690a9048bab1b8a7875211a606f21))
- **npm:** Setup npm publishing with changesets
-([18f1f1b](https://github.com/Pama-Lee/Ordo/commit/18f1f1bd2b2ecb7edf33c7b859384b713db855ac))
+([18f1f1b](https://github.com/Ordo-Engine/Ordo/commit/18f1f1bd2b2ecb7edf33c7b859384b713db855ac))
- Implement silent JIT compilation system
-([570211a](https://github.com/Pama-Lee/Ordo/commit/570211a36238cfefd68a0fceff610da85af95efc))
+([570211a](https://github.com/Ordo-Engine/Ordo/commit/570211a36238cfefd68a0fceff610da85af95efc))
- Add VM visualization debug system with ruleset debugging support
-([372aad9](https://github.com/Pama-Lee/Ordo/commit/372aad91484d45b4e02b00b7f71dbb1df62d81e1))
+([372aad9](https://github.com/Ordo-Engine/Ordo/commit/372aad91484d45b4e02b00b7f71dbb1df62d81e1))
- Add batch execution API for improved throughput
-([e54c6ef](https://github.com/Pama-Lee/Ordo/commit/e54c6ef966f9724d02c8d3efea9a8cdcedc54652))
+([e54c6ef](https://github.com/Ordo-Engine/Ordo/commit/e54c6ef966f9724d02c8d3efea9a8cdcedc54652))
- Add lightweight Prometheus metrics for better observability
-([da02a54](https://github.com/Pama-Lee/Ordo/commit/da02a54187a3da3b95d3ba0df52c999097f4911a))
+([da02a54](https://github.com/Ordo-Engine/Ordo/commit/da02a54187a3da3b95d3ba0df52c999097f4911a))
### Miscellaneous
- Bump version to 0.2.0
-([58b4086](https://github.com/Pama-Lee/Ordo/commit/58b40866e11bbf8c87dd1db8134a430b369433da))
+([58b4086](https://github.com/Ordo-Engine/Ordo/commit/58b40866e11bbf8c87dd1db8134a430b369433da))
- Remove benchmark results from git tracking
-([1d959ff](https://github.com/Pama-Lee/Ordo/commit/1d959ffce1e82bd073738c0e36fe27f043c7fc65))
+([1d959ff](https://github.com/Ordo-Engine/Ordo/commit/1d959ffce1e82bd073738c0e36fe27f043c7fc65))
### Refactor
- **release:** Simplify npm publishing workflow
-([8dbf510](https://github.com/Pama-Lee/Ordo/commit/8dbf5102e1db87c31408e736db4966f6a84bfe56))
+([8dbf510](https://github.com/Ordo-Engine/Ordo/commit/8dbf5102e1db87c31408e736db4966f6a84bfe56))
## [0.1.8] - 2026-01-14
### Features
- **docs:** Enhance documentation with multilingual support and new guides
-([17e9ab8](https://github.com/Pama-Lee/Ordo/commit/17e9ab81553b703874392a4d048b432d0199027b))
+([17e9ab8](https://github.com/Ordo-Engine/Ordo/commit/17e9ab81553b703874392a4d048b432d0199027b))
### Performance
- CPU efficiency optimizations for rule engine
-([6b39377](https://github.com/Pama-Lee/Ordo/commit/6b393776d1c28cd74c4f4489538dde1cf44a8a4a))
+([6b39377](https://github.com/Ordo-Engine/Ordo/commit/6b393776d1c28cd74c4f4489538dde1cf44a8a4a))
## [0.1.7] - 2026-01-12
### Features
- **core:** Implement MetricSink trait for custom rule metrics integration
-([f014700](https://github.com/Pama-Lee/Ordo/commit/f014700d30b11005087692c2318438cdd12a64c7))
+([f014700](https://github.com/Ordo-Engine/Ordo/commit/f014700d30b11005087692c2318438cdd12a64c7))
- Add favicons to Playground and Docs, add PostHog analytics to VitePress
-([3e21c5a](https://github.com/Pama-Lee/Ordo/commit/3e21c5abff075df2d416fba8d865206f9b9b9690))
+([3e21c5a](https://github.com/Ordo-Engine/Ordo/commit/3e21c5abff075df2d416fba8d865206f9b9b9690))
## [0.1.6] - 2026-01-12
### Bug Fixes
- **docs:** Include VitePress config.mts in git (was ignored)
-([5d53312](https://github.com/Pama-Lee/Ordo/commit/5d53312b62ec53ba1840adb4538cb181de4cfa18))
+([5d53312](https://github.com/Ordo-Engine/Ordo/commit/5d53312b62ec53ba1840adb4538cb181de4cfa18))
- **docs:** Wrap localhost URL in code backticks to avoid dead link error
-([cda6b8e](https://github.com/Pama-Lee/Ordo/commit/cda6b8e94a8d12aefd68bb295208b1d599249910))
+([cda6b8e](https://github.com/Ordo-Engine/Ordo/commit/cda6b8e94a8d12aefd68bb295208b1d599249910))
- **docs:** Correct formatting of PromQL examples in metrics documentation
-([f4ee5ea](https://github.com/Pama-Lee/Ordo/commit/f4ee5ea51ad6f47fa891568b733dde40e0d7ee85))
+([f4ee5ea](https://github.com/Ordo-Engine/Ordo/commit/f4ee5ea51ad6f47fa891568b733dde40e0d7ee85))
- Resolve clippy warnings and enhance pre-commit hook with clippy check
-([a7ccf2e](https://github.com/Pama-Lee/Ordo/commit/a7ccf2e3f50589e08f9e4853a66d5cf36664ec17))
+([a7ccf2e](https://github.com/Ordo-Engine/Ordo/commit/a7ccf2e3f50589e08f9e4853a66d5cf36664ec17))
- Add WASM stub for playground build without Rust
-([49b1171](https://github.com/Pama-Lee/Ordo/commit/49b1171a92f9a4f40a7d340ed941b94262bd3407))
+([49b1171](https://github.com/Ordo-Engine/Ordo/commit/49b1171a92f9a4f40a7d340ed941b94262bd3407))
### Documentation
- Update README with visual editor screenshots
-([629feb2](https://github.com/Pama-Lee/Ordo/commit/629feb2f9f389dcdf43308a21fe269d46c7a785d))
+([629feb2](https://github.com/Ordo-Engine/Ordo/commit/629feb2f9f389dcdf43308a21fe269d46c7a785d))
### Features
- **docs:** Add comprehensive documentation for Ordo rule engine
-([8c6bedd](https://github.com/Pama-Lee/Ordo/commit/8c6bedd8ec78e2eba72ea9b39beb481c0fda92ae))
+([8c6bedd](https://github.com/Ordo-Engine/Ordo/commit/8c6bedd8ec78e2eba72ea9b39beb481c0fda92ae))
- **playground:** Add PostHog analytics integration
-([8e79612](https://github.com/Pama-Lee/Ordo/commit/8e796129d509efa8440ee559e2f70dd8884df4cd))
+([8e79612](https://github.com/Ordo-Engine/Ordo/commit/8e796129d509efa8440ee559e2f70dd8884df4cd))
- **server:** Add structured audit logging with dynamic sample rate
-([ef651d5](https://github.com/Pama-Lee/Ordo/commit/ef651d5a0d77cac6365396ba13be8d7e39f17d05))
+([ef651d5](https://github.com/Ordo-Engine/Ordo/commit/ef651d5a0d77cac6365396ba13be8d7e39f17d05))
- **server:** Add rule versioning with rollback support
-([ec29570](https://github.com/Pama-Lee/Ordo/commit/ec29570aed40e613396389c1dcd3ee7883dbbab9))
+([ec29570](https://github.com/Ordo-Engine/Ordo/commit/ec29570aed40e613396389c1dcd3ee7883dbbab9))
- Add Prometheus metrics and enhanced health check endpoint
-([d92db86](https://github.com/Pama-Lee/Ordo/commit/d92db86e1d945834568a5f6b8a5378b74a4aa1c1))
+([d92db86](https://github.com/Ordo-Engine/Ordo/commit/d92db86e1d945834568a5f6b8a5378b74a4aa1c1))
- Implement file-based rule persistence in Ordo server
-([c64d0f5](https://github.com/Pama-Lee/Ordo/commit/c64d0f55fec57687d94843ab372c82f679f58461))
+([c64d0f5](https://github.com/Ordo-Engine/Ordo/commit/c64d0f55fec57687d94843ab372c82f679f58461))
- Build WASM in CI for GitHub Pages deployment
-([d971573](https://github.com/Pama-Lee/Ordo/commit/d971573e3337bbbe80865c58bc4b2824576d8fba))
+([d971573](https://github.com/Ordo-Engine/Ordo/commit/d971573e3337bbbe80865c58bc4b2824576d8fba))
- Add GitHub Pages deployment for playground
-([65c2043](https://github.com/Pama-Lee/Ordo/commit/65c2043f1bb2072cb60980cea62aab7cd1de8f34))
+([65c2043](https://github.com/Ordo-Engine/Ordo/commit/65c2043f1bb2072cb60980cea62aab7cd1de8f34))
- Add GitHub Pages deployment for playground
-([5aa77d4](https://github.com/Pama-Lee/Ordo/commit/5aa77d480b37a74cd19f3be31fdc69a5e9151905))
+([5aa77d4](https://github.com/Ordo-Engine/Ordo/commit/5aa77d480b37a74cd19f3be31fdc69a5e9151905))
### Miscellaneous
- Add driver.js dependency to pnpm-lock.yaml
-([7054f98](https://github.com/Pama-Lee/Ordo/commit/7054f98a709553745d36eef18d777ccf0af017ae))
+([7054f98](https://github.com/Ordo-Engine/Ordo/commit/7054f98a709553745d36eef18d777ccf0af017ae))
- Update dependency installation command in deploy-playground workflow
-([cc70797](https://github.com/Pama-Lee/Ordo/commit/cc707978f465cc0114ffd766d1a8429f0e59da1d))
+([cc70797](https://github.com/Ordo-Engine/Ordo/commit/cc707978f465cc0114ffd766d1a8429f0e59da1d))
- Update Dockerfile to include protoc and curl installation
-([61f77c1](https://github.com/Pama-Lee/Ordo/commit/61f77c137b88a9af1a5ad77d1410c654776f62f2))
+([61f77c1](https://github.com/Ordo-Engine/Ordo/commit/61f77c137b88a9af1a5ad77d1410c654776f62f2))
- Remove obsolete and engine integration summaries
-([848f899](https://github.com/Pama-Lee/Ordo/commit/848f899cd5326662c7c3ad00daa0e47b910dae53))
+([848f899](https://github.com/Ordo-Engine/Ordo/commit/848f899cd5326662c7c3ad00daa0e47b910dae53))
### Style
- Apply cargo fmt and add pre-commit hook for auto-formatting
-([9b0b306](https://github.com/Pama-Lee/Ordo/commit/9b0b30658d61967fb4cbe09c469b2f94e38fc797))
+([9b0b306](https://github.com/Ordo-Engine/Ordo/commit/9b0b30658d61967fb4cbe09c469b2f94e38fc797))
## [0.1.0] - 2026-01-07
### Bug Fixes
- Use std::slice::from_ref instead of clone in slice
-([b3ef2a9](https://github.com/Pama-Lee/Ordo/commit/b3ef2a959c7dfd8eead141e62b11ddbcc9298f9b))
+([b3ef2a9](https://github.com/Ordo-Engine/Ordo/commit/b3ef2a959c7dfd8eead141e62b11ddbcc9298f9b))
- Resolve all clippy warnings and formatting issues
-([cfa2cd5](https://github.com/Pama-Lee/Ordo/commit/cfa2cd595281d06ab8f266ddc5d157a1b9e5192c))
+([cfa2cd5](https://github.com/Ordo-Engine/Ordo/commit/cfa2cd595281d06ab8f266ddc5d157a1b9e5192c))
- Correct rust-toolchain action name in CI workflows
-([09912cd](https://github.com/Pama-Lee/Ordo/commit/09912cd38c6e47242da0c4b6057f611377fcc7a3))
+([09912cd](https://github.com/Ordo-Engine/Ordo/commit/09912cd38c6e47242da0c4b6057f611377fcc7a3))
- Correct rust-toolchain action name in CI workflows
-([d4d269e](https://github.com/Pama-Lee/Ordo/commit/d4d269e9ce42add976bae0a096918e7f7dc064e6))
+([d4d269e](https://github.com/Ordo-Engine/Ordo/commit/d4d269e9ce42add976bae0a096918e7f7dc064e6))
- Correct git clone URL in README
-([97522b6](https://github.com/Pama-Lee/Ordo/commit/97522b6c12b2183623b843c18c190dae022a2c64))
+([97522b6](https://github.com/Ordo-Engine/Ordo/commit/97522b6c12b2183623b843c18c190dae022a2c64))
### Documentation
- Add branch strategy to README
-([fe35eb5](https://github.com/Pama-Lee/Ordo/commit/fe35eb5bb120321d9af3aaa53207acd98b58a5fb))
+([fe35eb5](https://github.com/Ordo-Engine/Ordo/commit/fe35eb5bb120321d9af3aaa53207acd98b58a5fb))
### Features
- Add GitHub Actions CI/CD and Docker support
-([7c60909](https://github.com/Pama-Lee/Ordo/commit/7c60909866585140d0b20c7770515587dc991bf1))
+([7c60909](https://github.com/Ordo-Engine/Ordo/commit/7c60909866585140d0b20c7770515587dc991bf1))
### Style
- Apply rustfmt formatting to all source files
-([e0232ff](https://github.com/Pama-Lee/Ordo/commit/e0232ff212068c9c4cbb4ffd8e49878bb523e239))
-
-[Unreleased]: https://github.com/Pama-Lee/Ordo/compare/v0.3.0...HEAD
-[0.3.0]: https://github.com/Pama-Lee/Ordo/compare/v0.2.0...v0.3.0
-[0.2.0]: https://github.com/Pama-Lee/Ordo/compare/v0.1.8...v0.2.0
-[0.1.8]: https://github.com/Pama-Lee/Ordo/compare/v0.1.7...v0.1.8
-[0.1.7]: https://github.com/Pama-Lee/Ordo/compare/v0.1.6...v0.1.7
-[0.1.6]: https://github.com/Pama-Lee/Ordo/compare/v0.1.0...v0.1.6
+([e0232ff](https://github.com/Ordo-Engine/Ordo/commit/e0232ff212068c9c4cbb4ffd8e49878bb523e239))
+
+[Unreleased]: https://github.com/Ordo-Engine/Ordo/compare/v0.3.0...HEAD
+[0.3.0]: https://github.com/Ordo-Engine/Ordo/compare/v0.2.0...v0.3.0
+[0.2.0]: https://github.com/Ordo-Engine/Ordo/compare/v0.1.8...v0.2.0
+[0.1.8]: https://github.com/Ordo-Engine/Ordo/compare/v0.1.7...v0.1.8
+[0.1.7]: https://github.com/Ordo-Engine/Ordo/compare/v0.1.6...v0.1.7
+[0.1.6]: https://github.com/Ordo-Engine/Ordo/compare/v0.1.0...v0.1.6
diff --git a/Cargo.lock b/Cargo.lock
index 0a55630f..c54e03ce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -380,6 +380,14 @@ dependencies = [
"serde",
]
+[[package]]
+name = "capability-demo"
+version = "0.1.0"
+dependencies = [
+ "ordo-core",
+ "serde_json",
+]
+
[[package]]
name = "cast"
version = "0.3.0"
@@ -2380,6 +2388,7 @@ dependencies = [
"clap",
"dashmap",
"futures",
+ "hashbrown 0.14.5",
"hex",
"hmac",
"hostname",
diff --git a/Cargo.toml b/Cargo.toml
index 54b521c9..9adc4530 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,6 +8,7 @@ members = [
"crates/ordo-server",
"crates/ordo-platform",
"crates/ordo-wasm",
+ "examples/capability-demo",
]
[workspace.package]
diff --git a/README.md b/README.md
index 6b0f80c3..4bbb8eb4 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -53,7 +53,7 @@ docker compose up # platform + engine + studio
# open http://localhost:5173
```
-Or use the hosted **[Live Playground](https://pama-lee.github.io/Ordo/)** — no install needed.
+Or use the hosted **[Live Playground](https://ordo-engine.github.io/Ordo/)** — no install needed.
### Engine only
@@ -140,7 +140,7 @@ ordo/
| **Govern** | v0.8 | Change requests, impact analysis, approval flows |
| **Ordo Cloud** | v1.0 | Managed platform with hosted engine |
-Full roadmap → [docs/roadmap](https://pama-lee.github.io/Ordo/docs/en/roadmap)
+Full roadmap → [docs/roadmap](https://ordo-engine.github.io/Ordo/docs/en/roadmap)
---
@@ -148,4 +148,4 @@ Full roadmap → [docs/roadmap](https://pama-lee.github.io/Ordo/docs/en/roadmap)
MIT — see [LICENSE](LICENSE).
-
Built with Rust · Discord · Docs
+Built with Rust · Discord · Docs
diff --git a/ROADMAP.md b/ROADMAP.md
index 4628f661..9f728df4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,8 +2,8 @@
For the full roadmap with details on each milestone, visit our documentation site:
-- **English**: [Roadmap](https://pama-lee.github.io/Ordo/docs/en/roadmap)
-- **中文**: [产品路线图](https://pama-lee.github.io/Ordo/docs/zh/roadmap)
+- **English**: [Roadmap](https://ordo-engine.github.io/Ordo/docs/en/roadmap)
+- **中文**: [产品路线图](https://ordo-engine.github.io/Ordo/docs/zh/roadmap)
## Quick Overview
@@ -20,4 +20,4 @@ Milestones 1–5 are fully open source (MIT). Ordo Cloud adds managed hosting an
---
-Have feedback on priorities? [Open an issue](https://github.com/Pama-Lee/Ordo/issues) or join our [Discord](https://discord.gg/Y529FkArhh).
+Have feedback on priorities? [Open an issue](https://github.com/Ordo-Engine/Ordo/issues) or join our [Discord](https://discord.gg/Y529FkArhh).
diff --git a/cliff.toml b/cliff.toml
index 525edea9..e7fbcdee 100644
--- a/cliff.toml
+++ b/cliff.toml
@@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
"""
body = """
{%- macro remote_url() -%}
- https://github.com/Pama-Lee/Ordo
+ https://github.com/Ordo-Engine/Ordo
{%- endmacro -%}
{% if version -%}
@@ -41,7 +41,7 @@ body = """
"""
footer = """
{%- macro remote_url() -%}
- https://github.com/Pama-Lee/Ordo
+ https://github.com/Ordo-Engine/Ordo
{%- endmacro -%}
{% for release in releases -%}
@@ -64,7 +64,7 @@ split_commits = false
commit_preprocessors = [
# Link PR numbers to GitHub
- { pattern = '\(#(\d+)\)', replace = '([#${1}](https://github.com/Pama-Lee/Ordo/pull/${1}))' },
+ { pattern = '\(#(\d+)\)', replace = '([#${1}](https://github.com/Ordo-Engine/Ordo/pull/${1}))' },
]
# Strip commit body to keep changelog concise — only the first line matters
diff --git a/crates/ordo-core/src/capability/mod.rs b/crates/ordo-core/src/capability/mod.rs
new file mode 100644
index 00000000..8898a762
--- /dev/null
+++ b/crates/ordo-core/src/capability/mod.rs
@@ -0,0 +1,72 @@
+mod provider;
+mod registry;
+
+pub use provider::{
+ CapabilityCategory, CapabilityConfig, CapabilityDescriptor, CapabilityInvoker,
+ CapabilityProvider, CapabilityRequest, CapabilityResponse, CircuitBreakerConfig, RetryPolicy,
+};
+pub use registry::CapabilityRegistry;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::prelude::{Action, ActionKind, Expr, RuleExecutor, RuleSet, Step, TerminalResult};
+ use crate::{context::Value, error::Result};
+ use std::sync::Arc;
+
+ struct EchoProvider;
+
+ impl CapabilityProvider for EchoProvider {
+ fn descriptor(&self) -> CapabilityDescriptor {
+ CapabilityDescriptor::new("demo.echo", CapabilityCategory::Compute)
+ .with_description("Echo payloads back to the caller")
+ }
+
+ fn invoke(&self, request: &CapabilityRequest) -> Result {
+ Ok(CapabilityResponse::new(request.payload.clone()))
+ }
+ }
+
+ #[test]
+ fn executor_external_call_routes_through_capability_registry() {
+ let registry = Arc::new(CapabilityRegistry::new());
+ registry.register(Arc::new(EchoProvider));
+
+ let mut ruleset = RuleSet::new("capability_demo", "call_echo");
+ ruleset.add_step(Step::action(
+ "call_echo",
+ "Call echo",
+ vec![Action {
+ kind: ActionKind::ExternalCall {
+ service: "demo.echo".to_string(),
+ method: "echo".to_string(),
+ params: vec![("amount".to_string(), Expr::field("amount"))],
+ timeout_ms: 250,
+ result_variable: Some("capability_result".to_string()),
+ },
+ description: String::new(),
+ }],
+ "done",
+ ));
+ ruleset.add_step(Step::terminal(
+ "done",
+ "Done",
+ TerminalResult::new("OK").with_output(
+ "echoed_amount",
+ Expr::field("$capability_result.payload.amount"),
+ ),
+ ));
+
+ let mut executor = RuleExecutor::new();
+ executor.set_capability_invoker(registry);
+
+ let input: Value = serde_json::from_str(r#"{"amount": 42}"#).unwrap();
+ let result = executor.execute(&ruleset, input).unwrap();
+
+ let amount = result
+ .output
+ .get_path("echoed_amount")
+ .expect("echoed amount missing");
+ assert_eq!(amount, &Value::int(42));
+ }
+}
diff --git a/crates/ordo-core/src/capability/provider.rs b/crates/ordo-core/src/capability/provider.rs
new file mode 100644
index 00000000..b8ac130a
--- /dev/null
+++ b/crates/ordo-core/src/capability/provider.rs
@@ -0,0 +1,231 @@
+use crate::context::Value;
+use crate::error::Result;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+/// High-level execution tier for a capability.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum CapabilityCategory {
+ Network,
+ Compute,
+ Action,
+}
+
+/// Retry policy for a capability.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RetryPolicy {
+ pub max_attempts: u32,
+ pub backoff_ms: u64,
+}
+
+impl RetryPolicy {
+ #[inline]
+ pub fn disabled() -> Self {
+ Self {
+ max_attempts: 1,
+ backoff_ms: 0,
+ }
+ }
+}
+
+impl Default for RetryPolicy {
+ fn default() -> Self {
+ Self::disabled()
+ }
+}
+
+/// Consecutive-failure breaker for a capability.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CircuitBreakerConfig {
+ pub failure_threshold: u32,
+ pub reset_timeout_ms: u64,
+}
+
+impl CircuitBreakerConfig {
+ #[inline]
+ pub fn disabled() -> Self {
+ Self {
+ failure_threshold: 0,
+ reset_timeout_ms: 0,
+ }
+ }
+
+ #[inline]
+ pub fn enabled(&self) -> bool {
+ self.failure_threshold > 0
+ }
+}
+
+impl Default for CircuitBreakerConfig {
+ fn default() -> Self {
+ Self::disabled()
+ }
+}
+
+/// Runtime policy for a registered capability.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CapabilityConfig {
+ pub category: CapabilityCategory,
+ pub timeout_ms: Option,
+ pub retry: RetryPolicy,
+ pub circuit_breaker: CircuitBreakerConfig,
+}
+
+impl CapabilityConfig {
+ #[inline]
+ pub fn new(category: CapabilityCategory) -> Self {
+ Self {
+ category,
+ timeout_ms: None,
+ retry: RetryPolicy::default(),
+ circuit_breaker: CircuitBreakerConfig::default(),
+ }
+ }
+
+ #[inline]
+ pub fn timeout(mut self, timeout_ms: u64) -> Self {
+ self.timeout_ms = Some(timeout_ms);
+ self
+ }
+
+ #[inline]
+ pub fn retry(mut self, retry: RetryPolicy) -> Self {
+ self.retry = retry;
+ self
+ }
+
+ #[inline]
+ pub fn circuit_breaker(mut self, circuit_breaker: CircuitBreakerConfig) -> Self {
+ self.circuit_breaker = circuit_breaker;
+ self
+ }
+}
+
+/// Metadata for a registered capability.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct CapabilityDescriptor {
+ pub name: String,
+ pub description: String,
+ pub config: CapabilityConfig,
+}
+
+impl CapabilityDescriptor {
+ #[inline]
+ pub fn new(name: impl Into, category: CapabilityCategory) -> Self {
+ Self {
+ name: name.into(),
+ description: String::new(),
+ config: CapabilityConfig::new(category),
+ }
+ }
+
+ #[inline]
+ pub fn with_description(mut self, description: impl Into) -> Self {
+ self.description = description.into();
+ self
+ }
+
+ #[inline]
+ pub fn with_config(mut self, config: CapabilityConfig) -> Self {
+ self.config = config;
+ self
+ }
+}
+
+/// Normalized request shape passed to capability providers.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct CapabilityRequest {
+ pub capability: String,
+ pub operation: String,
+ #[serde(default)]
+ pub payload: Value,
+ #[serde(default)]
+ pub metadata: HashMap,
+ #[serde(default)]
+ pub timeout_ms: Option,
+ #[serde(default)]
+ pub category: Option,
+}
+
+impl CapabilityRequest {
+ #[inline]
+ pub fn new(
+ capability: impl Into,
+ operation: impl Into,
+ payload: Value,
+ ) -> Self {
+ Self {
+ capability: capability.into(),
+ operation: operation.into(),
+ payload,
+ metadata: HashMap::new(),
+ timeout_ms: None,
+ category: None,
+ }
+ }
+
+ #[inline]
+ pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
+ self.timeout_ms = Some(timeout_ms);
+ self
+ }
+
+ #[inline]
+ pub fn with_category(mut self, category: CapabilityCategory) -> Self {
+ self.category = Some(category);
+ self
+ }
+
+ #[inline]
+ pub fn with_metadata(mut self, key: impl Into, value: impl Into) -> Self {
+ self.metadata.insert(key.into(), value.into());
+ self
+ }
+}
+
+/// Normalized response shape returned by capability providers.
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct CapabilityResponse {
+ #[serde(default)]
+ pub payload: Value,
+ #[serde(default)]
+ pub metadata: HashMap,
+}
+
+impl CapabilityResponse {
+ #[inline]
+ pub fn new(payload: Value) -> Self {
+ Self {
+ payload,
+ metadata: HashMap::new(),
+ }
+ }
+
+ #[inline]
+ pub fn empty() -> Self {
+ Self::new(Value::Null)
+ }
+
+ #[inline]
+ pub fn with_metadata(mut self, key: impl Into, value: impl Into) -> Self {
+ self.metadata.insert(key.into(), value.into());
+ self
+ }
+}
+
+/// Provider implementation for a named capability.
+pub trait CapabilityProvider: Send + Sync {
+ fn descriptor(&self) -> CapabilityDescriptor;
+ fn invoke(&self, request: &CapabilityRequest) -> Result;
+}
+
+/// Trait used by the rule executor to call a capability registry.
+pub trait CapabilityInvoker: Send + Sync {
+ fn invoke(&self, request: &CapabilityRequest) -> Result;
+
+ fn describe(&self, capability: &str) -> Option {
+ let _ = capability;
+ None
+ }
+}
diff --git a/crates/ordo-core/src/capability/registry.rs b/crates/ordo-core/src/capability/registry.rs
new file mode 100644
index 00000000..4755140f
--- /dev/null
+++ b/crates/ordo-core/src/capability/registry.rs
@@ -0,0 +1,313 @@
+use super::provider::{
+ CapabilityDescriptor, CapabilityInvoker, CapabilityProvider, CapabilityRequest,
+ CapabilityResponse,
+};
+use crate::error::OrdoError;
+use crate::error::Result;
+use parking_lot::{Mutex, RwLock};
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::time::{Duration, Instant};
+
+#[derive(Debug, Default)]
+struct CircuitState {
+ consecutive_failures: u32,
+ opened_at: Option,
+}
+
+struct RegisteredCapability {
+ descriptor: CapabilityDescriptor,
+ provider: Arc,
+ state: Mutex,
+}
+
+/// In-memory capability registry used by the executor and tests.
+#[derive(Default)]
+pub struct CapabilityRegistry {
+ providers: RwLock>>,
+}
+
+impl CapabilityRegistry {
+ #[inline]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn register(
+ &self,
+ provider: Arc,
+ ) -> Option> {
+ let descriptor = provider.descriptor();
+ let name = descriptor.name.clone();
+ let registered = Arc::new(RegisteredCapability {
+ descriptor,
+ provider,
+ state: Mutex::new(CircuitState::default()),
+ });
+
+ self.providers
+ .write()
+ .insert(name, registered)
+ .map(|previous| previous.provider.clone())
+ }
+
+ #[inline]
+ pub fn contains(&self, capability: &str) -> bool {
+ self.providers.read().contains_key(capability)
+ }
+
+ pub fn list(&self) -> Vec {
+ self.providers
+ .read()
+ .values()
+ .map(|entry| entry.descriptor.clone())
+ .collect()
+ }
+
+ fn lookup(&self, capability: &str) -> Option> {
+ self.providers.read().get(capability).cloned()
+ }
+
+ fn ensure_circuit_closed(entry: &RegisteredCapability) -> Result<()> {
+ let config = &entry.descriptor.config.circuit_breaker;
+ if !config.enabled() {
+ return Ok(());
+ }
+
+ let mut state = entry.state.lock();
+ if let Some(opened_at) = state.opened_at {
+ let elapsed = opened_at.elapsed();
+ if elapsed >= Duration::from_millis(config.reset_timeout_ms) {
+ state.consecutive_failures = 0;
+ state.opened_at = None;
+ return Ok(());
+ }
+
+ let remaining = config
+ .reset_timeout_ms
+ .saturating_sub(elapsed.as_millis().min(u128::from(u64::MAX)) as u64);
+ return Err(OrdoError::CircuitOpen {
+ capability: entry.descriptor.name.clone(),
+ retry_after_ms: Some(remaining),
+ });
+ }
+
+ Ok(())
+ }
+
+ fn mark_success(entry: &RegisteredCapability) {
+ let mut state = entry.state.lock();
+ state.consecutive_failures = 0;
+ state.opened_at = None;
+ }
+
+ fn mark_failure(entry: &RegisteredCapability) {
+ let config = &entry.descriptor.config.circuit_breaker;
+ if !config.enabled() {
+ return;
+ }
+
+ let mut state = entry.state.lock();
+ state.consecutive_failures = state.consecutive_failures.saturating_add(1);
+ if state.consecutive_failures >= config.failure_threshold {
+ state.opened_at = Some(Instant::now());
+ }
+ }
+
+ fn is_retryable_error(error: &OrdoError) -> bool {
+ matches!(
+ error,
+ OrdoError::Timeout { .. } | OrdoError::CapabilityInvocation { .. }
+ )
+ }
+}
+
+impl CapabilityInvoker for CapabilityRegistry {
+ fn invoke(&self, request: &CapabilityRequest) -> Result {
+ let entry =
+ self.lookup(&request.capability)
+ .ok_or_else(|| OrdoError::CapabilityNotFound {
+ capability: request.capability.clone(),
+ })?;
+
+ Self::ensure_circuit_closed(&entry)?;
+
+ let attempts = entry.descriptor.config.retry.max_attempts.max(1);
+ let timeout_ms = request.timeout_ms.or(entry.descriptor.config.timeout_ms);
+
+ for attempt in 0..attempts {
+ let start = Instant::now();
+ let response = entry.provider.invoke(request);
+ let response = match (timeout_ms, response) {
+ (Some(limit), Ok(_)) if start.elapsed().as_millis() as u64 > limit => {
+ Err(OrdoError::Timeout { timeout_ms: limit })
+ }
+ (_, other) => other,
+ };
+
+ match response {
+ Ok(response) => {
+ Self::mark_success(&entry);
+ return Ok(response);
+ }
+ Err(error) => {
+ Self::mark_failure(&entry);
+ let should_retry = attempt + 1 < attempts && Self::is_retryable_error(&error);
+ if should_retry {
+ #[cfg(not(target_arch = "wasm32"))]
+ if entry.descriptor.config.retry.backoff_ms > 0 {
+ std::thread::sleep(Duration::from_millis(
+ entry.descriptor.config.retry.backoff_ms,
+ ));
+ }
+ continue;
+ }
+ return Err(error);
+ }
+ }
+ }
+
+ Err(OrdoError::internal_error_static(
+ "capability registry retry loop exited unexpectedly",
+ ))
+ }
+
+ fn describe(&self, capability: &str) -> Option {
+ self.lookup(capability)
+ .map(|entry| entry.descriptor.clone())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::capability::{
+ CapabilityCategory, CapabilityConfig, CapabilityDescriptor, CircuitBreakerConfig,
+ RetryPolicy,
+ };
+ use crate::context::Value;
+ use std::collections::VecDeque;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ struct SequenceProvider {
+ descriptor: CapabilityDescriptor,
+ calls: AtomicUsize,
+ responses: Mutex>>,
+ }
+
+ impl SequenceProvider {
+ fn new(
+ descriptor: CapabilityDescriptor,
+ responses: Vec>,
+ ) -> Self {
+ Self {
+ descriptor,
+ calls: AtomicUsize::new(0),
+ responses: Mutex::new(responses.into()),
+ }
+ }
+ }
+
+ impl CapabilityProvider for SequenceProvider {
+ fn descriptor(&self) -> CapabilityDescriptor {
+ self.descriptor.clone()
+ }
+
+ fn invoke(&self, _request: &CapabilityRequest) -> Result {
+ self.calls.fetch_add(1, Ordering::Relaxed);
+ self.responses
+ .lock()
+ .pop_front()
+ .unwrap_or_else(|| Ok(CapabilityResponse::new(Value::string("default"))))
+ }
+ }
+
+ #[test]
+ fn registers_and_invokes_provider() {
+ let registry = CapabilityRegistry::new();
+ registry.register(Arc::new(SequenceProvider::new(
+ CapabilityDescriptor::new("demo.echo", CapabilityCategory::Compute),
+ vec![Ok(CapabilityResponse::new(Value::string("ok")))],
+ )));
+
+ let response = registry
+ .invoke(&CapabilityRequest::new(
+ "demo.echo",
+ "run",
+ Value::object(std::collections::HashMap::new()),
+ ))
+ .unwrap();
+
+ assert_eq!(response.payload, Value::string("ok"));
+ assert!(registry.contains("demo.echo"));
+ assert_eq!(registry.list().len(), 1);
+ }
+
+ #[test]
+ fn retries_transient_errors() {
+ let registry = CapabilityRegistry::new();
+ let descriptor = CapabilityDescriptor::new("demo.retry", CapabilityCategory::Network)
+ .with_config(
+ CapabilityConfig::new(CapabilityCategory::Network).retry(RetryPolicy {
+ max_attempts: 2,
+ backoff_ms: 0,
+ }),
+ );
+ let provider = Arc::new(SequenceProvider::new(
+ descriptor,
+ vec![
+ Err(OrdoError::CapabilityInvocation {
+ capability: "demo.retry".to_string(),
+ message: "temporary".into(),
+ }),
+ Ok(CapabilityResponse::new(Value::string("recovered"))),
+ ],
+ ));
+ let provider_ref = provider.clone();
+ registry.register(provider);
+
+ let response = registry
+ .invoke(&CapabilityRequest::new("demo.retry", "fetch", Value::Null))
+ .unwrap();
+
+ assert_eq!(response.payload, Value::string("recovered"));
+ assert_eq!(provider_ref.calls.load(Ordering::Relaxed), 2);
+ }
+
+ #[test]
+ fn opens_circuit_after_repeated_failures() {
+ let registry = CapabilityRegistry::new();
+ registry.register(Arc::new(SequenceProvider::new(
+ CapabilityDescriptor::new("demo.breaker", CapabilityCategory::Action).with_config(
+ CapabilityConfig::new(CapabilityCategory::Action).circuit_breaker(
+ CircuitBreakerConfig {
+ failure_threshold: 2,
+ reset_timeout_ms: 1000,
+ },
+ ),
+ ),
+ vec![
+ Err(OrdoError::CapabilityInvocation {
+ capability: "demo.breaker".to_string(),
+ message: "boom".into(),
+ }),
+ Err(OrdoError::CapabilityInvocation {
+ capability: "demo.breaker".to_string(),
+ message: "boom".into(),
+ }),
+ ],
+ )));
+
+ assert!(registry
+ .invoke(&CapabilityRequest::new("demo.breaker", "emit", Value::Null))
+ .is_err());
+ assert!(registry
+ .invoke(&CapabilityRequest::new("demo.breaker", "emit", Value::Null))
+ .is_err());
+
+ let error = registry
+ .invoke(&CapabilityRequest::new("demo.breaker", "emit", Value::Null))
+ .unwrap_err();
+ assert!(matches!(error, OrdoError::CircuitOpen { .. }));
+ }
+}
diff --git a/crates/ordo-core/src/context/store.rs b/crates/ordo-core/src/context/store.rs
index 0ff6ef04..1f8c5342 100644
--- a/crates/ordo-core/src/context/store.rs
+++ b/crates/ordo-core/src/context/store.rs
@@ -61,8 +61,12 @@ impl Context {
/// - `_index`: get the current iteration index (if set)
pub fn get(&self, path: &str) -> Option<&Value> {
if let Some(var_name) = path.strip_prefix('$') {
- // Variable reference
- self.variables.get(var_name)
+ // Variable reference, with optional nested path access.
+ if let Some((name, nested)) = var_name.split_once('.') {
+ self.variables.get(name)?.get_path(nested)
+ } else {
+ self.variables.get(var_name)
+ }
} else if let Some(item_path) = path.strip_prefix("item.") {
// Current iteration item field
self.current_item.as_ref()?.get_path(item_path)
@@ -178,6 +182,28 @@ mod tests {
assert_eq!(ctx.get("$score"), None);
}
+ #[test]
+ fn test_context_nested_variable_paths() {
+ let mut ctx = Context::new(Value::Null);
+ ctx.set_variable(
+ "result",
+ Value::object({
+ let mut m = std::collections::HashMap::new();
+ m.insert(
+ "payload".to_string(),
+ Value::object({
+ let mut nested = std::collections::HashMap::new();
+ nested.insert("score".to_string(), Value::int(7));
+ nested
+ }),
+ );
+ m
+ }),
+ );
+
+ assert_eq!(ctx.get("$result.payload.score"), Some(&Value::int(7)));
+ }
+
#[test]
fn test_context_item() {
let mut ctx = Context::new(Value::Null);
diff --git a/crates/ordo-core/src/error.rs b/crates/ordo-core/src/error.rs
index 134aceac..8cd3de57 100644
--- a/crates/ordo-core/src/error.rs
+++ b/crates/ordo-core/src/error.rs
@@ -54,6 +54,24 @@ pub enum OrdoError {
#[error("RuleSet not found: {name}")]
RuleSetNotFound { name: String },
+ /// Capability not found
+ #[error("Capability not found: {capability}")]
+ CapabilityNotFound { capability: String },
+
+ /// Capability circuit breaker is open
+ #[error("Capability circuit open: {capability}")]
+ CircuitOpen {
+ capability: String,
+ retry_after_ms: Option,
+ },
+
+ /// Capability invocation failed
+ #[error("Capability invocation failed for {capability}: {message}")]
+ CapabilityInvocation {
+ capability: String,
+ message: Cow<'static, str>,
+ },
+
/// Step not found
#[error("Step not found: {step_id}")]
StepNotFound { step_id: String },
@@ -171,6 +189,17 @@ impl OrdoError {
}
}
+ /// Create a capability invocation error
+ pub fn capability_invocation(
+ capability: impl Into,
+ message: impl Into>,
+ ) -> Self {
+ Self::CapabilityInvocation {
+ capability: capability.into(),
+ message: message.into(),
+ }
+ }
+
/// Create an internal error from a static string (no allocation)
#[inline]
pub fn internal_error_static(message: &'static str) -> Self {
diff --git a/crates/ordo-core/src/lib.rs b/crates/ordo-core/src/lib.rs
index 2c5a243e..ef59ae15 100644
--- a/crates/ordo-core/src/lib.rs
+++ b/crates/ordo-core/src/lib.rs
@@ -50,6 +50,7 @@
#![allow(missing_docs)]
#![warn(clippy::all)]
+pub mod capability;
pub mod context;
pub mod error;
pub mod expr;
@@ -62,6 +63,11 @@ pub mod trace;
/// Prelude module for convenient imports
pub mod prelude {
+ pub use crate::capability::{
+ CapabilityCategory, CapabilityConfig, CapabilityDescriptor, CapabilityInvoker,
+ CapabilityProvider, CapabilityRegistry, CapabilityRequest, CapabilityResponse,
+ CircuitBreakerConfig, RetryPolicy,
+ };
pub use crate::context::{Context, Value};
pub use crate::error::{OrdoError, Result};
pub use crate::expr::{
diff --git a/crates/ordo-core/src/rule/compiled_executor.rs b/crates/ordo-core/src/rule/compiled_executor.rs
index d823cdfc..e5afba22 100644
--- a/crates/ordo-core/src/rule/compiled_executor.rs
+++ b/crates/ordo-core/src/rule/compiled_executor.rs
@@ -5,6 +5,7 @@ use super::compiled::{
};
use super::metrics::{MetricSink, NoOpMetricSink};
use super::{ExecutionResult, TerminalResult};
+use crate::capability::{CapabilityInvoker, CapabilityRequest};
use crate::context::{Context, IString, Value};
use crate::error::{OrdoError, Result};
use crate::expr::BytecodeVM;
@@ -36,6 +37,7 @@ use wasm_time::Instant;
pub struct CompiledRuleExecutor {
vm: BytecodeVM,
metric_sink: Arc,
+ capability_invoker: Option>,
}
impl Default for CompiledRuleExecutor {
@@ -49,6 +51,7 @@ impl CompiledRuleExecutor {
Self {
vm: BytecodeVM::new(),
metric_sink: Arc::new(NoOpMetricSink),
+ capability_invoker: None,
}
}
@@ -56,9 +59,18 @@ impl CompiledRuleExecutor {
Self {
vm: BytecodeVM::new(),
metric_sink,
+ capability_invoker: None,
}
}
+ pub fn set_capability_invoker(&mut self, capability_invoker: Arc) {
+ self.capability_invoker = Some(capability_invoker);
+ }
+
+ pub fn capability_invoker(&self) -> Option> {
+ self.capability_invoker.clone()
+ }
+
pub fn execute(&self, ruleset: &CompiledRuleSet, input: Value) -> Result {
let start_time = Instant::now();
let mut ctx = Context::new(input);
@@ -234,12 +246,38 @@ impl CompiledRuleExecutor {
))
})
.collect::>>()?;
- self.metric_sink.record_gauge(name, metric_value, &tags);
+ self.record_metric(name, metric_value, &tags)?;
}
}
Ok(())
}
+ fn record_metric(&self, name: &str, value: f64, tags: &[(String, String)]) -> Result<()> {
+ if let Some(capability_invoker) = &self.capability_invoker {
+ let mut tag_values = std::collections::HashMap::with_capacity(tags.len());
+ for (key, value) in tags {
+ tag_values.insert(key.clone(), Value::string(value));
+ }
+
+ let mut payload = std::collections::HashMap::with_capacity(3);
+ payload.insert("name".to_string(), Value::string(name));
+ payload.insert("value".to_string(), Value::float(value));
+ payload.insert("tags".to_string(), Value::object(tag_values));
+
+ let request =
+ CapabilityRequest::new("metrics.prometheus", "gauge", Value::object(payload));
+
+ match capability_invoker.invoke(&request) {
+ Ok(_) => return Ok(()),
+ Err(OrdoError::CapabilityNotFound { .. }) => {}
+ Err(error) => return Err(error),
+ }
+ }
+
+ self.metric_sink.record_gauge(name, value, tags);
+ Ok(())
+ }
+
fn build_output(
&self,
ruleset: &CompiledRuleSet,
@@ -273,3 +311,82 @@ impl CompiledRuleExecutor {
Ok(Value::object_optimized(output))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::capability::{
+ CapabilityCategory, CapabilityDescriptor, CapabilityProvider, CapabilityRegistry,
+ CapabilityResponse,
+ };
+ use crate::expr::Expr;
+ use crate::rule::metrics::MetricSink;
+ use crate::rule::{Action, ActionKind, RuleSet, RuleSetCompiler, Step, TerminalResult};
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ struct TestMetricSink {
+ gauge_calls: AtomicUsize,
+ }
+
+ impl MetricSink for TestMetricSink {
+ fn record_gauge(&self, _name: &str, _value: f64, _tags: &[(String, String)]) {
+ self.gauge_calls.fetch_add(1, Ordering::SeqCst);
+ }
+
+ fn record_counter(&self, _name: &str, _value: f64, _tags: &[(String, String)]) {}
+ }
+
+ struct TestMetricCapability {
+ calls: AtomicUsize,
+ }
+
+ impl CapabilityProvider for TestMetricCapability {
+ fn descriptor(&self) -> CapabilityDescriptor {
+ CapabilityDescriptor::new("metrics.prometheus", CapabilityCategory::Action)
+ }
+
+ fn invoke(&self, _request: &CapabilityRequest) -> Result {
+ self.calls.fetch_add(1, Ordering::SeqCst);
+ Ok(CapabilityResponse::empty())
+ }
+ }
+
+ #[test]
+ fn compiled_executor_prefers_capability_metrics_when_available() {
+ let mut ruleset = RuleSet::new("compiled_metric_test", "record_metric");
+ ruleset.add_step(Step::action(
+ "record_metric",
+ "Record Metric",
+ vec![Action {
+ kind: ActionKind::Metric {
+ name: "compiled_metric".to_string(),
+ value: Expr::literal(9.0f64),
+ tags: vec![("env".to_string(), "test".to_string())],
+ },
+ description: String::new(),
+ }],
+ "done",
+ ));
+ ruleset.add_step(Step::terminal("done", "Done", TerminalResult::new("OK")));
+ let compiled = RuleSetCompiler::compile(&ruleset).unwrap();
+
+ let sink = Arc::new(TestMetricSink {
+ gauge_calls: AtomicUsize::new(0),
+ });
+ let mut executor = CompiledRuleExecutor::with_metric_sink(sink.clone());
+ let registry = Arc::new(CapabilityRegistry::new());
+ let capability = Arc::new(TestMetricCapability {
+ calls: AtomicUsize::new(0),
+ });
+ let capability_ref = capability.clone();
+ registry.register(capability);
+ executor.set_capability_invoker(registry);
+
+ let input = serde_json::from_str(r#"{}"#).unwrap();
+ let result = executor.execute(&compiled, input).unwrap();
+
+ assert_eq!(result.code, "OK");
+ assert_eq!(capability_ref.calls.load(Ordering::SeqCst), 1);
+ assert_eq!(sink.gauge_calls.load(Ordering::SeqCst), 0);
+ }
+}
diff --git a/crates/ordo-core/src/rule/executor.rs b/crates/ordo-core/src/rule/executor.rs
index e7c3dde5..4815b9c4 100644
--- a/crates/ordo-core/src/rule/executor.rs
+++ b/crates/ordo-core/src/rule/executor.rs
@@ -5,6 +5,7 @@
use super::metrics::{MetricSink, NoOpMetricSink};
use super::model::{FieldMissingBehavior, RuleSet};
use super::step::{ActionKind, Condition, LogLevel, Step, StepKind, TerminalResult};
+use crate::capability::{CapabilityInvoker, CapabilityRequest};
use crate::context::{Context, Value};
use crate::error::{OrdoError, Result};
use crate::expr::{Evaluator, ExprParser};
@@ -90,6 +91,8 @@ pub struct RuleExecutor {
metric_sink: Arc,
/// Optional resolver for CallRuleSet actions
resolver: Option>,
+ /// Optional capability invoker for ExternalCall actions
+ capability_invoker: Option>,
/// Maximum nesting depth for CallRuleSet (prevents unbounded recursion)
max_call_depth: usize,
}
@@ -101,6 +104,9 @@ impl Default for RuleExecutor {
}
impl RuleExecutor {
+ const METRIC_CAPABILITY: &'static str = "metrics.prometheus";
+ const METRIC_OPERATION_GAUGE: &'static str = "gauge";
+
/// Create a new executor
pub fn new() -> Self {
Self {
@@ -108,6 +114,7 @@ impl RuleExecutor {
trace_config: TraceConfig::default(),
metric_sink: Arc::new(NoOpMetricSink),
resolver: None,
+ capability_invoker: None,
max_call_depth: 10,
}
}
@@ -119,6 +126,7 @@ impl RuleExecutor {
trace_config,
metric_sink: Arc::new(NoOpMetricSink),
resolver: None,
+ capability_invoker: None,
max_call_depth: 10,
}
}
@@ -130,6 +138,7 @@ impl RuleExecutor {
trace_config: TraceConfig::default(),
metric_sink,
resolver: None,
+ capability_invoker: None,
max_call_depth: 10,
}
}
@@ -144,6 +153,7 @@ impl RuleExecutor {
trace_config,
metric_sink,
resolver: None,
+ capability_invoker: None,
max_call_depth: 10,
}
}
@@ -153,6 +163,16 @@ impl RuleExecutor {
self.resolver = Some(resolver);
}
+ /// Set an invoker for ExternalCall actions
+ pub fn set_capability_invoker(&mut self, capability_invoker: Arc) {
+ self.capability_invoker = Some(capability_invoker);
+ }
+
+ /// Get the configured capability invoker
+ pub fn capability_invoker(&self) -> Option> {
+ self.capability_invoker.clone()
+ }
+
/// Get the metric sink
pub fn metric_sink(&self) -> &Arc {
&self.metric_sink
@@ -544,8 +564,7 @@ impl RuleExecutor {
return Ok(());
}
};
- // Record metric via sink
- self.metric_sink.record_gauge(name, metric_value, tags);
+ self.record_metric(name, metric_value, tags)?;
tracing::debug!(metric = %name, value = %metric_value, tags = ?tags, "Metric recorded");
}
@@ -599,11 +618,78 @@ impl RuleExecutor {
ctx.set_variable(result_variable, result_obj);
}
- ActionKind::ExternalCall { .. } => {
- // TODO: Implement external calls
- tracing::warn!("External calls not yet implemented");
+ ActionKind::ExternalCall {
+ service,
+ method,
+ params,
+ result_variable,
+ timeout_ms,
+ } => {
+ let capability_invoker = self.capability_invoker.as_ref().ok_or_else(|| {
+ OrdoError::eval_error("ExternalCall requires a capability invoker")
+ })?;
+
+ let mut payload = std::collections::HashMap::with_capacity(params.len());
+ for (name, expr) in params {
+ payload.insert(name.clone(), self.evaluator.eval(expr, ctx)?);
+ }
+
+ let mut request =
+ CapabilityRequest::new(service.clone(), method.clone(), Value::object(payload));
+ if *timeout_ms > 0 {
+ request = request.with_timeout(*timeout_ms);
+ }
+
+ let response = capability_invoker.invoke(&request)?;
+ if let Some(result_variable) = result_variable {
+ let response_obj = Value::object({
+ let mut m = std::collections::HashMap::new();
+ m.insert("capability".to_string(), Value::string(service));
+ m.insert("operation".to_string(), Value::string(method));
+ m.insert("payload".to_string(), response.payload);
+ let metadata = Value::object(
+ response
+ .metadata
+ .into_iter()
+ .map(|(key, value)| (key, Value::string(value)))
+ .collect(),
+ );
+ m.insert("metadata".to_string(), metadata);
+ m
+ });
+ ctx.set_variable(result_variable, response_obj);
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn record_metric(&self, name: &str, value: f64, tags: &[(String, String)]) -> Result<()> {
+ if let Some(capability_invoker) = &self.capability_invoker {
+ let mut tag_values = std::collections::HashMap::with_capacity(tags.len());
+ for (key, value) in tags {
+ tag_values.insert(key.clone(), Value::string(value));
+ }
+
+ let mut payload = std::collections::HashMap::with_capacity(3);
+ payload.insert("name".to_string(), Value::string(name));
+ payload.insert("value".to_string(), Value::float(value));
+ payload.insert("tags".to_string(), Value::object(tag_values));
+
+ let request = CapabilityRequest::new(
+ Self::METRIC_CAPABILITY,
+ Self::METRIC_OPERATION_GAUGE,
+ Value::object(payload),
+ );
+
+ match capability_invoker.invoke(&request) {
+ Ok(_) => return Ok(()),
+ Err(OrdoError::CapabilityNotFound { .. }) => {}
+ Err(error) => return Err(error),
}
}
+
+ self.metric_sink.record_gauge(name, value, tags);
Ok(())
}
@@ -867,6 +953,79 @@ mod tests {
assert_eq!(sink.gauge_calls.load(Ordering::SeqCst), 1);
}
+ #[test]
+ fn test_execute_metric_via_capability_invoker() {
+ use crate::capability::{
+ CapabilityCategory, CapabilityDescriptor, CapabilityProvider, CapabilityRegistry,
+ CapabilityRequest, CapabilityResponse,
+ };
+ use crate::rule::metrics::MetricSink;
+ use crate::rule::step::{Action, ActionKind};
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ struct TestMetricSink {
+ gauge_calls: AtomicUsize,
+ }
+
+ impl MetricSink for TestMetricSink {
+ fn record_gauge(&self, _name: &str, _value: f64, _tags: &[(String, String)]) {
+ self.gauge_calls.fetch_add(1, Ordering::SeqCst);
+ }
+
+ fn record_counter(&self, _name: &str, _value: f64, _tags: &[(String, String)]) {}
+ }
+
+ struct TestMetricCapability {
+ calls: AtomicUsize,
+ }
+
+ impl CapabilityProvider for TestMetricCapability {
+ fn descriptor(&self) -> CapabilityDescriptor {
+ CapabilityDescriptor::new("metrics.prometheus", CapabilityCategory::Action)
+ }
+
+ fn invoke(&self, _request: &CapabilityRequest) -> Result {
+ self.calls.fetch_add(1, Ordering::SeqCst);
+ Ok(CapabilityResponse::empty())
+ }
+ }
+
+ let sink = Arc::new(TestMetricSink {
+ gauge_calls: AtomicUsize::new(0),
+ });
+ let mut executor = RuleExecutor::with_metric_sink(sink.clone());
+ let registry = Arc::new(CapabilityRegistry::new());
+ let capability = Arc::new(TestMetricCapability {
+ calls: AtomicUsize::new(0),
+ });
+ let capability_ref = capability.clone();
+ registry.register(capability);
+ executor.set_capability_invoker(registry);
+
+ let mut ruleset = RuleSet::new("metric_capability_test", "record_metric");
+ ruleset.add_step(Step::action(
+ "record_metric",
+ "Record Metric",
+ vec![Action {
+ kind: ActionKind::Metric {
+ name: "cap_metric".to_string(),
+ value: Expr::literal(7.0f64),
+ tags: vec![("env".to_string(), "test".to_string())],
+ },
+ description: String::new(),
+ }],
+ "done",
+ ));
+ ruleset.add_step(Step::terminal("done", "Done", TerminalResult::new("OK")));
+
+ let input = serde_json::from_str(r#"{}"#).unwrap();
+ let result = executor.execute(&ruleset, input).unwrap();
+
+ assert_eq!(result.code, "OK");
+ assert_eq!(capability_ref.calls.load(Ordering::SeqCst), 1);
+ assert_eq!(sink.gauge_calls.load(Ordering::SeqCst), 0);
+ }
+
#[test]
fn test_execute_batch_sequential() {
let ruleset = create_test_ruleset();
diff --git a/crates/ordo-core/src/rule/step.rs b/crates/ordo-core/src/rule/step.rs
index 6b88b2b9..023a2502 100644
--- a/crates/ordo-core/src/rule/step.rs
+++ b/crates/ordo-core/src/rule/step.rs
@@ -332,11 +332,13 @@ pub enum ActionKind {
},
/// External call (future)
- #[serde(skip)]
ExternalCall {
service: String,
method: String,
params: Vec<(String, Expr)>,
+ #[serde(default)]
+ result_variable: Option,
+ #[serde(default)]
timeout_ms: u64,
},
}
diff --git a/crates/ordo-proto/proto/ordo.proto b/crates/ordo-proto/proto/ordo.proto
index 1203ccbd..14877814 100644
--- a/crates/ordo-proto/proto/ordo.proto
+++ b/crates/ordo-proto/proto/ordo.proto
@@ -4,7 +4,7 @@ package ordo.v1;
option java_package = "com.ordo.proto.v1";
option java_multiple_files = true;
-option go_package = "github.com/pama-lee/ordo/proto/v1";
+option go_package = "github.com/Ordo-Engine/Ordo/proto/v1";
// =============================================================================
// Ordo Rule Engine Service
@@ -303,4 +303,3 @@ message HealthResponse {
// Server uptime in seconds
uint64 uptime_seconds = 4;
}
-
diff --git a/crates/ordo-server/Cargo.toml b/crates/ordo-server/Cargo.toml
index 5fd06dcf..ba75f99b 100644
--- a/crates/ordo-server/Cargo.toml
+++ b/crates/ordo-server/Cargo.toml
@@ -14,6 +14,7 @@ once_cell = "1.19"
ordo-core = { version = "0.4.2", path = "../ordo-core" }
ordo-proto = { version = "0.4.2", path = "../ordo-proto" }
parking_lot.workspace = true
+hashbrown.workspace = true
prost.workspace = true
rand = "0.8"
rayon.workspace = true
diff --git a/crates/ordo-server/src/api.rs b/crates/ordo-server/src/api.rs
index 8486b306..ac6f4d7c 100644
--- a/crates/ordo-server/src/api.rs
+++ b/crates/ordo-server/src/api.rs
@@ -14,6 +14,7 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
+use crate::capability_registry::emit_rule_execution_audit;
use crate::error::ApiError;
use crate::json::SimdJson;
use crate::metrics;
@@ -486,9 +487,14 @@ pub async fn execute_ruleset(
// Log audit event (with sampling)
let source_ip = connect_info.map(|ci| ci.0.ip().to_string());
let rule_id = format!("{}/{}", tenant.id, name);
- state
- .audit_logger
- .log_execution(&rule_id, result.duration_us, &result.code, source_ip);
+ emit_rule_execution_audit(
+ state.executor.capability_invoker(),
+ &state.audit_logger,
+ &rule_id,
+ result.duration_us,
+ &result.code,
+ source_ip,
+ );
result
}
@@ -504,7 +510,9 @@ pub async fn execute_ruleset(
// Log audit event for errors (with sampling)
let source_ip = connect_info.map(|ci| ci.0.ip().to_string());
let rule_id = format!("{}/{}", tenant.id, name);
- state.audit_logger.log_execution(
+ emit_rule_execution_audit(
+ state.executor.capability_invoker(),
+ &state.audit_logger,
&rule_id,
start.elapsed().as_micros() as u64,
"error",
@@ -1437,6 +1445,9 @@ pub async fn execute_pipeline(
let mut pipeline_executor =
RuleExecutor::with_trace_and_metrics(TraceConfig::minimal(), state.metric_sink.clone());
pipeline_executor.set_resolver(Arc::new(snapshot));
+ if let Some(capability_invoker) = state.executor.capability_invoker() {
+ pipeline_executor.set_capability_invoker(capability_invoker);
+ }
let mut current_input = request.input;
let mut stages = Vec::with_capacity(resolved.len());
diff --git a/crates/ordo-server/src/api_integration_tests.rs b/crates/ordo-server/src/api_integration_tests.rs
index d9b3ed36..00d34c6c 100644
--- a/crates/ordo-server/src/api_integration_tests.rs
+++ b/crates/ordo-server/src/api_integration_tests.rs
@@ -27,6 +27,7 @@ use tower_http::trace::TraceLayer;
use crate::{
api,
audit::AuditLogger,
+ capability_registry::build_server_executor,
debug::DebugSessionManager,
metrics::PrometheusMetricSink,
middleware,
@@ -36,14 +37,13 @@ use crate::{
tenant::{TenantDefaults, TenantManager},
AppState, ServerConfig,
};
-use ordo_core::prelude::RuleExecutor;
/// Build a full test app with all API routes and middleware (matching main.rs router).
async fn build_full_test_app() -> Router {
let store = Arc::new(RwLock::new(RuleStore::new()));
- let executor = Arc::new(RuleExecutor::new());
let metric_sink = Arc::new(PrometheusMetricSink::new());
let audit_logger = Arc::new(AuditLogger::new(None, 10));
+ let executor = build_server_executor(metric_sink.clone(), Some(audit_logger.clone()));
let debug_sessions = Arc::new(DebugSessionManager::new());
let defaults = TenantDefaults {
default_qps_limit: None,
diff --git a/crates/ordo-server/src/api_tests.rs b/crates/ordo-server/src/api_tests.rs
index 0b1bf4ac..7e9d6fc2 100644
--- a/crates/ordo-server/src/api_tests.rs
+++ b/crates/ordo-server/src/api_tests.rs
@@ -16,6 +16,7 @@ use tower_http::trace::TraceLayer;
use crate::{
api,
audit::AuditLogger,
+ capability_registry::build_server_executor,
debug::DebugSessionManager,
metrics::PrometheusMetricSink,
middleware,
@@ -25,13 +26,12 @@ use crate::{
tenant::{TenantDefaults, TenantManager},
AppState, ServerConfig,
};
-use ordo_core::prelude::RuleExecutor;
async fn build_test_app() -> Router {
let store = Arc::new(RwLock::new(RuleStore::new()));
- let executor = Arc::new(RuleExecutor::new());
let metric_sink = Arc::new(PrometheusMetricSink::new());
let audit_logger = Arc::new(AuditLogger::new(None, 0));
+ let executor = build_server_executor(metric_sink.clone(), Some(audit_logger.clone()));
let debug_sessions = Arc::new(DebugSessionManager::new());
let defaults = TenantDefaults {
default_qps_limit: None,
@@ -372,9 +372,9 @@ async fn test_admin_reload_with_persistence() {
let store = Arc::new(RwLock::new(RuleStore::new_with_persistence(
dir.path().to_path_buf(),
)));
- let executor = Arc::new(RuleExecutor::new());
let metric_sink = Arc::new(PrometheusMetricSink::new());
let audit_logger = Arc::new(AuditLogger::new(None, 0));
+ let executor = build_server_executor(metric_sink.clone(), Some(audit_logger.clone()));
let debug_sessions = Arc::new(DebugSessionManager::new());
let defaults = TenantDefaults {
default_qps_limit: None,
diff --git a/crates/ordo-server/src/capability_registry.rs b/crates/ordo-server/src/capability_registry.rs
new file mode 100644
index 00000000..16fefd6b
--- /dev/null
+++ b/crates/ordo-server/src/capability_registry.rs
@@ -0,0 +1,576 @@
+use crate::audit::AuditLogger;
+use crate::metrics::PrometheusMetricSink;
+use ordo_core::context::Value;
+use ordo_core::prelude::{
+ CapabilityCategory, CapabilityConfig, CapabilityDescriptor, CapabilityInvoker,
+ CapabilityProvider, CapabilityRegistry, CapabilityRequest, CapabilityResponse, MetricSink,
+ OrdoError, Result, RuleExecutor, TraceConfig,
+};
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::time::Duration;
+
+/// Server-side runtime registry wrapper that adds tracing around capability calls.
+#[derive(Default)]
+pub struct ServerCapabilityRegistry {
+ inner: CapabilityRegistry,
+}
+
+impl ServerCapabilityRegistry {
+ #[inline]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ #[inline]
+ pub fn register(
+ &self,
+ provider: Arc,
+ ) -> Option> {
+ self.inner.register(provider)
+ }
+
+ pub fn register_metric_sink(&self, sink: Arc) {
+ self.register(Arc::new(PrometheusMetricCapability { sink }));
+ }
+
+ pub fn register_audit_logger(&self, audit_logger: Arc) {
+ self.register(Arc::new(AuditCapability { audit_logger }));
+ }
+
+ pub fn register_http_client(&self) {
+ self.register(Arc::new(HttpCapability::new()));
+ }
+}
+
+impl CapabilityInvoker for ServerCapabilityRegistry {
+ fn invoke(&self, request: &CapabilityRequest) -> Result {
+ let span = tracing::info_span!(
+ "capability.invoke",
+ capability = %request.capability,
+ operation = %request.operation
+ );
+ let _guard = span.enter();
+ self.inner.invoke(request)
+ }
+
+ fn describe(&self, capability: &str) -> Option {
+ self.inner.describe(capability)
+ }
+}
+
+pub fn build_server_capability_invoker(
+ metric_sink: Arc,
+ audit_logger: Option>,
+) -> Arc {
+ let registry = Arc::new(ServerCapabilityRegistry::new());
+ registry.register_http_client();
+ registry.register_metric_sink(metric_sink);
+ if let Some(audit_logger) = audit_logger {
+ registry.register_audit_logger(audit_logger);
+ }
+ registry
+}
+
+pub fn build_rule_executor(
+ metric_sink: Arc,
+ capability_invoker: Option>,
+) -> RuleExecutor {
+ let mut executor = RuleExecutor::with_trace_and_metrics(TraceConfig::minimal(), metric_sink);
+ if let Some(capability_invoker) = capability_invoker {
+ executor.set_capability_invoker(capability_invoker);
+ }
+ executor
+}
+
+#[cfg(test)]
+pub fn build_server_executor(
+ metric_sink: Arc,
+ audit_logger: Option>,
+) -> Arc {
+ let capability_invoker = build_server_capability_invoker(metric_sink.clone(), audit_logger);
+ let metric_sink_trait: Arc = metric_sink;
+ Arc::new(build_rule_executor(
+ metric_sink_trait,
+ Some(capability_invoker),
+ ))
+}
+
+pub fn emit_rule_execution_audit(
+ capability_invoker: Option>,
+ audit_logger: &AuditLogger,
+ rule_name: &str,
+ duration_us: u64,
+ result: &str,
+ source_ip: Option,
+) {
+ if let Some(capability_invoker) = capability_invoker {
+ let payload = Value::object({
+ let mut m = std::collections::HashMap::new();
+ m.insert("rule_name".to_string(), Value::string(rule_name));
+ m.insert("duration_us".to_string(), Value::int(duration_us as i64));
+ m.insert("result".to_string(), Value::string(result));
+ if let Some(source_ip) = &source_ip {
+ m.insert("source_ip".to_string(), Value::string(source_ip));
+ }
+ m
+ });
+ let request = CapabilityRequest::new("audit.logger", "rule_executed", payload);
+ match capability_invoker.invoke(&request) {
+ Ok(_) => return,
+ Err(OrdoError::CapabilityNotFound { .. }) => {}
+ Err(error) => {
+ tracing::warn!(rule = %rule_name, error = %error, "Audit capability invocation failed, falling back to direct logger");
+ }
+ }
+ }
+
+ audit_logger.log_execution(rule_name, duration_us, result, source_ip);
+}
+
+pub fn invoke_http_json(
+ capability_invoker: Option>,
+ method: &str,
+ url: &str,
+ headers: HashMap,
+ json_body: &serde_json::Value,
+ timeout_ms: Option,
+) -> Result