├── CLAUDE.md ├── examples ├── 00-counter │ ├── reducer │ │ ├── .gitkeep │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── air │ │ ├── module.air.json │ │ ├── manifest.air.json │ │ └── schemas.air.json │ └── README.md ├── 02-blob-echo │ ├── air │ │ ├── .gitkeep │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── policies.air.json │ │ ├── schemas.air.json │ │ └── manifest.air.json │ ├── reducer │ │ ├── .gitkeep │ │ └── Cargo.toml │ └── README.md ├── 01-hello-timer │ ├── reducer │ │ ├── .gitkeep │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── air │ │ ├── capabilities.air.json │ │ ├── policies.air.json │ │ ├── module.air.json │ │ ├── schemas.air.json │ │ └── manifest.air.json │ └── README.md ├── 08-retry-backoff │ ├── air │ │ ├── capabilities.air.json │ │ ├── policies.air.json │ │ ├── module.air.json │ │ ├── plans.air.json │ │ ├── schemas.air.json │ │ └── manifest.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 09-worldfs-lab │ ├── air │ │ ├── policies.air.json │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── schemas.air.json │ │ ├── manifest.air.json │ │ └── plans.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 07-llm-summarizer │ ├── air │ │ ├── defsecret.llm.air.json │ │ ├── module.air.json │ │ ├── capabilities.air.json │ │ ├── policies.air.json │ │ └── schemas.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 03-fetch-notify │ ├── air │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── policies.air.json │ │ ├── schemas.air.json │ │ ├── fetch_plan.air.json │ │ └── manifest.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 04-aggregator │ ├── air │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── policies.air.json │ │ ├── manifest.air.json │ │ └── schemas.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 05-chain-comp │ ├── air │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── policies.air.json │ │ ├── notify_plan.air.json │ │ ├── refund_plan.air.json │ │ └── charge_plan.air.json │ ├── reducer │ │ └── Cargo.toml │ └── README.md ├── 06-safe-upgrade │ ├── air.v1 │ │ ├── capabilities.air.json │ │ ├── module.air.json │ │ ├── policies.air.json │ │ ├── schemas.air.json │ │ ├── manifest.air.json │ │ └── fetch_plan.air.json │ ├── reducer │ │ └── Cargo.toml │ ├── air.v2 │ │ ├── module.air.json │ │ ├── capabilities.air.json │ │ ├── policies.air.json │ │ ├── schemas.air.json │ │ └── manifest.air.json │ └── README.md └── README.md ├── crates ├── aos-host │ ├── src │ │ ├── cli │ │ │ ├── mod.rs │ │ │ └── commands.rs │ │ ├── modes │ │ │ ├── mod.rs │ │ │ └── batch.rs │ │ ├── adapters │ │ │ ├── mod.rs │ │ │ ├── traits.rs │ │ │ ├── stub.rs │ │ │ └── registry.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── util.rs │ │ └── config.rs │ ├── tests │ │ ├── host_smoke.rs │ │ └── daemon_integration.rs │ └── Cargo.toml ├── aos-air-exec │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── aos-wasm-build │ ├── src │ │ ├── backends │ │ │ ├── mod.rs │ │ │ └── rust.rs │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── util.rs │ │ ├── artifact.rs │ │ ├── config.rs │ │ ├── hash.rs │ │ └── cache │ │ │ └── mod.rs │ └── Cargo.toml ├── aos-wasm-abi │ └── Cargo.toml ├── aos-kernel │ ├── src │ │ ├── shadow │ │ │ ├── mod.rs │ │ │ ├── config.rs │ │ │ └── summary.rs │ │ ├── event.rs │ │ ├── lib.rs │ │ ├── scheduler.rs │ │ ├── reducer.rs │ │ ├── journal │ │ │ └── mem.rs │ │ ├── manifest.rs │ │ └── query.rs │ └── Cargo.toml ├── aos-cbor │ └── Cargo.toml ├── aos-wasm-sdk │ └── Cargo.toml ├── aos-wasm │ └── Cargo.toml ├── aos-store │ └── Cargo.toml ├── aos-air-types │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── tests │ │ ├── patch.rs │ │ ├── mod.rs │ │ └── policies.rs │ │ ├── schemas.rs │ │ └── catalog.rs ├── aos-effects │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ ├── traits.rs │ │ └── receipt.rs ├── aos-cli │ ├── tests │ │ ├── help_flags.rs │ │ ├── blob_cli.rs │ │ ├── obj_cli.rs │ │ └── patch_dir.rs │ ├── src │ │ ├── commands │ │ │ ├── stop.rs │ │ │ ├── replay.rs │ │ │ ├── init.rs │ │ │ ├── snapshot.rs │ │ │ ├── head.rs │ │ │ └── info.rs │ │ ├── input.rs │ │ └── output.rs │ └── Cargo.toml ├── aos-sys │ ├── Cargo.toml │ └── src │ │ ├── bin │ │ ├── agent_registry.rs │ │ └── object_catalog.rs │ │ └── lib.rs └── aos-examples │ ├── Cargo.toml │ ├── src │ ├── counter.rs │ └── fetch_notify.rs │ └── tests │ └── cli.rs ├── .cargo └── config.toml ├── Cargo.toml ├── spec ├── defs │ ├── builtin-caps.air.json │ └── object-catalog.air.json ├── schemas │ ├── defschema.schema.json │ ├── defsecret.schema.json │ ├── defcap.schema.json │ ├── defeffect.schema.json │ ├── defmodule.schema.json │ └── defpolicy.schema.json └── test-vectors │ ├── schemas.json │ ├── canonical-cbor.json │ └── plans.json ├── NOTICE ├── .vscode └── settings.json ├── roadmap ├── v0.1-secrets │ └── p2-secret-todos.md └── v0.3-host │ ├── p0-kernel-prep.md │ └── p5-tests-and-hardening.md ├── CONTRIBUTING.md └── LICENSE-SPEC /CLAUDE.md: -------------------------------------------------------------------------------- 1 | AGENTS.md -------------------------------------------------------------------------------- /examples/00-counter/reducer/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/02-blob-echo/air/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/02-blob-echo/reducer/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/01-hello-timer/reducer/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/aos-host/src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod commands; 2 | -------------------------------------------------------------------------------- /crates/aos-host/src/modes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod batch; 2 | pub mod daemon; 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = ["-C", "target-feature=+multivalue"] 3 | -------------------------------------------------------------------------------- /examples/02-blob-echo/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "sys/blob@1", 5 | "cap_type": "blob", 6 | "schema": { "record": {} } 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /examples/01-hello-timer/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "sys/timer@1", 5 | "cap_type": "timer", 6 | "schema": { "record": {} } 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "sys/timer@1", 5 | "cap_type": "timer", 6 | "schema": { "record": {} } 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "notes/allow_all@1", 5 | "rules": [ 6 | { "when": {}, "decision": "allow" } 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /examples/02-blob-echo/README.md: -------------------------------------------------------------------------------- 1 | # Example 02 — Blob Echo 2 | 3 | - Scope: reducer uses `blob.put`/`blob.get` micro-effects with receipt routing. 4 | - Use `air/`, `reducer/`, and `runner/` to keep this example self-contained. 5 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/default_policy@1", 5 | "rules": [ 6 | { "when": {}, "decision": "allow" } 7 | ] 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/air/defsecret.llm.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "defsecret", 3 | "name": "demo/llm_api_secret@1", 4 | "binding_id": "env:LLM_API_KEY", 5 | "allowed_caps": ["cap_llm"], 6 | "allowed_plans": ["demo/summarize_plan@1"] 7 | } 8 | -------------------------------------------------------------------------------- /crates/aos-air-exec/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! AIR expression evaluation engine plus deterministic value model. 2 | 3 | mod expr; 4 | mod value; 5 | 6 | pub use expr::{Env, EvalError, EvalResult, eval_expr}; 7 | pub use value::{Value, ValueKey, ValueMap, ValueSet}; 8 | -------------------------------------------------------------------------------- /examples/01-hello-timer/README.md: -------------------------------------------------------------------------------- 1 | # Example 01 — Hello Timer 2 | 3 | - Scope: reducer emits `timer.set`, handles `TimerEvent::Fired` (wrapped `sys/TimerFired@1` receipt). 4 | - Follow the detailed spec in TODO.MD when populating `air/` and the reducer crate. 5 | -------------------------------------------------------------------------------- /crates/aos-host/src/adapters/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod registry; 2 | pub mod stub; 3 | pub mod timer; 4 | pub mod traits; 5 | 6 | #[cfg(feature = "adapter-http")] 7 | pub mod http; 8 | #[cfg(feature = "adapter-llm")] 9 | pub mod llm; 10 | 11 | #[cfg(any(feature = "test-fixtures", test))] 12 | pub mod mock; 13 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/backends/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::artifact::BuildArtifact; 2 | use crate::builder::BuildRequest; 3 | use crate::error::BuildError; 4 | 5 | pub mod rust; 6 | 7 | pub trait ModuleCompiler { 8 | fn compile(&self, request: BuildRequest) -> Result; 9 | } 10 | -------------------------------------------------------------------------------- /crates/aos-host/src/adapters/traits.rs: -------------------------------------------------------------------------------- 1 | use aos_effects::EffectIntent; 2 | use async_trait::async_trait; 3 | 4 | #[async_trait] 5 | pub trait AsyncEffectAdapter: Send + Sync { 6 | fn kind(&self) -> &str; 7 | async fn execute(&self, intent: &EffectIntent) -> anyhow::Result; 8 | } 9 | -------------------------------------------------------------------------------- /crates/aos-wasm-abi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-wasm-abi" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | serde = { version = "1", features = ["derive"] } 9 | serde_bytes = "0.11" 10 | thiserror = "1" 11 | serde_cbor = { version = "0.11", default-features = true } 12 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/shadow/mod.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod runner; 3 | mod summary; 4 | 5 | pub use config::{ShadowConfig, ShadowHarness}; 6 | pub use runner::ShadowExecutor; 7 | pub use summary::{ 8 | DeltaKind, LedgerDelta, LedgerKind, PendingPlanReceipt, PlanResultPreview, PredictedEffect, 9 | ShadowSummary, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/00-counter/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "counter-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | serde = { version = "1", features = ["derive"] } 14 | -------------------------------------------------------------------------------- /crates/aos-cbor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-cbor" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | serde = { version = "1", features = ["derive"] } 9 | serde_cbor = "0.11" 10 | sha2 = "0.10" 11 | hex = "0.4" 12 | thiserror = "1" 13 | 14 | [dev-dependencies] 15 | serde_json = "1" 16 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_fetch_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "verbs": { "set": { "text": {} } } 10 | } 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /examples/04-aggregator/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_aggregate_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "verbs": { "set": { "text": {} } } 10 | } 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_chain_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "methods": { "set": { "text": {} } } 10 | } 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_fetch_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "methods": { "set": { "text": {} } } 10 | } 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /examples/01-hello-timer/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/default_policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "timer.set", 9 | "origin_kind": "reducer" 10 | }, 11 | "decision": "allow" 12 | } 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "fetch-notify-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | serde = { version = "1", features = ["derive"] } 14 | serde_cbor = "0.11" 15 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "safe-upgrade-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | serde = { version = "1", features = ["derive"] } 14 | serde_cbor = "0.11" 15 | -------------------------------------------------------------------------------- /crates/aos-air-exec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-air-exec" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-air-types = { path = "../aos-air-types" } 9 | indexmap = "2" 10 | thiserror = "1" 11 | base64 = "0.22" 12 | serde = { version = "1", features = ["derive"] } 13 | serde_cbor = "0.11" 14 | serde_bytes = "0.11" 15 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/event.rs: -------------------------------------------------------------------------------- 1 | use aos_wasm_abi::DomainEvent; 2 | 3 | /// High-level kernel events processed by the deterministic stepper. 4 | #[derive(Debug, Clone)] 5 | pub enum KernelEvent { 6 | Reducer(ReducerEvent), 7 | } 8 | 9 | #[derive(Debug, Clone)] 10 | pub struct ReducerEvent { 11 | pub reducer: String, 12 | pub event: DomainEvent, 13 | } 14 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod artifact; 2 | pub mod backends; 3 | pub mod builder; 4 | pub mod cache; 5 | pub mod config; 6 | pub mod error; 7 | pub mod hash; 8 | pub mod util; 9 | 10 | pub use artifact::BuildArtifact; 11 | pub use builder::{BackendKind, BuildRequest, Builder}; 12 | pub use config::{BuildConfig, Toolchain}; 13 | pub use error::BuildError; 14 | -------------------------------------------------------------------------------- /examples/04-aggregator/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "aggregator-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | indexmap = "2" 14 | serde = { version = "1", features = ["derive"] } 15 | serde_cbor = "0.11" 16 | -------------------------------------------------------------------------------- /examples/05-chain-comp/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "chain-comp-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | indexmap = "2" 14 | serde = { version = "1", features = ["derive"] } 15 | serde_cbor = "0.11" 16 | -------------------------------------------------------------------------------- /examples/00-counter/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/CounterSM@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/CounterState@1", 9 | "event": "demo/CounterEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/ChainComp@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/ChainState@1", 9 | "event": "demo/ChainEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "llm-summarizer-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | indexmap = "2" 14 | serde = { version = "1", features = ["derive"] } 15 | serde_cbor = "0.11" 16 | -------------------------------------------------------------------------------- /examples/04-aggregator/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/Aggregator@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/AggregatorState@1", 9 | "event": "demo/AggregatorEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/FetchNotify@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/FetchNotifyState@1", 9 | "event": "demo/FetchNotifyEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/SafeUpgrade@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/SafeUpgradeState@1", 9 | "event": "demo/SafeUpgradeEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v2/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/SafeUpgrade@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/SafeUpgradeState@1", 9 | "event": "demo/SafeUpgradeEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/LlmSummarizer@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/LlmSummarizerState@1", 9 | "event": "demo/LlmSummarizerEvent@1", 10 | "effects_emitted": [], 11 | "cap_slots": {} 12 | } 13 | } 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /crates/aos-wasm-sdk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-wasm-sdk" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [features] 8 | default = [] 9 | std = [] 10 | 11 | [dependencies] 12 | aos-wasm-abi = { path = "../aos-wasm-abi" } 13 | serde = { version = "1", features = ["derive"] } 14 | serde_cbor = "0.11" 15 | serde_bytes = "0.11" 16 | 17 | [dev-dependencies] 18 | serde_json = "1" 19 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/module.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "defmodule", 3 | "name": "demo/RetrySM@1", 4 | "module_kind": "reducer", 5 | "abi": { 6 | "reducer": { 7 | "state": "demo/RetryState@1", 8 | "event": "demo/RetryEvent@1", 9 | "annotations": "sys/BytesOrSecretRef@1", 10 | "effects_emitted": ["timer.set"], 11 | "cap_slots": { "timer": "timer" } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/aos-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-wasm" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-wasm-abi = { path = "../aos-wasm-abi" } 9 | anyhow = "1" 10 | sha2 = "0.10" 11 | wasmtime = { version = "36.0.3", features = ["cache"] } 12 | log = "0.4" 13 | 14 | [dev-dependencies] 15 | serde = { version = "1", features = ["derive"] } 16 | wat = "1" 17 | tempfile = "3" 18 | -------------------------------------------------------------------------------- /examples/01-hello-timer/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/TimerSM@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/TimerState@1", 9 | "event": "demo/TimerEvent@1", 10 | "effects_emitted": ["timer.set"], 11 | "cap_slots": { 12 | "timer": "timer" 13 | } 14 | } 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/http-policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/fetch_plan@1" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { "when": {}, "decision": "deny" } 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/aos-air-types", 4 | "crates/aos-air-exec", 5 | "crates/aos-cbor", 6 | "crates/aos-store", 7 | "crates/aos-wasm-abi", 8 | "crates/aos-wasm", 9 | "crates/aos-effects", 10 | "crates/aos-kernel", 11 | "crates/aos-host", 12 | "crates/aos-wasm-sdk", 13 | "crates/aos-cli", 14 | "crates/aos-examples", 15 | "crates/aos-wasm-build", 16 | "crates/aos-sys" 17 | ] 18 | resolver = "2" 19 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-wasm-build" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license = "Apache-2.0" 6 | description = "Deterministic WASM compiler for AOS reducers" 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | camino = { version = "1", features = ["serde1"] } 11 | sha2 = "0.10" 12 | tempfile = "3" 13 | thiserror = "1" 14 | which = "4" 15 | home = "0.5.5" 16 | walkdir = "2" 17 | hex = "0.4" 18 | log = "0.4" 19 | -------------------------------------------------------------------------------- /examples/02-blob-echo/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "demo/BlobEchoSM@1", 5 | "module_kind": "reducer", 6 | "abi": { 7 | "reducer": { 8 | "state": "demo/BlobEchoState@1", 9 | "event": "demo/BlobEchoEvent@1", 10 | "effects_emitted": ["blob.put", "blob.get"], 11 | "cap_slots": { 12 | "blob": "blob" 13 | } 14 | } 15 | } 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum BuildError { 6 | #[error("io error: {0}")] 7 | Io(#[from] std::io::Error), 8 | #[error("cargo not found: {0}")] 9 | CargoNotFound(String), 10 | #[error("build process failed: {0}")] 11 | BuildFailed(String), 12 | #[error("wasm artifact not found in {0:?}")] 13 | ArtifactNotFound(PathBuf), 14 | } 15 | -------------------------------------------------------------------------------- /examples/01-hello-timer/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "hello-timer-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | serde = { version = "1", default-features = false, features = ["derive", "alloc"] } 14 | serde_cbor = { version = "0.11", default-features = false, features = ["alloc"] } 15 | -------------------------------------------------------------------------------- /examples/04-aggregator/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/http-aggregate-policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/aggregator_plan@1" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { "when": {}, "decision": "deny" } 15 | ] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /crates/aos-store/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-store" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-cbor = { path = "../aos-cbor" } 9 | aos-air-types = { path = "../aos-air-types" } 10 | serde = { version = "1", features = ["derive"] } 11 | serde_cbor = "0.11" 12 | serde_json = "1" 13 | anyhow = "1" 14 | thiserror = "1" 15 | hex = "0.4" 16 | 17 | [dev-dependencies] 18 | tempfile = "3" 19 | indexmap = "2" 20 | -------------------------------------------------------------------------------- /crates/aos-air-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-air-types" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | serde = { version = "1", features = ["derive"] } 9 | serde_json = "1" 10 | serde_cbor = "0.11" 11 | indexmap = { version = "2", features = ["serde"] } 12 | petgraph = "0.6" 13 | thiserror = "1" 14 | once_cell = "1" 15 | aos-cbor = { path = "../aos-cbor" } 16 | 17 | [dev-dependencies] 18 | jsonschema = "0.17" 19 | -------------------------------------------------------------------------------- /spec/defs/builtin-caps.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "sys/query@1", 5 | "cap_type": "query", 6 | "schema": { 7 | "record": { 8 | "scope": { 9 | "option": { "text": {} }, 10 | "$comment": "Optional scope string or prefix hint; empty = all introspection" 11 | } 12 | } 13 | }, 14 | "$comment": "Grants use of introspect.* effects (read-only world inspection)." 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /crates/aos-effects/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-effects" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-cbor = { path = "../aos-cbor" } 9 | aos-air-types = { path = "../aos-air-types" } 10 | indexmap = { version = "2", features = ["serde"] } 11 | serde = { version = "1", features = ["derive"] } 12 | serde_bytes = "0.11" 13 | serde_json = "1" 14 | thiserror = "1" 15 | hex = "0.4" 16 | serde_cbor = "0.11" 17 | once_cell = "1" 18 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/http-policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/fetch_plan@1" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { 15 | "when": {}, 16 | "decision": "deny" 17 | } 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::process::Command; 3 | 4 | pub fn resolve_cargo() -> Result { 5 | which::which("cargo").context("cargo executable not found") 6 | } 7 | 8 | pub fn spawn_command(mut cmd: Command) -> Result { 9 | let program = format!("{:?}", cmd); 10 | let output = cmd 11 | .output() 12 | .with_context(|| format!("failed to run {program}"))?; 13 | Ok(output) 14 | } 15 | -------------------------------------------------------------------------------- /spec/schemas/defschema.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defschema.schema.json", 4 | "title": "AIR v1 defschema", 5 | "type": "object", 6 | "properties": { 7 | "$kind": { "const": "defschema" }, 8 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 9 | "type": { "$ref": "common.schema.json#/$defs/TypeExpr" } 10 | }, 11 | "required": ["$kind","name","type"], 12 | "additionalProperties": false 13 | } -------------------------------------------------------------------------------- /examples/02-blob-echo/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/blob_policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "blob.put", 9 | "origin_kind": "reducer" 10 | }, 11 | "decision": "allow" 12 | }, 13 | { 14 | "when": { 15 | "effect_kind": "blob.get", 16 | "origin_kind": "reducer" 17 | }, 18 | "decision": "allow" 19 | } 20 | ] 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /crates/aos-host/src/cli/commands.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, Subcommand}; 4 | 5 | #[derive(Parser, Debug)] 6 | pub struct Cli { 7 | #[command(subcommand)] 8 | pub command: Commands, 9 | } 10 | 11 | #[derive(Subcommand, Debug)] 12 | pub enum Commands { 13 | Init { 14 | path: PathBuf, 15 | }, 16 | Step { 17 | path: PathBuf, 18 | #[arg(long)] 19 | event: Option, 20 | #[arg(long)] 21 | value: Option, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /crates/aos-host/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum HostError { 5 | #[error("kernel error: {0}")] 6 | Kernel(#[from] aos_kernel::KernelError), 7 | #[error("adapter error: {0}")] 8 | Adapter(String), 9 | #[error("invalid external event: {0}")] 10 | External(String), 11 | #[error("store error: {0}")] 12 | Store(String), 13 | #[error("manifest error: {0}")] 14 | Manifest(String), 15 | #[error("timer error: {0}")] 16 | Timer(String), 17 | } 18 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "retry_sm" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 13 | serde = { version = "1", default-features = false, features = ["derive", "alloc"] } 14 | serde_json = { version = "1", default-features = false, features = ["alloc"] } 15 | serde_cbor = { version = "0.11", default-features = false, features = ["alloc"] } 16 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! AIR v1 core data model and semantic validation (informed by spec/03-air.md). 2 | 3 | pub mod builtins; 4 | pub mod catalog; 5 | mod model; 6 | pub mod plan_literals; 7 | mod refs; 8 | pub mod schemas; 9 | pub mod typecheck; 10 | pub mod validate; 11 | pub mod value_normalize; 12 | 13 | pub use model::*; 14 | pub use refs::{HashRef, RefError, SchemaRef}; 15 | pub use typecheck::{ValueTypeError, validate_value_literal}; 16 | pub use validate::validate_manifest; 17 | 18 | #[cfg(test)] 19 | mod tests; 20 | -------------------------------------------------------------------------------- /examples/02-blob-echo/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "blob-echo-reducer" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | aos-air-types = { path = "../../../crates/aos-air-types" } 13 | aos-effects = { path = "../../../crates/aos-effects" } 14 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 15 | serde = { version = "1", features = ["derive"] } 16 | serde_bytes = "0.11" 17 | serde_cbor = "0.11" 18 | sha2 = "0.10" 19 | hex = "0.4" 20 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/README.md: -------------------------------------------------------------------------------- 1 | # Example 03 — Fetch & Notify 2 | 3 | Plan-driven demo that shows a reducer emitting a `FetchRequest` DomainIntent, 4 | triggering a plan that performs an HTTP request, then raising a typed 5 | `NotifyComplete` event back to the reducer. 6 | 7 | Artifacts: 8 | 9 | - `air/` — all AIR assets (schemas, reducer module, manifest, capabilities, policies, and plans) 10 | - `reducer/` — Wasm reducer crate compiled via `aos-wasm-build` 11 | - `defs/` — reserved for shared/builtin JSON definitions used by the example 12 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/artifact.rs: -------------------------------------------------------------------------------- 1 | use crate::hash::WasmDigest; 2 | use std::fs; 3 | use std::path::Path; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct BuildArtifact { 7 | pub wasm_bytes: Vec, 8 | pub wasm_hash: WasmDigest, 9 | pub build_log: Option, 10 | } 11 | 12 | impl BuildArtifact { 13 | pub fn write_to(&self, output: impl AsRef) -> std::io::Result<()> { 14 | fs::write(output, &self.wasm_bytes) 15 | } 16 | 17 | pub fn bytes(&self) -> &[u8] { 18 | &self.wasm_bytes 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "notes/blob_cap@1", 5 | "cap_type": "blob", 6 | "schema": { "record": {} } 7 | }, 8 | { 9 | "$kind": "defcap", 10 | "name": "sys/blob@1", 11 | "cap_type": "blob", 12 | "schema": { "record": {} } 13 | }, 14 | { 15 | "$kind": "defcap", 16 | "name": "sys/query@1", 17 | "cap_type": "query", 18 | "schema": { 19 | "record": { 20 | "scope": { "option": { "text": {} } } 21 | } 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AgentOS – A deterministic substrate and runtime for agent systems 2 | Copyright (c) 2025 Smart Computer Co. and the AgentOS contributors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this work except in compliance with the License. 6 | You may obtain a copy at: http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | This distribution includes code, documentation, and schemas contributed 9 | by independent developers under the same license. Individual source files 10 | may carry additional notices and attribution per Section 4(d) of the License. 11 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/air/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_fetch_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "methods": { "set": { "text": {} } } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defcap", 15 | "name": "demo/llm_summarize_cap@1", 16 | "cap_type": "llm.basic", 17 | "schema": { 18 | "record": { 19 | "models": { "set": { "text": {} } }, 20 | "max_tokens": { "nat": {} } 21 | } 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /crates/aos-effects/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Shared effect intent/receipt/capability types and helpers. 2 | 3 | pub mod builtins; 4 | pub mod normalize; 5 | 6 | mod capability; 7 | mod intent; 8 | mod receipt; 9 | pub mod traits; 10 | 11 | pub use aos_air_types::EffectKind; 12 | pub use capability::{ 13 | CapabilityBudget, CapabilityEncodeError, CapabilityGrant, CapabilityGrantBuilder, 14 | }; 15 | pub use intent::{EffectIntent, EffectSource, IdempotencyKey, IntentBuilder, IntentEncodeError}; 16 | pub use normalize::{NormalizeError, normalize_effect_params}; 17 | pub use receipt::{EffectReceipt, ReceiptDecodeError, ReceiptStatus}; 18 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v2/capabilities.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defcap", 4 | "name": "demo/http_fetch_cap@1", 5 | "cap_type": "http.out", 6 | "schema": { 7 | "record": { 8 | "hosts": { "set": { "text": {} } }, 9 | "methods": { "set": { "text": {} } } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defcap", 15 | "name": "demo/http_followup_cap@1", 16 | "cap_type": "http.out", 17 | "schema": { 18 | "record": { 19 | "hosts": { "set": { "text": {} } }, 20 | "methods": { "set": { "text": {} } } 21 | } 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /crates/aos-host/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod adapters; 2 | pub mod cli; 3 | pub mod config; 4 | pub mod control; 5 | pub mod error; 6 | pub mod host; 7 | pub mod manifest_loader; 8 | pub mod modes; 9 | pub mod util; 10 | 11 | pub mod testhost; 12 | 13 | #[cfg(any(feature = "test-fixtures", test))] 14 | pub mod fixtures; 15 | 16 | pub use adapters::timer::TimerScheduler; 17 | pub use control::{ControlClient, ControlServer, RequestEnvelope, ResponseEnvelope}; 18 | pub use host::{ExternalEvent, RunMode, WorldHost, now_wallclock_ns}; 19 | pub use modes::batch::{BatchRunner, StepResult}; 20 | pub use modes::daemon::{ControlMsg, WorldDaemon}; 21 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/shadow/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::governance::ManifestPatch; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct ShadowConfig { 7 | pub proposal_id: u64, 8 | pub patch: ManifestPatch, 9 | pub patch_hash: String, 10 | #[serde(default, skip_serializing_if = "Option::is_none")] 11 | pub harness: Option, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 15 | pub struct ShadowHarness { 16 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 17 | pub seed_events: Vec<(String, Vec)>, 18 | } 19 | -------------------------------------------------------------------------------- /spec/test-vectors/schemas.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "defschema_point", 4 | "json": { 5 | "$kind": "defschema", 6 | "name": "com.acme/Point@1", 7 | "type": { 8 | "record": { 9 | "x": { 10 | "int": {} 11 | }, 12 | "y": { 13 | "int": {} 14 | } 15 | } 16 | } 17 | }, 18 | "cbor_hex": "d9d9f7a3646e616d6570636f6d2e61636d652f506f696e7440316474797065a1667265636f7264a26178a163696e74a06179a163696e74a065246b696e6469646566736368656d61", 19 | "hash": "sha256:9b74eadb8810612f1becfbf9b4502cccb0ef290b90e3e912b316fc279e779add" 20 | } 21 | ] -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable every feature flag across the workspace; the old `features: ["all"]` 3 | // setting passed a nonexistent feature to every crate and broke cargo checks. 4 | "rust-analyzer.cargo.allFeatures": true, 5 | // Explicitly clear any user-level `cargo.features` override (e.g. ["all"]) 6 | // so rust-analyzer doesn't keep passing `--features all`. 7 | "rust-analyzer.cargo.features": [], 8 | // Use the rustup-provided rust-analyzer (stable toolchain) to avoid bundled-server regressions. 9 | //"rust-analyzer.server.path": "/Users/lukas/.rustup/toolchains/stable-aarch64-apple-darwin/bin/rust-analyzer" 10 | } 11 | -------------------------------------------------------------------------------- /examples/00-counter/README.md: -------------------------------------------------------------------------------- 1 | # Example 00 — CounterSM 2 | 3 | A minimal counter state machine with no micro-effects. 4 | 5 | ## Structure 6 | 7 | - `air/` — AIR JSON definitions (schemas, module, manifest) 8 | - `reducer/` — WASM reducer crate 9 | 10 | ## Running 11 | 12 | ```bash 13 | # Via example runner (with replay verification) 14 | cargo run -p aos-examples -- counter 15 | 16 | # Via CLI 17 | aos world step examples/00-counter --reset-journal 18 | aos world step examples/00-counter --event demo/CounterEvent@1 --value '{"Start": {"target": 3}}' 19 | aos world step examples/00-counter --event demo/CounterEvent@1 --value '"Tick"' 20 | ``` 21 | -------------------------------------------------------------------------------- /crates/aos-cli/tests/help_flags.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn help_mentions_new_flags_and_nouns() { 3 | let output = std::process::Command::new(assert_cmd::cargo::cargo_bin!("aos")) 4 | .arg("--help") 5 | .output() 6 | .expect("run help"); 7 | assert!(output.status.success(), "--help should succeed"); 8 | let text = String::from_utf8_lossy(&output.stdout); 9 | 10 | // Check a couple of important flags and nouns. 11 | for needle in ["--no-meta", "--pretty", "event", "obj", "blob"] { 12 | assert!( 13 | text.contains(needle), 14 | "help output should contain '{needle}'" 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/README.md: -------------------------------------------------------------------------------- 1 | # Example 07 — LLM Summarizer 2 | 3 | This demo wires together an HTTP fetch with a mocked LLM call to produce a 4 | summary. The plan fetches a document, hands it to a deterministic LLM harness, 5 | then reports the summary and token usage back to the reducer. 6 | 7 | * Reducer: `demo/LlmSummarizer@1` (tracks requests and summaries) 8 | * Plan: `demo/summarize_plan@1` (HTTP → LLM → event) 9 | * Capabilities: `demo/http_fetch_cap@1`, `demo/llm_summarize_cap@1` 10 | * Policy: `demo/llm-policy@1` allowing `llm.generate` only from the plan 11 | 12 | Run it with: 13 | 14 | ``` 15 | cargo run -p aos-examples -- llm-summarizer 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v2/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/http-policy@2", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/fetch_plan@2" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { 15 | "when": { 16 | "effect_kind": "http.request", 17 | "origin_kind": "plan", 18 | "origin_name": "demo/fetch_plan@1" 19 | }, 20 | "decision": "allow" 21 | }, 22 | { "when": {}, "decision": "deny" } 23 | ] 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /crates/aos-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-sys" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [lib] 8 | path = "src/lib.rs" 9 | 10 | [[bin]] 11 | name = "object_catalog" 12 | path = "src/bin/object_catalog.rs" 13 | 14 | [[bin]] 15 | name = "agent_registry" 16 | path = "src/bin/agent_registry.rs" 17 | 18 | [[bin]] 19 | name = "notes_notebook" 20 | path = "src/bin/notes_notebook.rs" 21 | 22 | [dependencies] 23 | aos-wasm-sdk = { path = "../aos-wasm-sdk" } 24 | serde = { version = "1", features = ["derive"] } 25 | serde_cbor = "0.11" 26 | sha2 = { version = "0.10", default-features = false } 27 | hex = { version = "0.4", default-features = false, features = ["alloc"] } 28 | -------------------------------------------------------------------------------- /examples/00-counter/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/CounterPc@1" 6 | }, 7 | { 8 | "name": "demo/CounterState@1" 9 | }, 10 | { 11 | "name": "demo/CounterEvent@1" 12 | } 13 | ], 14 | "modules": [ 15 | { 16 | "name": "demo/CounterSM@1" 17 | } 18 | ], 19 | "plans": [], 20 | "caps": [], 21 | "policies": [], 22 | "effects": [], 23 | "triggers": [], 24 | "routing": { 25 | "events": [ 26 | { 27 | "event": "demo/CounterEvent@1", 28 | "reducer": "demo/CounterSM@1" 29 | } 30 | ], 31 | "inboxes": [] 32 | }, 33 | "air_version": "1" 34 | } 35 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/llm-policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/summarize_plan@1" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { 15 | "when": { 16 | "effect_kind": "llm.generate", 17 | "origin_kind": "plan", 18 | "origin_name": "demo/summarize_plan@1" 19 | }, 20 | "decision": "allow" 21 | }, 22 | { 23 | "when": {}, 24 | "decision": "deny" 25 | } 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/plans.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/WorkPlan@1", 5 | "input": "demo/WorkRequested@1", 6 | "steps": [ 7 | { 8 | "id": "fail", 9 | "op": "raise_event", 10 | "event": "demo/RetryEvent@1", 11 | "value": { 12 | "err": { 13 | "req_id": "req-123", 14 | "transient": true, 15 | "message": "simulated transient failure" 16 | } 17 | } 18 | }, 19 | { "id": "end", "op": "end" } 20 | ], 21 | "edges": [ {"from": "fail", "to": "end"} ], 22 | "required_caps": [], 23 | "allowed_effects": [], 24 | "invariants": [] 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/config.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug)] 2 | pub struct BuildConfig { 3 | pub toolchain: Toolchain, 4 | pub release: bool, 5 | } 6 | 7 | impl Default for BuildConfig { 8 | fn default() -> Self { 9 | Self { 10 | toolchain: Toolchain::default(), 11 | release: true, 12 | } 13 | } 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct Toolchain { 18 | pub rustup_toolchain: Option, 19 | pub target: String, 20 | } 21 | 22 | impl Default for Toolchain { 23 | fn default() -> Self { 24 | Self { 25 | rustup_toolchain: None, 26 | target: "wasm32-unknown-unknown".into(), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/test-vectors/canonical-cbor.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "empty_map", 4 | "json": {}, 5 | "cbor_hex": "d9d9f7a0", 6 | "hash": "sha256:75bd49cb964988d6fd2c058f51230c3dc2a254753751fbe0f313b4b1e0681b69" 7 | }, 8 | { 9 | "label": "simple_record", 10 | "json": { 11 | "a": 1, 12 | "b": 2 13 | }, 14 | "cbor_hex": "d9d9f7a2616101616202", 15 | "hash": "sha256:7d5fc784e8d341244b992c902413575266e3140e9081fe12fc95849cf27e7df6" 16 | }, 17 | { 18 | "label": "list_mixed", 19 | "json": [ 20 | "hi", 21 | 1, 22 | true, 23 | null 24 | ], 25 | "cbor_hex": "d9d9f78462686901f5f6", 26 | "hash": "sha256:0e2c2f23ecdea86fcc33f4975460ea705a19da61c5eef7218112387036581271" 27 | } 28 | ] -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/stop.rs: -------------------------------------------------------------------------------- 1 | //! `aos stop` command. 2 | 3 | use anyhow::Result; 4 | use serde_json; 5 | 6 | use crate::opts::{WorldOpts, resolve_dirs}; 7 | use crate::output::print_success; 8 | 9 | use super::try_control_client; 10 | 11 | pub async fn cmd_stop(opts: &WorldOpts) -> Result<()> { 12 | let dirs = resolve_dirs(opts)?; 13 | 14 | let mut client = try_control_client(&dirs) 15 | .await 16 | .ok_or_else(|| anyhow::anyhow!("No daemon running. Nothing to shut down."))?; 17 | 18 | let resp = client.shutdown("cli-shutdown").await?; 19 | if !resp.ok { 20 | anyhow::bail!("shutdown failed: {:?}", resp.error); 21 | } 22 | print_success(opts, serde_json::json!({ "stopped": true }), None, vec![]) 23 | } 24 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/tests/patch.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | 3 | use super::assert_json_schema; 4 | use crate::schemas::PATCH; 5 | 6 | #[test] 7 | fn patch_schema_accepts_minimal_add_def() { 8 | let doc = json!({ 9 | "base_manifest_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", 10 | "patches": [ 11 | { 12 | "add_def": { 13 | "kind": "defschema", 14 | "node": { 15 | "$kind": "defschema", 16 | "name": "demo/Foo@1", 17 | "type": { "bool": {} } 18 | } 19 | } 20 | } 21 | ] 22 | }); 23 | assert_json_schema(PATCH, &doc); 24 | } 25 | -------------------------------------------------------------------------------- /examples/04-aggregator/README.md: -------------------------------------------------------------------------------- 1 | # Example 04 — Aggregator (fan-out + join) 2 | 3 | This rung fans out three HTTP requests through a plan, waits for their 4 | receipts out of order, and then raises an `AggregateComplete` event back 5 | to the reducer. The reducer tracks pending fan-out work keyed by 6 | `request_id`, emits the `AggregateRequested@1` intent (including the 7 | per-target method/URL/name supplied in the `Start` event), and stores the 8 | response summaries for every target once the plan rejoins. 9 | 10 | Artifacts: 11 | 12 | - `air/` — canonical JSON AIR assets (schemas, reducer module, manifest, 13 | capabilities, policies, and plans) 14 | - `reducer/` — Wasm reducer crate compiled via `aos-wasm-build` 15 | - `defs/` — reserved for shared/builtin JSON definitions 16 | -------------------------------------------------------------------------------- /spec/test-vectors/plans.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "defplan_min_end", 4 | "json": { 5 | "$kind": "defplan", 6 | "name": "com.acme/Plan@1", 7 | "input": "com.acme/Point@1", 8 | "steps": [ 9 | { 10 | "id": "end", 11 | "op": "end" 12 | } 13 | ], 14 | "edges": [], 15 | "required_caps": [], 16 | "allowed_effects": [] 17 | }, 18 | "cbor_hex": "d9d9f7a7646e616d656f636f6d2e61636d652f506c616e403165246b696e6467646566706c616e6565646765738065696e70757470636f6d2e61636d652f506f696e74403165737465707381a262696463656e64626f7063656e646d72657175697265645f63617073806f616c6c6f7765645f6566666563747380", 19 | "hash": "sha256:d22044a0312bc264029427c93af440d1c27749468f7bd94cdad3100849b343ae" 20 | } 21 | ] -------------------------------------------------------------------------------- /examples/09-worldfs-lab/reducer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | [package] 4 | name = "worldfs_notebook" 5 | version = "0.1.0" 6 | edition = "2024" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | path = "src/lib.rs" 11 | 12 | [dependencies] 13 | aos-wasm-sdk = { path = "../../../crates/aos-wasm-sdk" } 14 | aos-air-types = { path = "../../../crates/aos-air-types" } 15 | serde = { version = "1", default-features = false, features = ["derive"] } 16 | serde_bytes = { version = "0.11", default-features = false, features = ["alloc"] } 17 | sha2 = { version = "0.10", default-features = false } 18 | hex = { version = "0.4", default-features = false, features = ["alloc"] } 19 | serde_cbor = { version = "0.11", default-features = false, features = ["alloc"] } 20 | 21 | [features] 22 | default = [] 23 | std = ["serde/std", "sha2/std", "hex/std"] 24 | -------------------------------------------------------------------------------- /examples/00-counter/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/CounterPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Counting": { "unit": {} }, 9 | "Done": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/CounterState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/CounterPc@1" }, 19 | "remaining": { "nat": {} } 20 | } 21 | } 22 | }, 23 | { 24 | "$kind": "defschema", 25 | "name": "demo/CounterEvent@1", 26 | "type": { 27 | "variant": { 28 | "Start": { 29 | "record": { 30 | "target": { "nat": {} } 31 | } 32 | }, 33 | "Tick": { "unit": {} } 34 | } 35 | } 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/module.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "notes/NotebookSM@1", 5 | "module_kind": "reducer", 6 | "key_schema": "notes/NoteKey@1", 7 | "abi": { 8 | "reducer": { 9 | "state": "notes/NoteState@1", 10 | "event": "notes/NoteEvent@1", 11 | "annotations": "sys/BytesOrSecretRef@1", 12 | "effects_emitted": [], 13 | "cap_slots": {} 14 | } 15 | } 16 | }, 17 | { 18 | "$kind": "defmodule", 19 | "name": "sys/ObjectCatalog@1", 20 | "module_kind": "reducer", 21 | "key_schema": "sys/ObjectKey@1", 22 | "abi": { 23 | "reducer": { 24 | "state": "sys/ObjectVersions@1", 25 | "event": "sys/ObjectRegistered@1", 26 | "effects_emitted": [], 27 | "cap_slots": {} 28 | } 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /crates/aos-kernel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-kernel" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-air-types = { path = "../aos-air-types" } 9 | aos-air-exec = { path = "../aos-air-exec" } 10 | aos-effects = { path = "../aos-effects" } 11 | aos-wasm = { path = "../aos-wasm" } 12 | aos-wasm-abi = { path = "../aos-wasm-abi" } 13 | aos-cbor = { path = "../aos-cbor" } 14 | aos-store = { path = "../aos-store" } 15 | anyhow = "1" 16 | indexmap = "2" 17 | thiserror = "1" 18 | serde = { version = "1", features = ["derive"] } 19 | serde_cbor = "0.11" 20 | serde_json = "1" 21 | log = "0.4" 22 | serde_bytes = "0.11" 23 | hex = "0.4" 24 | base64 = "0.21" 25 | wasmtime = "36.0.3" 26 | 27 | [dev-dependencies] 28 | wat = "1" 29 | tempfile = "3" 30 | aos-wasm-build = { path = "../aos-wasm-build" } 31 | camino = "1" 32 | walkdir = "2" 33 | 34 | [lints.rust] 35 | unused_imports = "allow" 36 | unused_variables = "allow" 37 | dead_code = "allow" 38 | -------------------------------------------------------------------------------- /crates/aos-sys/src/bin/agent_registry.rs: -------------------------------------------------------------------------------- 1 | #![crate_type = "cdylib"] 2 | 3 | // Minimal main to satisfy bin target when built for wasm. 4 | #[cfg(target_arch = "wasm32")] 5 | fn main() {} 6 | 7 | #[cfg(not(target_arch = "wasm32"))] 8 | fn main() {} 9 | 10 | use aos_wasm_sdk::{ReduceError, Reducer, ReducerCtx, Value, aos_reducer}; 11 | use serde::{Deserialize, Serialize}; 12 | 13 | aos_reducer!(AgentRegistry); 14 | 15 | #[derive(Default)] 16 | struct AgentRegistry; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | struct AgentEvent; 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 22 | struct AgentState; 23 | 24 | impl Reducer for AgentRegistry { 25 | type State = AgentState; 26 | type Event = AgentEvent; 27 | type Ann = Value; 28 | 29 | fn reduce( 30 | &mut self, 31 | _event: Self::Event, 32 | _ctx: &mut ReducerCtx, 33 | ) -> Result<(), ReduceError> { 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spec/defs/object-catalog.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defmodule", 4 | "name": "sys/ObjectCatalog@1", 5 | "module_kind": "reducer", 6 | "key_schema": "sys/ObjectKey@1", 7 | "abi": { 8 | "reducer": { 9 | "state": "sys/ObjectVersions@1", 10 | "event": "sys/ObjectRegistered@1", 11 | "effects_emitted": [], 12 | "cap_slots": {} 13 | } 14 | }, 15 | "$comment": "Keyed reducer for versioned object catalog. Key = meta.name (text)." 16 | }, 17 | { 18 | "$kind": "defcap", 19 | "name": "sys/catalog.write@1", 20 | "cap_type": "catalog.write", 21 | "schema": { 22 | "record": { 23 | "prefixes": { 24 | "set": { "text": {} }, 25 | "$comment": "Allowed name prefixes (e.g., 'agents/self/'). Empty set = all names." 26 | } 27 | } 28 | }, 29 | "$comment": "Capability for writing objects to the catalog. Policy should require this cap for plans emitting sys/ObjectRegistered@1." 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /examples/05-chain-comp/README.md: -------------------------------------------------------------------------------- 1 | # Example 05 — Chain + Compensation (M4 multi-plan choreography) 2 | 3 | This rung demonstrates reducer-driven sagas that stitch multiple plans 4 | into a deterministic choreography: 5 | 6 | 1. `charge_plan` handles payment authorization 7 | 2. `reserve_plan` reserves inventory 8 | 3. `notify_plan` emits a downstream notification when everything succeeds 9 | 4. `refund_plan` compensates the charge when the reservation fails 10 | 11 | The reducer keeps track of a `request_id` correlation key, emits 12 | `*_Requested@1` intents with matching keys, and triggers the refund plan 13 | if a `ReserveFailed` event arrives. Plans reuse the shared HTTP harness so 14 | we can respond with synthetic receipts and exercise the failure path. 15 | 16 | Artifacts: 17 | 18 | - `air/` — canonical JSON AIR assets (schemas, reducer module, manifest, 19 | capabilities, policies, and plans: `charge`, `reserve`, `notify`, 20 | `refund`) 21 | - `reducer/` — Wasm reducer crate compiled via `aos-wasm-build` 22 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/hash.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | use std::io::Read; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 5 | pub struct WasmDigest(pub [u8; 32]); 6 | 7 | impl WasmDigest { 8 | pub fn of_bytes(bytes: &[u8]) -> Self { 9 | let mut hasher = Sha256::new(); 10 | hasher.update(bytes); 11 | let digest = hasher.finalize(); 12 | let mut arr = [0u8; 32]; 13 | arr.copy_from_slice(&digest); 14 | WasmDigest(arr) 15 | } 16 | 17 | pub fn of_reader(mut reader: impl Read) -> std::io::Result { 18 | let mut hasher = Sha256::new(); 19 | let mut buf = [0u8; 8192]; 20 | loop { 21 | let n = reader.read(&mut buf)?; 22 | if n == 0 { 23 | break; 24 | } 25 | hasher.update(&buf[..n]); 26 | } 27 | let digest = hasher.finalize(); 28 | let mut arr = [0u8; 32]; 29 | arr.copy_from_slice(&digest); 30 | Ok(WasmDigest(arr)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/README.md: -------------------------------------------------------------------------------- 1 | # Example 06 — Safe Upgrade 2 | 3 | This demo exercises the governance loop for upgrading a plan. It starts with 4 | `demo/fetch_plan@1`, proposes a manifest that switches to `demo/fetch_plan@2` 5 | (and adds a new HTTP capability + policy), runs a shadow prediction, and then 6 | approves/applies the change before re-running the workflow. 7 | 8 | * Reducer: `demo/SafeUpgrade@1` (WASM in `reducer/`) 9 | * Plans: `demo/fetch_plan@1` (single HTTP) → `demo/fetch_plan@2` (adds follow-up HTTP) 10 | * Capabilities: `demo/http_fetch_cap@1` plus new `demo/http_followup_cap@1` 11 | * Policy upgrade: `demo/http-policy@1` → `demo/http-policy@2` 12 | 13 | Layout: 14 | 15 | - `air.v1/` — AIR v1 bundle (schemas, module, manifest, caps/policies, `fetch_plan@1`) 16 | - `air.v2/` — upgraded AIR bundle (adds `fetch_plan@2`, new cap/policy) used for the proposal 17 | - `reducer/` — Wasm reducer crate compiled via `aos-wasm-build` 18 | 19 | Run it with: 20 | 21 | ``` 22 | cargo run -p aos-examples -- safe-upgrade 23 | ``` 24 | -------------------------------------------------------------------------------- /crates/aos-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-cli" 3 | version = "0.1.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | aos-air-types = { path = "../aos-air-types" } 9 | aos-kernel = { path = "../aos-kernel" } 10 | aos-host = { path = "../aos-host" } 11 | aos-store = { path = "../aos-store" } 12 | aos-sys = { path = "../aos-sys" } 13 | aos-wasm-build = { path = "../aos-wasm-build" } 14 | camino = "1" 15 | clap = { version = "4", features = ["derive", "env"] } 16 | hex = "0.4" 17 | base64 = "0.22" 18 | serde_json = "1" 19 | serde_cbor = "0.11" 20 | tokio = { version = "1", features = ["full", "signal"] } 21 | anyhow = "1" 22 | tracing = "0.1" 23 | tracing-subscriber = { version = "0.3", features = ["fmt"] } 24 | dotenvy = "0.15" 25 | jsonschema = "0.17" 26 | aos-cbor = { path = "../aos-cbor" } 27 | serde = { version = "1", features = ["derive"] } 28 | walkdir = "2" 29 | 30 | [dev-dependencies] 31 | anyhow = "1" 32 | serde_json = "1" 33 | assert_cmd = "2" 34 | tempfile = "3" 35 | 36 | [[bin]] 37 | name = "aos" 38 | path = "src/main.rs" 39 | -------------------------------------------------------------------------------- /examples/01-hello-timer/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/TimerPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Awaiting": { "unit": {} }, 9 | "Done": { "unit": {} }, 10 | "TimedOut": { "unit": {} } 11 | } 12 | } 13 | }, 14 | { 15 | "$kind": "defschema", 16 | "name": "demo/TimerState@1", 17 | "type": { 18 | "record": { 19 | "pc": { "ref": "demo/TimerPc@1" }, 20 | "key": { "option": { "text": {} } }, 21 | "deadline_ns": { "option": { "nat": {} } }, 22 | "fired_key": { "option": { "text": {} } } 23 | } 24 | } 25 | }, 26 | { 27 | "$kind": "defschema", 28 | "name": "demo/TimerEvent@1", 29 | "type": { 30 | "variant": { 31 | "Start": { 32 | "record": { 33 | "deliver_at_ns": { "nat": {} }, 34 | "key": { "option": { "text": {} } } 35 | } 36 | }, 37 | "Fired": { "ref": "sys/TimerFired@1" } 38 | } 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/policies.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defpolicy", 4 | "name": "demo/http-chain-policy@1", 5 | "rules": [ 6 | { 7 | "when": { 8 | "effect_kind": "http.request", 9 | "origin_kind": "plan", 10 | "origin_name": "demo/charge_plan@1" 11 | }, 12 | "decision": "allow" 13 | }, 14 | { 15 | "when": { 16 | "effect_kind": "http.request", 17 | "origin_kind": "plan", 18 | "origin_name": "demo/reserve_plan@1" 19 | }, 20 | "decision": "allow" 21 | }, 22 | { 23 | "when": { 24 | "effect_kind": "http.request", 25 | "origin_kind": "plan", 26 | "origin_name": "demo/notify_plan@1" 27 | }, 28 | "decision": "allow" 29 | }, 30 | { 31 | "when": { 32 | "effect_kind": "http.request", 33 | "origin_kind": "plan", 34 | "origin_name": "demo/refund_plan@1" 35 | }, 36 | "decision": "allow" 37 | }, 38 | { "when": {}, "decision": "deny" } 39 | ] 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /roadmap/v0.1-secrets/p2-secret-todos.md: -------------------------------------------------------------------------------- 1 | From the current code vs. p1-secrets.md, the main gaps are: 2 | 3 | - **Vault effects / rotation path:** We added enums/schemas, but there’s no runtime support or example harness for vault.put/vault.rotate, nor a design-time rotation flow that emits a manifest patch. 4 | - **Receipt redaction & secret_meta:** Injection happens, but receipts/journal don’t yet carry secret_meta, and redaction is left to adapters; there’s no kernel-level enforcement or adapter implementations beyond the HTTP/LLM mocks. 5 | - **Strict resolver requirement in examples:** The LLM demo still relies on a demo key resolver; there’s no real resolver configuration story beyond that. 6 | - (DONE) **Normalization choice:** We’re tolerating multiple SecretRef variant shapes instead of canonicalizing params before hashing/dispatch (spec allows one canonical form). 7 | - **Docs/examples for rotation and resolver config:** No example/readme showing how to rotate secrets via vault.* or how to configure resolvers outside the demo key. 8 | 9 | Everything else in the spec (manifest secrets, SecretRef schemas, policy ACLs, injection-only v1 stance) is implemented. -------------------------------------------------------------------------------- /spec/schemas/defsecret.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defsecret.schema.json", 4 | "title": "AIR v1 defsecret", 5 | "type": "object", 6 | "properties": { 7 | "$kind": { "const": "defsecret" }, 8 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 9 | "binding_id": { 10 | "type": "string", 11 | "description": "Opaque ID mapped to a backend in node-local resolver config" 12 | }, 13 | "expected_digest": { 14 | "$ref": "common.schema.json#/$defs/Hash", 15 | "description": "Optional hash of plaintext for drift detection" 16 | }, 17 | "allowed_caps": { 18 | "type": "array", 19 | "items": { "$ref": "common.schema.json#/$defs/CapGrantName" }, 20 | "description": "Capability grants that may use this secret" 21 | }, 22 | "allowed_plans": { 23 | "type": "array", 24 | "items": { "$ref": "common.schema.json#/$defs/Name" }, 25 | "description": "Plans that may use this secret" 26 | } 27 | }, 28 | "required": ["$kind", "name", "binding_id"], 29 | "additionalProperties": false 30 | } 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AgentOS 2 | 3 | Thank you for your interest in contributing! 4 | 5 | ## How to Contribute 6 | 1. Fork the repository and create a topic branch. 7 | 2. Make changes in line with the coding style and determinism rules 8 | described in the design docs (reducers, AIR, etc.). 9 | 3. Include tests or replay vectors for any change to kernel logic. 10 | 4. Submit a pull request with a clear description. 11 | 12 | All contributions are made under the Apache 2.0 license. 13 | By submitting a pull request you agree that your contribution may be 14 | distributed under that license. 15 | 16 | ## Developer Certificate of Origin (DCO) 17 | By contributing, you certify that: 18 | > (a) The contribution was created in whole or in part by you and you have 19 | > the right to submit it under the open-source license indicated; or 20 | > (b) The contribution is based upon previous work that, to the best of 21 | > your knowledge, is covered under an appropriate open-source license 22 | > and you have the right to submit it under the same license. 23 | 24 | Include a `Signed-off-by:` line in each commit message using 25 | `git commit -s`. 26 | 27 | -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/replay.rs: -------------------------------------------------------------------------------- 1 | //! `aos journal replay` command (experimental). 2 | //! 3 | //! Opens a world, replays journal + snapshot to head, and reports heights/state hashes. 4 | 5 | use anyhow::Result; 6 | use clap::Args; 7 | 8 | use crate::opts::{WorldOpts, resolve_dirs}; 9 | use crate::output::print_success; 10 | use crate::util::load_world_env; 11 | 12 | use super::{create_host, prepare_world}; 13 | 14 | #[derive(Args, Debug)] 15 | pub struct ReplayArgs {} 16 | 17 | pub async fn cmd_replay(opts: &WorldOpts, _args: &ReplayArgs) -> Result<()> { 18 | let dirs = resolve_dirs(opts)?; 19 | load_world_env(&dirs.world)?; 20 | 21 | let (store, loaded) = prepare_world(&dirs, opts)?; 22 | let mut host = create_host(store, loaded, &dirs, opts)?; 23 | 24 | // Replaying happens on open; run a drain to ensure idle. 25 | let _ = host.drain()?; 26 | 27 | let heights = host.heights(); 28 | print_success( 29 | opts, 30 | serde_json::json!({ 31 | "replay": "ok", 32 | "head": heights.head, 33 | "snapshot": heights.snapshot 34 | }), 35 | None, 36 | vec![], 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /crates/aos-host/src/modes/batch.rs: -------------------------------------------------------------------------------- 1 | use aos_store::Store; 2 | 3 | use crate::error::HostError; 4 | use crate::host::{CycleOutcome, ExternalEvent, RunMode, WorldHost}; 5 | 6 | pub struct BatchRunner { 7 | host: WorldHost, 8 | } 9 | 10 | impl BatchRunner { 11 | pub fn new(host: WorldHost) -> Self { 12 | Self { host } 13 | } 14 | 15 | pub async fn step(&mut self, events: Vec) -> Result { 16 | let events_injected = events.len(); 17 | for evt in events { 18 | self.host.enqueue_external(evt)?; 19 | } 20 | let cycle = self.host.run_cycle(RunMode::Batch).await?; 21 | self.host.snapshot()?; 22 | Ok(StepResult { 23 | cycle, 24 | events_injected, 25 | }) 26 | } 27 | 28 | pub fn host(&self) -> &WorldHost { 29 | &self.host 30 | } 31 | 32 | pub fn host_mut(&mut self) -> &mut WorldHost { 33 | &mut self.host 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy)] 38 | pub struct StepResult { 39 | pub cycle: CycleOutcome, 40 | pub events_injected: usize, 41 | } 42 | -------------------------------------------------------------------------------- /crates/aos-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-examples" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1" 8 | aos-air-types = { path = "../aos-air-types" } 9 | aos-cbor = { path = "../aos-cbor" } 10 | aos-kernel = { path = "../aos-kernel" } 11 | aos-store = { path = "../aos-store" } 12 | aos-air-exec = { path = "../aos-air-exec" } 13 | aos-wasm-abi = { path = "../aos-wasm-abi" } 14 | aos-wasm-sdk = { path = "../aos-wasm-sdk" } 15 | aos-effects = { path = "../aos-effects" } 16 | aos-host = { path = "../aos-host", features = ["test-fixtures"] } 17 | aos-wasm-build = { path = "../aos-wasm-build" } 18 | clap = { version = "4", features = ["derive"] } 19 | indexmap = "2" 20 | once_cell = "1" 21 | camino = "1" 22 | serde = { version = "1", features = ["derive"] } 23 | serde_cbor = "0.11" 24 | serde_json = "1" 25 | serde_bytes = "0.11" 26 | sha2 = "0.10" 27 | hex = "0.4" 28 | log = "0.4" 29 | env_logger = "0.11" 30 | walkdir = "2" 31 | tokio = { version = "1", features = ["rt", "time"] } 32 | 33 | [dev-dependencies] 34 | tempfile = "3" 35 | assert_cmd = "2" 36 | predicates = "3" 37 | 38 | [lints.rust] 39 | unused_imports = "allow" 40 | unused_variables = "allow" 41 | dead_code = "allow" 42 | -------------------------------------------------------------------------------- /examples/02-blob-echo/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/BlobEchoPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Putting": { "unit": {} }, 9 | "Getting": { "unit": {} }, 10 | "Done": { "unit": {} } 11 | } 12 | } 13 | }, 14 | { 15 | "$kind": "defschema", 16 | "name": "demo/BlobEchoState@1", 17 | "type": { 18 | "record": { 19 | "pc": { "ref": "demo/BlobEchoPc@1" }, 20 | "namespace": { "option": { "text": {} } }, 21 | "key": { "option": { "text": {} } }, 22 | "stored_blob_ref": { "option": { "text": {} } }, 23 | "retrieved_blob_ref": { "option": { "text": {} } } 24 | } 25 | } 26 | }, 27 | { 28 | "$kind": "defschema", 29 | "name": "demo/BlobEchoEvent@1", 30 | "type": { 31 | "variant": { 32 | "Start": { 33 | "record": { 34 | "namespace": { "text": {} }, 35 | "key": { "text": {} }, 36 | "data": { "bytes": {} } 37 | } 38 | }, 39 | "PutResult": { "ref": "sys/BlobPutResult@1" }, 40 | "GetResult": { "ref": "sys/BlobGetResult@1" } 41 | } 42 | } 43 | } 44 | ] 45 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Deterministic kernel entry points: load manifests, run reducers, emit intents. 2 | 3 | pub mod capability; 4 | pub mod cell_index; 5 | pub mod effects; 6 | pub mod error; 7 | pub mod event; 8 | pub mod governance; 9 | pub mod internal_effects; 10 | pub mod journal; 11 | pub mod manifest; 12 | pub mod patch_doc; 13 | pub mod plan; 14 | pub mod policy; 15 | pub mod query; 16 | pub mod receipts; 17 | pub mod reducer; 18 | pub mod scheduler; 19 | pub mod schema_value; 20 | pub mod secret; 21 | pub mod shadow; 22 | pub mod snapshot; 23 | pub mod world; 24 | 25 | pub use effects::{EffectManager, EffectQueue}; 26 | pub use error::KernelError; 27 | pub use event::{KernelEvent, ReducerEvent}; 28 | pub use manifest::{LoadedManifest, ManifestLoader}; 29 | pub use query::{Consistency, ReadMeta, StateRead, StateReader}; 30 | pub use reducer::ReducerRegistry; 31 | pub use secret::{ 32 | MapSecretResolver, PlaceholderSecretResolver, ResolvedSecret, SecretResolver, 33 | SecretResolverError, SharedSecretResolver, 34 | }; 35 | pub use shadow::{ShadowConfig, ShadowExecutor, ShadowSummary}; 36 | pub use world::{ 37 | DefListing, Kernel, KernelBuilder, KernelConfig, KernelHeights, PlanResultEntry, TailIntent, 38 | TailReceipt, TailScan, 39 | }; 40 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/schemas.rs: -------------------------------------------------------------------------------- 1 | //! Embedded AIR JSON Schema documents. Source of truth lives under `spec/schemas/`. 2 | 3 | pub const AIR_SPEC_VERSION: &str = "1.0"; 4 | 5 | macro_rules! embed_schema { 6 | ($($const:ident => $path:literal),+ $(,)?) => { 7 | $(pub const $const: &str = include_str!($path);)+ 8 | 9 | pub const ALL: &[SchemaDoc] = &[ 10 | $(SchemaDoc { name: stringify!($const), json: $const },)+ 11 | ]; 12 | }; 13 | } 14 | 15 | #[derive(Debug, Clone, Copy)] 16 | pub struct SchemaDoc { 17 | pub name: &'static str, 18 | pub json: &'static str, 19 | } 20 | 21 | embed_schema! { 22 | COMMON => "../../../spec/schemas/common.schema.json", 23 | DEFSCHEMA => "../../../spec/schemas/defschema.schema.json", 24 | DEFMODULE => "../../../spec/schemas/defmodule.schema.json", 25 | DEFPLAN => "../../../spec/schemas/defplan.schema.json", 26 | DEFCAP => "../../../spec/schemas/defcap.schema.json", 27 | DEFPOLICY => "../../../spec/schemas/defpolicy.schema.json", 28 | MANIFEST => "../../../spec/schemas/manifest.schema.json", 29 | PATCH => "../../../spec/schemas/patch.schema.json", 30 | } 31 | 32 | pub fn find(name: &str) -> Option<&'static str> { 33 | ALL.iter() 34 | .find(|doc| doc.name.eq_ignore_ascii_case(name)) 35 | .map(|doc| doc.json) 36 | } 37 | -------------------------------------------------------------------------------- /roadmap/v0.3-host/p0-kernel-prep.md: -------------------------------------------------------------------------------- 1 | # P0: Kernel Hooks for Host Recovery 2 | 3 | P1’s host/daemon needs a few kernel surfaces that don’t exist yet. Add these before or alongside P1 so restart safety and durable dispatch can be implemented without poking kernel internals: 4 | 5 | - [x] **Tail scan helper**: given the last snapshot height, return journal entries (intents and receipts) after that point. Needed to requeue intents that were recorded but not yet snapshotted and to build the “receipts seen” set for de-dupe. 6 | - [x] **Pending reducer receipt contexts**: expose the `pending_reducer_receipts` map (effect kind + params) so timer scheduling can be rebuilt on restart. 7 | - [x] **Queued effects snapshot**: accessor for current in-memory `queued_effects` (already serialized into snapshots) to hydrate the dispatch queue on open. 8 | - [x] **Plan pending receipts**: accessor for `pending_receipts` (plan_id + intent_hash) so hosts can avoid re-dispatching intents already awaited by plans. 9 | - [x] **Structured snapshot/tail heights**: ensure callers can get the latest snapshot height and the journal head to decide whether a tail scan is needed. 10 | - [x] **Journal head helper**: light-weight API to read current `next_seq` without loading the full log (used by control-server `journal-head`/health checks). 11 | 12 | These are kernel-only changes; the host will call them to implement the durable outbox/rehydration flow described in P1. 13 | -------------------------------------------------------------------------------- /spec/schemas/defcap.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defcap.schema.json", 4 | "title": "AIR v1 defcap", 5 | "description": "Capability type definition. Defines parameter constraints enforced at enqueue time. Standard v1 types: sys/http.out@1, sys/llm.basic@1, sys/blob@1, sys/timer@1, sys/query@1, and sys/secret@1.", 6 | "type": "object", 7 | "properties": { 8 | "$kind": { "const": "defcap" }, 9 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 10 | "cap_type": { 11 | "$ref": "common.schema.json#/$defs/CapType", 12 | "description": "Capability type identifier (namespaced string). Built-in v1 types: http.out, blob, timer, llm.basic, secret, query. Adapters may introduce additional types when supported by the runtime." 13 | }, 14 | "schema": { 15 | "$ref": "common.schema.json#/$defs/TypeExpr", 16 | "description": "Parameter schema defining allowlists and ceilings. Standard v1 schemas: sys/http.out@1 = { hosts: set, verbs: set, path_prefixes?: set }; sys/llm.basic@1 = { providers?: set, models?: set, max_tokens_max?: nat, temperature_max?: dec128, tools_allow?: set }; sys/blob@1 = { namespaces?: set }; sys/timer@1 = {}; sys/query@1 = { scope: text } (scope optional/opaque)." 17 | } 18 | }, 19 | "required": ["$kind","name","cap_type","schema"], 20 | "additionalProperties": false 21 | } 22 | -------------------------------------------------------------------------------- /examples/01-hello-timer/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/TimerPc@1" 6 | }, 7 | { 8 | "name": "demo/TimerState@1" 9 | }, 10 | { 11 | "name": "demo/TimerEvent@1" 12 | }, 13 | { 14 | "name": "sys/TimerSetParams@1" 15 | }, 16 | { 17 | "name": "sys/TimerSetReceipt@1" 18 | }, 19 | { 20 | "name": "sys/TimerFired@1" 21 | } 22 | ], 23 | "modules": [ 24 | { 25 | "name": "demo/TimerSM@1" 26 | } 27 | ], 28 | "plans": [], 29 | "caps": [ 30 | { 31 | "name": "sys/timer@1" 32 | } 33 | ], 34 | "policies": [ 35 | { 36 | "name": "demo/default_policy@1" 37 | } 38 | ], 39 | "defaults": { 40 | "policy": "demo/default_policy@1", 41 | "cap_grants": [ 42 | { 43 | "name": "timer_grant", 44 | "cap": "sys/timer@1", 45 | "params": { 46 | "record": {} 47 | } 48 | } 49 | ] 50 | }, 51 | "module_bindings": { 52 | "demo/TimerSM@1": { 53 | "slots": { 54 | "timer": "timer_grant" 55 | } 56 | } 57 | }, 58 | "routing": { 59 | "events": [ 60 | { 61 | "event": "demo/TimerEvent@1", 62 | "reducer": "demo/TimerSM@1" 63 | } 64 | ], 65 | "inboxes": [] 66 | }, 67 | "triggers": [], 68 | "air_version": "1", 69 | "effects": [ 70 | { 71 | "name": "sys/timer.set@1" 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /crates/aos-effects/src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::{CapabilityGrant, EffectIntent, EffectReceipt, EffectSource, ReceiptStatus}; 2 | 3 | /// Result of a policy decision. 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 5 | pub enum PolicyDecision { 6 | Allow, 7 | Deny, 8 | } 9 | 10 | /// Capability gate resolves and validates capability grants before dispatch. 11 | pub trait CapabilityGate { 12 | type Error; 13 | 14 | fn resolve(&self, cap_name: &str, effect_kind: &str) -> Result; 15 | fn check_constraints( 16 | &self, 17 | intent: &EffectIntent, 18 | grant: &CapabilityGrant, 19 | ) -> Result<(), Self::Error>; 20 | } 21 | 22 | /// Policy gate evaluates origin/effect metadata for allow/deny flows. 23 | pub trait PolicyGate { 24 | type Error; 25 | 26 | fn decide( 27 | &self, 28 | intent: &EffectIntent, 29 | grant: &CapabilityGrant, 30 | source: &EffectSource, 31 | ) -> Result; 32 | } 33 | 34 | /// Adapter trait executed by the effect manager; async runtimes can wrap it as needed. 35 | pub trait EffectAdapter { 36 | type Error; 37 | 38 | fn kind(&self) -> &str; 39 | fn execute(&self, intent: &EffectIntent) -> Result; 40 | } 41 | 42 | /// Helper describing the desired receipt status for waiting plans. 43 | #[derive(Debug, Clone, PartialEq, Eq)] 44 | pub struct ReceiptExpectation { 45 | pub accept: ReceiptStatus, 46 | } 47 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/FetchNotifyPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Fetching": { "unit": {} }, 9 | "Done": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/FetchNotifyState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/FetchNotifyPc@1" }, 19 | "next_request_id": { "nat": {} }, 20 | "pending_request": { "option": { "nat": {} } }, 21 | "last_status": { "option": { "int": {} } }, 22 | "last_body_ref": { "option": { "hash": {} } } 23 | } 24 | } 25 | }, 26 | { 27 | "$kind": "defschema", 28 | "name": "demo/FetchNotifyEvent@1", 29 | "type": { 30 | "variant": { 31 | "Start": { 32 | "record": { 33 | "url": { "text": {} }, 34 | "method": { "text": {} } 35 | } 36 | }, 37 | "NotifyComplete": { 38 | "record": { 39 | "status": { "int": {} }, 40 | "body_preview": { "text": {} } 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | { 47 | "$kind": "defschema", 48 | "name": "demo/FetchRequest@1", 49 | "type": { 50 | "record": { 51 | "request_id": { "nat": {} }, 52 | "url": { "text": {} }, 53 | "method": { "text": {} } 54 | } 55 | } 56 | } 57 | ] 58 | -------------------------------------------------------------------------------- /spec/schemas/defeffect.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defeffect.schema.json", 4 | "title": "AIR v1 defeffect", 5 | "description": "Effect kind definition. Declares an effect's param/receipt schemas and required capability type.", 6 | "type": "object", 7 | "properties": { 8 | "$kind": { "const": "defeffect" }, 9 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 10 | "kind": { 11 | "$ref": "common.schema.json#/$defs/EffectKind", 12 | "description": "The effect kind string used in emit_effect steps" 13 | }, 14 | "params_schema": { 15 | "$ref": "common.schema.json#/$defs/SchemaRef", 16 | "description": "Schema for effect parameters" 17 | }, 18 | "receipt_schema": { 19 | "$ref": "common.schema.json#/$defs/SchemaRef", 20 | "description": "Schema for effect receipts" 21 | }, 22 | "cap_type": { 23 | "$ref": "common.schema.json#/$defs/CapType", 24 | "description": "Capability type that guards this effect" 25 | }, 26 | "origin_scope": { 27 | "type": "string", 28 | "enum": ["reducer", "plan", "both"], 29 | "description": "Which emitters may use this effect in v1" 30 | }, 31 | "description": { 32 | "type": "string", 33 | "description": "Optional human-readable description" 34 | } 35 | }, 36 | "required": ["$kind", "name", "kind", "params_schema", "receipt_schema", "cap_type", "origin_scope"], 37 | "additionalProperties": false 38 | } 39 | -------------------------------------------------------------------------------- /crates/aos-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Shared types for system reducers (`sys/*`). 2 | //! 3 | //! This crate provides common data structures used by built-in system reducers 4 | //! like `sys/ObjectCatalog@1`. The types mirror the schemas in 5 | //! `spec/defs/builtin-schemas.air.json`. 6 | 7 | #![no_std] 8 | 9 | extern crate alloc; 10 | 11 | use alloc::collections::{BTreeMap, BTreeSet}; 12 | use alloc::string::String; 13 | use serde::{Deserialize, Serialize}; 14 | 15 | // --------------------------------------------------------------------------- 16 | // ObjectCatalog types (sys/ObjectCatalog@1) 17 | // --------------------------------------------------------------------------- 18 | 19 | /// Version counter for catalog entries. 20 | pub type Version = u64; 21 | 22 | /// Metadata describing a single object version (`sys/ObjectMeta@1`). 23 | #[derive(Debug, Clone, Serialize, Deserialize)] 24 | pub struct ObjectMeta { 25 | pub name: String, 26 | pub kind: String, 27 | pub hash: String, 28 | pub tags: BTreeSet, 29 | pub created_at: u64, 30 | pub owner: String, 31 | } 32 | 33 | /// Reducer state: append-only versions per object name (`sys/ObjectVersions@1`). 34 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 35 | pub struct ObjectVersions { 36 | pub latest: Version, 37 | pub versions: BTreeMap, 38 | } 39 | 40 | /// Event to register an object in the catalog (`sys/ObjectRegistered@1`). 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | pub struct ObjectRegistered { 43 | pub meta: ObjectMeta, 44 | } 45 | -------------------------------------------------------------------------------- /crates/aos-host/tests/host_smoke.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Write; 3 | use std::sync::Arc; 4 | 5 | use aos_host::testhost::TestHost; 6 | use aos_store::MemStore; 7 | use serde_json::json; 8 | use tempfile::TempDir; 9 | 10 | fn write_minimal_manifest(path: &std::path::Path) { 11 | let manifest = json!({ 12 | "air_version": "1", 13 | "schemas": [], 14 | "modules": [], 15 | "plans": [], 16 | "effects": [], 17 | "caps": [], 18 | "policies": [], 19 | "triggers": [] 20 | }); 21 | let bytes = serde_cbor::to_vec(&manifest).expect("cbor encode"); 22 | let mut file = File::create(path).expect("create manifest"); 23 | file.write_all(&bytes).expect("write manifest"); 24 | } 25 | 26 | #[tokio::test] 27 | async fn reopen_and_replay_smoke() { 28 | let tmp = TempDir::new().unwrap(); 29 | let manifest_path = tmp.path().join("manifest.cbor"); 30 | write_minimal_manifest(&manifest_path); 31 | 32 | let store = Arc::new(MemStore::new()); 33 | 34 | // First open: send an event, run cycle, snapshot. 35 | { 36 | let mut host = TestHost::open(store.clone(), &manifest_path).unwrap(); 37 | host.send_event("demo/Event@1", json!({"n": 1})).unwrap(); 38 | host.run_cycle_batch().await.unwrap(); 39 | host.snapshot().unwrap(); 40 | } 41 | 42 | // Reopen and ensure we can run another idle cycle without errors. 43 | { 44 | let mut host = TestHost::open(store.clone(), &manifest_path).unwrap(); 45 | host.run_cycle_batch().await.unwrap(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE-SPEC: -------------------------------------------------------------------------------- 1 | AgentOS AIR and Schema Specifications 2 | ===================================== 3 | 4 | Copyright (c) 2025 Smart Computer Co. and the AgentOS contributors 5 | 6 | Textual specifications, schemas, and diagrams describing the AIR format, 7 | schemas, and manifest semantics are licensed under the 8 | Creative Commons Attribution 4.0 International License (CC BY 4.0). 9 | 10 | You are free to share and adapt this material for any purpose, even 11 | commercially, provided you give appropriate credit, provide a link to 12 | the license, and indicate if changes were made. 13 | See: https://creativecommons.org/licenses/by/4.0/ 14 | 15 | Patent Non-Assert 16 | ----------------- 17 | To promote open implementations, each contributor to the AIR 18 | specification and its schemas grants a perpetual, worldwide, 19 | royalty-free, non-exclusive license to any patent claims that are 20 | necessarily infringed by implementing the normative portions of 21 | these specifications. 22 | 23 | This patent license applies only to implementations of the AIR 24 | specification as defined here; it does not grant rights for any 25 | other use of a contributor’s patents. 26 | 27 | If a contributor initiates or participates in a claim alleging that 28 | an implementation of this specification infringes a patent it owns 29 | or controls, this license and non-assert from that contributor 30 | terminate as of the date the claim is filed. 31 | 32 | The AgentOS name and logo are trademarks of the AgentOS project and 33 | may not be used to imply endorsement or certification except as 34 | permitted under published trademark guidelines. 35 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/README.md: -------------------------------------------------------------------------------- 1 | # Example 09 — WorldFS Lab (Notes + Catalog) 2 | 3 | A keyed notebook reducer plus ObjectCatalog and a small plan. Finalized notes trigger a plan that writes a report blob and registers it in the catalog, originally meant to be explored via the `aos world fs` CLI. 4 | 5 | ## What it does 6 | - Keyed reducer `notes/NotebookSM@1` owns one note per key. 7 | - Runner sends `notes/NoteEvent@1` variants (Start/Append/Finalize) to seed notes. 8 | - `NoteFinalized` emits `SnapshotRequested`; plan `notes/SnapshotPlan@1` does: 9 | 1. `blob.put` the report (namespace = note_id). 10 | 2. Raise `sys/ObjectRegistered@1` (object `notes//report`, kind `note.report`, tags `report,worldfs`). 11 | 3. Raise `NoteArchived` to close the cell. 12 | - Runner seeds two notes (alpha, beta), drives blob receipts, and verifies replay. 13 | 14 | ## Run it 15 | ``` 16 | cargo run -p aos-examples -- worldfs-lab 17 | ``` 18 | 19 | If you change schemas/manifests or rerun after a code edit, wipe any stale journal/store first (old entries won’t match new schemas): 20 | ``` 21 | rm -rf .aos 22 | ``` 23 | 24 | ## Note on CLI 25 | The experimental `aos world fs` CLI has been removed. The example still builds and runs, but there is currently no supported CLI wrapper to browse the catalog or blobs. You can inspect the journal/store directly or wait for the upcoming replacement commands (object and blob readers) to land. 26 | 27 | ## Layout 28 | ``` 29 | examples/09-worldfs-lab/ 30 | air/ # schemas, manifest, plan, caps, policies 31 | reducer/ # Notebook reducer (wasm built via aos-wasm-build) 32 | README.md 33 | ``` 34 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::schemas::COMMON; 2 | use jsonschema::{JSONSchema, paths::JSONPointer}; 3 | use once_cell::sync::Lazy; 4 | use serde_json::Value; 5 | 6 | pub mod caps; 7 | pub mod effects; 8 | pub mod manifest; 9 | pub mod modules; 10 | pub mod patch; 11 | pub mod plans; 12 | pub mod policies; 13 | pub mod schemas; 14 | 15 | static COMMON_SCHEMA: Lazy = 16 | Lazy::new(|| serde_json::from_str(COMMON).expect("embedded common schema must be valid JSON")); 17 | 18 | pub(crate) fn assert_json_schema(schema_json: &str, instance: &Value) { 19 | let schema_value: Value = 20 | serde_json::from_str(schema_json).expect("embedded schema must be valid JSON"); 21 | let mut options = JSONSchema::options(); 22 | for id in [ 23 | "common.schema.json", 24 | "https://aos.dev/air/v1/common.schema.json", 25 | ] { 26 | options.with_document(id.to_string(), COMMON_SCHEMA.clone()); 27 | } 28 | let compiled = options 29 | .compile(&schema_value) 30 | .expect("embedded schema must compile successfully"); 31 | if let Err(errors) = compiled.validate(instance) { 32 | let mut messages = Vec::new(); 33 | for err in errors { 34 | messages.push(format!("{}: {}", format_pointer(&err.instance_path), err)); 35 | } 36 | panic!( 37 | "schema validation failed: {}\ninstance: {}", 38 | messages.join("; "), 39 | instance 40 | ); 41 | } 42 | } 43 | 44 | fn format_pointer(pointer: &JSONPointer) -> String { 45 | let text = pointer.to_string(); 46 | if text.is_empty() { "/".into() } else { text } 47 | } 48 | -------------------------------------------------------------------------------- /crates/aos-cli/src/input.rs: -------------------------------------------------------------------------------- 1 | //! Input parsing utilities for @file and @- syntax. 2 | 3 | use std::io::Read; 4 | 5 | use anyhow::{Context, Result}; 6 | 7 | /// Parse an input value that may be a JSON literal, @file, or @- for stdin. 8 | /// 9 | /// - `@-` reads from stdin 10 | /// - `@path` reads from the specified file 11 | /// - Otherwise, returns the value as-is (assumed to be JSON literal) 12 | pub fn parse_input_value(value: &str) -> Result { 13 | if value == "@-" { 14 | let mut buf = String::new(); 15 | std::io::stdin() 16 | .read_to_string(&mut buf) 17 | .context("failed to read from stdin")?; 18 | Ok(buf) 19 | } else if let Some(path) = value.strip_prefix('@') { 20 | std::fs::read_to_string(path).with_context(|| format!("failed to read file: {}", path)) 21 | } else { 22 | Ok(value.to_string()) 23 | } 24 | } 25 | 26 | /// Parse input as raw bytes. 27 | /// 28 | /// - `@-` reads from stdin 29 | /// - `@path` reads from the specified file 30 | /// - Otherwise, returns an error (literal data not supported for binary) 31 | #[allow(dead_code)] 32 | pub fn parse_input_bytes(value: &str) -> Result> { 33 | if value == "@-" { 34 | let mut buf = Vec::new(); 35 | std::io::stdin() 36 | .read_to_end(&mut buf) 37 | .context("failed to read from stdin")?; 38 | Ok(buf) 39 | } else if let Some(path) = value.strip_prefix('@') { 40 | std::fs::read(path).with_context(|| format!("failed to read file: {}", path)) 41 | } else { 42 | anyhow::bail!("expected @file or @- for binary input, not literal data") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/SafeUpgradePc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Fetching": { "unit": {} }, 9 | "Completed": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/SafeUpgradeState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/SafeUpgradePc@1" }, 19 | "next_request_id": { "nat": {} }, 20 | "pending_request": { "option": { "nat": {} } }, 21 | "primary_status": { "option": { "int": {} } }, 22 | "follow_status": { "option": { "int": {} } }, 23 | "requests_observed": { "nat": {} } 24 | } 25 | } 26 | }, 27 | { 28 | "$kind": "defschema", 29 | "name": "demo/SafeUpgradeEvent@1", 30 | "type": { 31 | "variant": { 32 | "Start": { "record": { "url": { "text": {} } } }, 33 | "NotifyComplete": { 34 | "record": { 35 | "primary_status": { "int": {} }, 36 | "follow_status": { "int": {} }, 37 | "request_count": { "nat": {} } 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | { 44 | "$kind": "defschema", 45 | "name": "demo/UpgradeFetchRequest@1", 46 | "type": { "record": { "request_id": { "nat": {} }, "url": { "text": {} } } } 47 | }, 48 | { 49 | "$kind": "defschema", 50 | "name": "demo/SafeUpgradeResult@1", 51 | "type": { 52 | "record": { 53 | "primary_status": { "int": {} }, 54 | "follow_status": { "int": {} } 55 | } 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v2/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/SafeUpgradePc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Fetching": { "unit": {} }, 9 | "Completed": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/SafeUpgradeState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/SafeUpgradePc@1" }, 19 | "next_request_id": { "nat": {} }, 20 | "pending_request": { "option": { "nat": {} } }, 21 | "primary_status": { "option": { "int": {} } }, 22 | "follow_status": { "option": { "int": {} } }, 23 | "requests_observed": { "nat": {} } 24 | } 25 | } 26 | }, 27 | { 28 | "$kind": "defschema", 29 | "name": "demo/SafeUpgradeEvent@1", 30 | "type": { 31 | "variant": { 32 | "Start": { "record": { "url": { "text": {} } } }, 33 | "NotifyComplete": { 34 | "record": { 35 | "primary_status": { "int": {} }, 36 | "follow_status": { "int": {} }, 37 | "request_count": { "nat": {} } 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | { 44 | "$kind": "defschema", 45 | "name": "demo/UpgradeFetchRequest@1", 46 | "type": { "record": { "request_id": { "nat": {} }, "url": { "text": {} } } } 47 | }, 48 | { 49 | "$kind": "defschema", 50 | "name": "demo/SafeUpgradeResult@1", 51 | "type": { 52 | "record": { 53 | "primary_status": { "int": {} }, 54 | "follow_status": { "int": {} } 55 | } 56 | } 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /crates/aos-cli/tests/blob_cli.rs: -------------------------------------------------------------------------------- 1 | use aos_store::{FsStore, Store}; 2 | use assert_cmd::prelude::*; 3 | use std::fs; 4 | use tempfile::TempDir; 5 | 6 | #[test] 7 | fn blob_get_defaults_to_metadata_without_raw_or_out() { 8 | let tmp = TempDir::new().expect("tmpdir"); 9 | let world = tmp.path(); 10 | fs::create_dir_all(world.join(".aos")).unwrap(); 11 | 12 | // Seed a blob in the store. 13 | let store = FsStore::open(world).expect("store"); 14 | let data = b"hello-bytes"; 15 | let hash = store.put_blob(data).expect("put blob"); 16 | 17 | // Run CLI without --raw/--out; expect metadata + warning, no raw bytes. 18 | let assert = std::process::Command::new(assert_cmd::cargo::cargo_bin!("aos")) 19 | .current_dir(world) 20 | .args([ 21 | "--world", 22 | world.to_str().unwrap(), 23 | "blob", 24 | "get", 25 | &hash.to_hex(), 26 | "--json", 27 | ]) 28 | .assert() 29 | .success(); 30 | 31 | let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); 32 | let json: serde_json::Value = serde_json::from_str(&output).expect("json"); 33 | 34 | assert_eq!(json["data"]["bytes"].as_u64(), Some(data.len() as u64)); 35 | assert!( 36 | json["data"]["hint"] 37 | .as_str() 38 | .unwrap_or_default() 39 | .contains("--raw") 40 | ); 41 | let warnings = json["warnings"].as_array().cloned().unwrap_or_default(); 42 | assert!( 43 | warnings 44 | .iter() 45 | .any(|w| w.as_str().unwrap_or_default().contains("metadata-only")), 46 | "expected metadata-only warning" 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | use sha2::{Digest, Sha256}; 2 | use std::fs; 3 | use std::path::{Path, PathBuf}; 4 | 5 | const CACHE_DIR: &str = ".aos/cache/modules"; 6 | 7 | pub fn cache_root() -> PathBuf { 8 | resolve_root(None) 9 | } 10 | 11 | fn resolve_root(override_dir: Option<&Path>) -> PathBuf { 12 | if let Some(dir) = override_dir { 13 | return dir.to_path_buf(); 14 | } 15 | if let Ok(dir) = std::env::var("AOS_WASM_CACHE_DIR") { 16 | return PathBuf::from(dir); 17 | } 18 | let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 19 | manifest 20 | .parent() 21 | .and_then(|p| p.parent()) 22 | .map(|p| p.join(CACHE_DIR)) 23 | .unwrap_or_else(|| PathBuf::from(CACHE_DIR)) 24 | } 25 | 26 | pub fn fingerprint(inputs: &[(&str, String)]) -> String { 27 | let mut hasher = Sha256::new(); 28 | for (k, v) in inputs { 29 | hasher.update(k.as_bytes()); 30 | hasher.update(b"="); 31 | hasher.update(v.as_bytes()); 32 | hasher.update(b"\n"); 33 | } 34 | hex::encode(hasher.finalize()) 35 | } 36 | 37 | pub fn lookup(fingerprint: &str, override_dir: Option<&Path>) -> std::io::Result>> { 38 | let path = resolve_root(override_dir) 39 | .join(fingerprint) 40 | .join("artifact.wasm"); 41 | if path.exists() { 42 | Ok(Some(fs::read(path)?)) 43 | } else { 44 | Ok(None) 45 | } 46 | } 47 | 48 | pub fn store(fingerprint: &str, bytes: &[u8], override_dir: Option<&Path>) -> std::io::Result<()> { 49 | let dir = resolve_root(override_dir).join(fingerprint); 50 | fs::create_dir_all(&dir)?; 51 | fs::write(dir.join("artifact.wasm"), bytes)?; 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/init.rs: -------------------------------------------------------------------------------- 1 | //! `aos world init` command. 2 | 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | use anyhow::Result; 7 | use clap::Args; 8 | 9 | #[derive(Args, Debug)] 10 | pub struct InitArgs { 11 | /// Path to create world (defaults to current directory) 12 | #[arg(default_value = ".")] 13 | pub path: PathBuf, 14 | 15 | /// Template to use (counter, http, llm-chat) 16 | #[arg(long)] 17 | pub template: Option, 18 | } 19 | 20 | pub fn cmd_init(args: &InitArgs) -> Result<()> { 21 | let path = &args.path; 22 | 23 | fs::create_dir_all(path)?; 24 | fs::create_dir_all(path.join(".aos"))?; 25 | fs::create_dir_all(path.join("air"))?; 26 | fs::create_dir_all(path.join("modules"))?; 27 | fs::create_dir_all(path.join("reducer/src"))?; 28 | 29 | // Write minimal manifest 30 | let manifest = r#"{ 31 | "$kind": "manifest", 32 | "air_version": "1", 33 | "schemas": [], 34 | "modules": [], 35 | "plans": [], 36 | "caps": [], 37 | "policies": [], 38 | "effects": [], 39 | "triggers": [] 40 | }"#; 41 | fs::write(path.join("air/manifest.air.json"), manifest)?; 42 | 43 | // TODO: Support --template to scaffold different starter manifests 44 | 45 | println!("World initialized at {}", path.display()); 46 | println!(" AIR assets: {}", path.join("air").display()); 47 | println!(" Reducer: {}", path.join("reducer").display()); 48 | println!(" Modules: {}", path.join("modules").display()); 49 | println!(" Store: {}", path.join(".aos").display()); 50 | 51 | if args.template.is_some() { 52 | println!("\nNote: --template is not yet implemented; created minimal manifest."); 53 | } 54 | 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /examples/02-blob-echo/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/BlobEchoPc@1" 6 | }, 7 | { 8 | "name": "demo/BlobEchoState@1" 9 | }, 10 | { 11 | "name": "demo/BlobEchoEvent@1" 12 | }, 13 | { 14 | "name": "sys/BlobPutParams@1" 15 | }, 16 | { 17 | "name": "sys/BlobPutReceipt@1" 18 | }, 19 | { 20 | "name": "sys/BlobGetParams@1" 21 | }, 22 | { 23 | "name": "sys/BlobGetReceipt@1" 24 | }, 25 | { 26 | "name": "sys/BlobPutResult@1" 27 | }, 28 | { 29 | "name": "sys/BlobGetResult@1" 30 | } 31 | ], 32 | "modules": [ 33 | { 34 | "name": "demo/BlobEchoSM@1" 35 | } 36 | ], 37 | "plans": [], 38 | "caps": [ 39 | { 40 | "name": "sys/blob@1" 41 | } 42 | ], 43 | "policies": [ 44 | { 45 | "name": "demo/blob_policy@1" 46 | } 47 | ], 48 | "defaults": { 49 | "policy": "demo/blob_policy@1", 50 | "cap_grants": [ 51 | { 52 | "name": "blob_grant", 53 | "cap": "sys/blob@1", 54 | "params": { 55 | "record": {} 56 | } 57 | } 58 | ] 59 | }, 60 | "module_bindings": { 61 | "demo/BlobEchoSM@1": { 62 | "slots": { 63 | "blob": "blob_grant" 64 | } 65 | } 66 | }, 67 | "routing": { 68 | "events": [ 69 | { 70 | "event": "demo/BlobEchoEvent@1", 71 | "reducer": "demo/BlobEchoSM@1" 72 | } 73 | ], 74 | "inboxes": [] 75 | }, 76 | "triggers": [], 77 | "air_version": "1", 78 | "effects": [ 79 | { 80 | "name": "sys/blob.put@1" 81 | }, 82 | { 83 | "name": "sys/blob.get@1" 84 | } 85 | ] 86 | } 87 | -------------------------------------------------------------------------------- /crates/aos-host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aos-host" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [features] 7 | # Real effect adapters are feature-gated so tests and consumers can opt-out of 8 | # networked dependencies. They are ON by default. 9 | default = ["adapter-http", "adapter-llm"] 10 | adapter-http = ["reqwest", "url"] 11 | adapter-llm = ["reqwest"] 12 | test-fixtures = ["aos-air-exec", "aos-wasm-abi", "indexmap", "wat", "sha2"] 13 | 14 | [dependencies] 15 | aos-kernel = { path = "../aos-kernel" } 16 | aos-effects = { path = "../aos-effects" } 17 | aos-cbor = { path = "../aos-cbor" } 18 | aos-store = { path = "../aos-store" } 19 | aos-air-types = { path = "../aos-air-types" } 20 | 21 | walkdir = "2" 22 | log = "0.4" 23 | 24 | # Optional dependencies for test-fixtures feature 25 | aos-air-exec = { path = "../aos-air-exec", optional = true } 26 | aos-wasm-abi = { path = "../aos-wasm-abi", optional = true } 27 | indexmap = { version = "2", optional = true } 28 | wat = { version = "1", optional = true } 29 | sha2 = { version = "0.10", optional = true } 30 | hex = "0.4" 31 | 32 | tokio = { version = "1", features = ["full"] } 33 | async-trait = "0.1" 34 | anyhow = "1" 35 | thiserror = "1" 36 | tracing = "0.1" 37 | base64 = "0.21" 38 | clap = { version = "4", features = ["derive"] } 39 | serde = { version = "1", features = ["derive"] } 40 | serde_json = "1" 41 | serde_cbor = "0.11" 42 | tempfile = "3" 43 | reqwest = { version = "0.11", features = ["json"], optional = true } 44 | url = { version = "2", optional = true } 45 | once_cell = "1" 46 | jsonschema = "0.17" 47 | 48 | [dev-dependencies] 49 | aos-air-exec = { path = "../aos-air-exec" } 50 | aos-wasm-abi = { path = "../aos-wasm-abi" } 51 | indexmap = "2" 52 | wat = "1" 53 | sha2 = "0.10" 54 | hex = "0.4" 55 | -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/snapshot.rs: -------------------------------------------------------------------------------- 1 | //! `aos snapshot create` command. 2 | 3 | use anyhow::Result; 4 | 5 | use crate::opts::{Mode, WorldOpts, resolve_dirs}; 6 | use crate::output::print_success; 7 | use crate::util::load_world_env; 8 | 9 | use super::{create_host, prepare_world, should_use_control, try_control_client}; 10 | 11 | pub async fn cmd_snapshot(opts: &WorldOpts) -> Result<()> { 12 | let dirs = resolve_dirs(opts)?; 13 | 14 | // Try daemon first 15 | if should_use_control(opts) { 16 | if let Some(mut client) = try_control_client(&dirs).await { 17 | let resp = client.snapshot("cli-snapshot").await?; 18 | if !resp.ok { 19 | anyhow::bail!("snapshot failed: {:?}", resp.error); 20 | } 21 | return print_success( 22 | opts, 23 | serde_json::json!({ "snapshot": "created" }), 24 | None, 25 | vec![], 26 | ); 27 | } else if matches!(opts.mode, Mode::Daemon) { 28 | anyhow::bail!( 29 | "daemon mode requested but no control socket at {}", 30 | dirs.control_socket.display() 31 | ); 32 | } else if !opts.quiet { 33 | // fall through to batch 34 | } 35 | } 36 | 37 | // Fall back to batch mode 38 | load_world_env(&dirs.world)?; 39 | let (store, loaded) = prepare_world(&dirs, opts)?; 40 | let mut host = create_host(store, loaded, &dirs, opts)?; 41 | host.snapshot()?; 42 | print_success( 43 | opts, 44 | serde_json::json!({ "snapshot": "created" }), 45 | None, 46 | if opts.quiet { 47 | vec![] 48 | } else { 49 | vec!["daemon unavailable; created snapshot in batch mode".into()] 50 | }, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/StartWork@1", 5 | "type": { 6 | "record": { 7 | "req_id": { "text": {} }, 8 | "payload": { "text": {} }, 9 | "max_attempts": { "nat": {} }, 10 | "base_delay_ms": { "nat": {} }, 11 | "now_ns": { "nat": {} } 12 | } 13 | } 14 | }, 15 | { 16 | "$kind": "defschema", 17 | "name": "demo/WorkRequested@1", 18 | "type": { 19 | "record": { 20 | "req_id": { "text": {} }, 21 | "payload": { "text": {} } 22 | } 23 | } 24 | }, 25 | { 26 | "$kind": "defschema", 27 | "name": "demo/WorkOk@1", 28 | "type": { 29 | "record": { 30 | "req_id": { "text": {} } 31 | } 32 | } 33 | }, 34 | { 35 | "$kind": "defschema", 36 | "name": "demo/WorkErr@1", 37 | "type": { 38 | "record": { 39 | "req_id": { "text": {} }, 40 | "transient": { "bool": {} }, 41 | "message": { "text": {} } 42 | } 43 | } 44 | }, 45 | { 46 | "$kind": "defschema", 47 | "name": "demo/RetryState@1", 48 | "type": { 49 | "record": { 50 | "pc": { "text": {} }, 51 | "attempt": { "nat": {} }, 52 | "max_attempts": { "nat": {} }, 53 | "base_delay_ms": { "nat": {} }, 54 | "anchor_ns": { "nat": {} }, 55 | "payload": { "text": {} }, 56 | "req_id": { "text": {} } 57 | } 58 | } 59 | }, 60 | { 61 | "$kind": "defschema", 62 | "name": "demo/RetryEvent@1", 63 | "type": { 64 | "variant": { 65 | "start": { "ref": "demo/StartWork@1" }, 66 | "ok": { "ref": "demo/WorkOk@1" }, 67 | "err": { "ref": "demo/WorkErr@1" }, 68 | "timer": { "ref": "sys/TimerFired@1" } 69 | } 70 | } 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /roadmap/v0.3-host/p5-tests-and-hardening.md: -------------------------------------------------------------------------------- 1 | # P5: Tests & Hardening 2 | 3 | **Goal (trimmed):** Finish a practical host-first test story and add replay tooling, without overbuilding. Focus on TestHost parity, example coverage, a replay command, and a small snapshot safety check. 4 | 5 | ## Focus Areas 6 | 7 | - TestHost parity for near-term needs (matches daemon/batch loops). 8 | - Examples + CLI smoke use TestHost with mocked effects (no network). 9 | - Replay command for manual/CI experiments (no doc/CI wiring yet). 10 | - Snapshot boundary sanity check at the WorldHost layer (small, high-signal). 11 | 12 | ## Tasks 13 | 14 | 1) **TestHost affordances (right-now scope).** ✅ Added helpers: `run_to_idle`, `run_cycle_with_timers` (daemon-style), `drain_and_dispatch`, `apply_receipt`, `state_json`; kept API tight and skipped pending_effects/record/replay plumbing. 15 | 2) **Examples through TestHost.** ✅ Counter, hello-timer, blob-echo, fetch-notify, aggregator, chain-comp, retry-backoff, safe-upgrade, llm-summarizer now run through `ExampleHost` (TestHost-based) with mock adapters; CLI smoke remains opt-in and still mock-only. 16 | 3) **Replay command.** ✅ `aos world replay` added to `aos-cli` (experimental; no CONTRIBUTING/CI wiring). 17 | 4) **Snapshot safety (lightweight).** ✅ WorldHost-level test added (`crates/aos-host/tests/host_snapshot.rs`) to ensure queued intents persist across snapshot/reopen. 18 | 19 | ## Explicitly deferred for now 20 | 21 | - Recording/replay fixtures or adapters. 22 | - Host-level allowlists/size/model limits (handled by caps/policies instead). 23 | - CI wiring or docs for replay beyond the new command. 24 | 25 | ## Success Criteria 26 | 27 | - TestHost helpers in place and exercised by new integration tests. 28 | - Example flows run under TestHost + mock adapters; CLI smoke remains network-free. 29 | - `aos world replay` works locally for manual runs. 30 | - Snapshot safety test passes (or is consciously deferred). 31 | -------------------------------------------------------------------------------- /spec/schemas/defmodule.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defmodule.schema.json", 4 | "title": "AIR v1 defmodule", 5 | "type": "object", 6 | "properties": { 7 | "$kind": { "const": "defmodule" }, 8 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 9 | "module_kind": { "type": "string", "enum": ["reducer"] }, 10 | "wasm_hash": { "$ref": "common.schema.json#/$defs/Hash" }, 11 | "key_schema": { "$ref": "common.schema.json#/$defs/SchemaRef" }, 12 | "abi": { 13 | "type": "object", 14 | "properties": { 15 | "reducer": { 16 | "type": "object", 17 | "properties": { 18 | "state": { "$ref": "common.schema.json#/$defs/SchemaRef" }, 19 | "event": { "$ref": "common.schema.json#/$defs/SchemaRef" }, 20 | "annotations": { "$ref": "common.schema.json#/$defs/SchemaRef" }, 21 | "effects_emitted": { 22 | "type": "array", 23 | "items": { "$ref": "common.schema.json#/$defs/EffectKind" } 24 | }, 25 | "cap_slots": { 26 | "type": "object", 27 | "description": "slot_name → cap_type", 28 | "patternProperties": { 29 | "^[A-Za-z_][A-Za-z0-9_.-]{0,63}$": { "$ref": "common.schema.json#/$defs/CapType" } 30 | }, 31 | "additionalProperties": false 32 | } 33 | }, 34 | "required": ["state","event"], 35 | "additionalProperties": false 36 | } 37 | }, 38 | "additionalProperties": false 39 | } 40 | }, 41 | "required": ["$kind","name","module_kind","wasm_hash","abi"], 42 | "allOf": [ 43 | { "if": { "properties": { "module_kind": { "const": "reducer" } } }, 44 | "then": { "required": ["abi"], "properties": { "abi": { "required": ["reducer"] } } } } 45 | ], 46 | "additionalProperties": false 47 | } 48 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # AOS Examples Ladder 2 | 3 | These demos are a rung-by-rung ladder from a deterministic core to a self-upgrading world. Each example is a tiny, runnable integration test that forces the path the architecture cares about—deterministic replay, micro-effects, plan orchestration, governance, and finally LLM with budgets. The north star is a world an agent can safely modify via the constitutional loop (propose → shadow → approve → apply) while staying on the same deterministic journal. 4 | 5 | What the ladder proves (in order): 6 | - Deterministic reducer execution and replay on a journal. 7 | - Reducers emitting one micro-effect (timer/blob) and handling receipts. 8 | - Single-plan orchestration with HTTP and typed reducer boundaries. 9 | - Deterministic fan-out/fan-in inside a plan. 10 | - Multi-plan choreography and reducer-driven compensations. 11 | - Governance loop with shadowed diffs and manifest swaps. 12 | - Plans invoking LLM with capability budgets and policy gates, nd secrets injection for llm based effects (e.g. API keys). 13 | 14 | | No. | Slug | Summary | 15 | | --- | ------------- | -------------------------------- | 16 | | 00 | counter | Deterministic reducer SM | 17 | | 01 | hello-timer | Reducer micro-effect demo | 18 | | 02 | blob-echo | Reducer blob round-trip | 19 | | 03 | fetch-notify | Plan-triggered HTTP demo | 20 | | 04 | aggregator | Fan-out plan join demo | 21 | | 05 | chain-comp | Multi-plan saga + refund | 22 | | 06 | safe-upgrade | Governance shadow/apply demo | 23 | | 07 | llm-summarizer | HTTP + LLM summarization demo | 24 | | 08 | retry-backoff | Reducer-driven retry with timer | 25 | | 09 | worldfs-lab | WorldFS view over notes + catalog | 26 | 27 | Use the `aos-examples` CLI to list or run demos. Run them in order—the ladder is deliberate, and later steps assume the earlier capabilities and policies are already in place. 28 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/shadow/summary.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] 4 | pub struct ShadowSummary { 5 | pub manifest_hash: String, 6 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 7 | pub predicted_effects: Vec, 8 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 9 | pub pending_receipts: Vec, 10 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 11 | pub plan_results: Vec, 12 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 13 | pub ledger_deltas: Vec, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 17 | pub struct PredictedEffect { 18 | pub kind: String, 19 | pub cap: String, 20 | pub intent_hash: String, 21 | #[serde(default, skip_serializing_if = "Option::is_none")] 22 | pub params_json: Option, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 26 | pub struct PendingPlanReceipt { 27 | pub plan_id: u64, 28 | pub plan: Option, 29 | pub intent_hash: String, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 33 | pub struct PlanResultPreview { 34 | pub plan: String, 35 | pub plan_id: u64, 36 | pub output_schema: String, 37 | } 38 | 39 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 40 | pub struct LedgerDelta { 41 | pub ledger: LedgerKind, 42 | pub name: String, 43 | pub change: DeltaKind, 44 | } 45 | 46 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] 47 | #[serde(rename_all = "snake_case")] 48 | pub enum LedgerKind { 49 | Capability, 50 | Policy, 51 | } 52 | 53 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] 54 | #[serde(rename_all = "snake_case")] 55 | pub enum DeltaKind { 56 | Added, 57 | Removed, 58 | Changed, 59 | } 60 | -------------------------------------------------------------------------------- /examples/07-llm-summarizer/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/LlmSummarizerPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Summarizing": { "unit": {} }, 9 | "Done": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/LlmSummarizerState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/LlmSummarizerPc@1" }, 19 | "next_request_id": { "nat": {} }, 20 | "pending_request": { "option": { "nat": {} } }, 21 | "last_summary": { "option": { "text": {} } }, 22 | "last_tokens_prompt": { "option": { "nat": {} } }, 23 | "last_tokens_completion": { "option": { "nat": {} } }, 24 | "last_cost_millis": { "option": { "nat": {} } } 25 | } 26 | } 27 | }, 28 | { 29 | "$kind": "defschema", 30 | "name": "demo/LlmSummarizerEvent@1", 31 | "type": { 32 | "variant": { 33 | "Start": { "record": { "url": { "text": {} } } }, 34 | "SummaryReady": { 35 | "record": { 36 | "request_id": { "nat": {} }, 37 | "summary": { "text": {} }, 38 | "tokens_prompt": { "nat": {} }, 39 | "tokens_completion": { "nat": {} }, 40 | "cost_millis": { "nat": {} } 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | { 47 | "$kind": "defschema", 48 | "name": "demo/SummarizeRequest@1", 49 | "type": { 50 | "record": { 51 | "request_id": { "nat": {} }, 52 | "url": { "text": {} } 53 | } 54 | } 55 | }, 56 | { 57 | "$kind": "defschema", 58 | "name": "demo/SummarizeResult@1", 59 | "type": { 60 | "record": { 61 | "request_id": { "nat": {} }, 62 | "summary": { "text": {} }, 63 | "tokens_prompt": { "nat": {} }, 64 | "tokens_completion": { "nat": {} }, 65 | "cost_millis": { "nat": {} } 66 | } 67 | } 68 | } 69 | ] 70 | -------------------------------------------------------------------------------- /crates/aos-effects/src/receipt.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; 2 | use thiserror::Error; 3 | 4 | /// Signed adapter receipt referencing an effect intent hash. 5 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 6 | pub struct EffectReceipt { 7 | pub intent_hash: [u8; 32], 8 | pub adapter_id: String, 9 | pub status: ReceiptStatus, 10 | #[serde(with = "serde_bytes")] 11 | pub payload_cbor: Vec, 12 | #[serde(default, skip_serializing_if = "Option::is_none")] 13 | pub cost_cents: Option, 14 | #[serde(with = "serde_bytes")] 15 | pub signature: Vec, 16 | } 17 | 18 | impl EffectReceipt { 19 | pub fn payload(&self) -> Result { 20 | serde_cbor::from_slice(&self.payload_cbor).map_err(ReceiptDecodeError::Payload) 21 | } 22 | } 23 | 24 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 25 | #[serde(rename_all = "snake_case")] 26 | pub enum ReceiptStatus { 27 | Ok, 28 | Error, 29 | Timeout, 30 | } 31 | 32 | #[derive(Debug, Error)] 33 | pub enum ReceiptDecodeError { 34 | #[error("failed to decode receipt payload: {0}")] 35 | Payload(#[from] serde_cbor::Error), 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | use serde::Serialize; 42 | 43 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 44 | struct DummyReceipt { 45 | ok: bool, 46 | } 47 | 48 | #[test] 49 | fn payload_round_trip() { 50 | let payload = serde_cbor::to_vec(&DummyReceipt { ok: true }).unwrap(); 51 | let receipt = EffectReceipt { 52 | intent_hash: [1u8; 32], 53 | adapter_id: "adapter.http".into(), 54 | status: ReceiptStatus::Ok, 55 | payload_cbor: payload, 56 | cost_cents: Some(42), 57 | signature: vec![9, 9, 9], 58 | }; 59 | let decoded: DummyReceipt = receipt.payload().unwrap(); 60 | assert_eq!(decoded, DummyReceipt { ok: true }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/00-counter/reducer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(improper_ctypes_definitions)] 2 | #![no_std] 3 | 4 | use aos_wasm_sdk::{aos_reducer, aos_variant, ReduceError, Reducer, ReducerCtx, Value}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | aos_reducer!(CounterSm); 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 10 | struct CounterState { 11 | pc: CounterPc, 12 | remaining: u64, 13 | } 14 | 15 | aos_variant! { 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | enum CounterPc { 18 | Idle, 19 | Counting, 20 | Done, 21 | } 22 | } 23 | 24 | impl Default for CounterPc { 25 | fn default() -> Self { 26 | CounterPc::Idle 27 | } 28 | } 29 | 30 | aos_variant! { 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | enum CounterEvent { 33 | Start { target: u64 }, 34 | Tick, 35 | } 36 | } 37 | 38 | #[derive(Default)] 39 | struct CounterSm; 40 | 41 | impl Reducer for CounterSm { 42 | type State = CounterState; 43 | type Event = CounterEvent; 44 | type Ann = Value; 45 | 46 | fn reduce( 47 | &mut self, 48 | event: Self::Event, 49 | ctx: &mut ReducerCtx, 50 | ) -> Result<(), ReduceError> { 51 | match event { 52 | CounterEvent::Start { target } => { 53 | if target == 0 { 54 | ctx.state.pc = CounterPc::Done; 55 | ctx.state.remaining = 0; 56 | } else { 57 | ctx.state.pc = CounterPc::Counting; 58 | ctx.state.remaining = target; 59 | } 60 | } 61 | CounterEvent::Tick => { 62 | if matches!(ctx.state.pc, CounterPc::Counting) && ctx.state.remaining > 0 { 63 | ctx.state.remaining -= 1; 64 | if ctx.state.remaining == 0 { 65 | ctx.state.pc = CounterPc::Done; 66 | } 67 | } 68 | } 69 | } 70 | Ok(()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/aos-sys/src/bin/object_catalog.rs: -------------------------------------------------------------------------------- 1 | //! ObjectCatalog reducer (`sys/ObjectCatalog@1`). 2 | //! 3 | //! A keyed reducer that maintains a versioned catalog of named objects. 4 | //! Each object name maps to an append-only history of versions. 5 | 6 | #![allow(improper_ctypes_definitions)] 7 | #![no_std] 8 | 9 | extern crate alloc; 10 | 11 | use alloc::string::String; 12 | use aos_sys::{ObjectRegistered, ObjectVersions, Version}; 13 | use aos_wasm_sdk::{ReduceError, Reducer, ReducerCtx, Value, aos_reducer}; 14 | use serde_cbor; 15 | 16 | // Required for WASM binary entry point 17 | #[cfg(target_arch = "wasm32")] 18 | fn main() {} 19 | 20 | #[cfg(not(target_arch = "wasm32"))] 21 | fn main() {} 22 | 23 | aos_reducer!(ObjectCatalog); 24 | 25 | /// ObjectCatalog reducer — keyed by object name. 26 | /// 27 | /// Invariants: 28 | /// - Key must equal `meta.name` (enforced via `ensure_key_eq`) 29 | /// - Versions are append-only; `latest` increments monotonically 30 | /// - No micro-effects; pure state machine 31 | #[derive(Default)] 32 | struct ObjectCatalog; 33 | 34 | impl Reducer for ObjectCatalog { 35 | type State = ObjectVersions; 36 | type Event = ObjectRegistered; 37 | type Ann = Value; 38 | 39 | fn reduce( 40 | &mut self, 41 | event: Self::Event, 42 | ctx: &mut ReducerCtx, 43 | ) -> Result<(), ReduceError> { 44 | let meta = event.meta; 45 | 46 | // Key must match meta.name for keyed routing (safeguard). 47 | if let Some(key) = ctx.key() { 48 | let decoded_key: String = 49 | serde_cbor::from_slice(key).map_err(|_| ReduceError::new("key decode failed"))?; 50 | if decoded_key != meta.name { 51 | return Err(ReduceError::new("key mismatch")); 52 | } 53 | } 54 | 55 | // Append-only version bump (0 → 1 on first registration) 56 | let next: Version = ctx.state.latest.saturating_add(1); 57 | ctx.state.latest = next; 58 | ctx.state.versions.insert(next, meta); 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/scheduler.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use crate::event::ReducerEvent; 4 | 5 | pub enum Task { 6 | Reducer(ReducerEvent), 7 | Plan(u64), 8 | } 9 | 10 | #[derive(Default)] 11 | pub struct Scheduler { 12 | reducer_queue: VecDeque, 13 | plan_queue: VecDeque, 14 | last_was_plan: bool, 15 | next_plan_id: u64, 16 | } 17 | 18 | impl Scheduler { 19 | pub fn push_reducer(&mut self, event: ReducerEvent) { 20 | self.reducer_queue.push_back(event); 21 | } 22 | 23 | pub fn push_plan(&mut self, instance_id: u64) { 24 | self.plan_queue.push_back(instance_id); 25 | } 26 | 27 | pub fn pop(&mut self) -> Option { 28 | match (self.reducer_queue.is_empty(), self.plan_queue.is_empty()) { 29 | (true, true) => None, 30 | (false, true) => self.reducer_queue.pop_front().map(Task::Reducer), 31 | (true, false) => self.plan_queue.pop_front().map(Task::Plan), 32 | (false, false) => { 33 | // Round-robin between plan and reducer to keep fairness. 34 | if self.last_was_plan { 35 | self.last_was_plan = false; 36 | self.reducer_queue.pop_front().map(Task::Reducer) 37 | } else { 38 | self.last_was_plan = true; 39 | self.plan_queue.pop_front().map(Task::Plan) 40 | } 41 | } 42 | } 43 | } 44 | 45 | pub fn alloc_plan_id(&mut self) -> u64 { 46 | let id = self.next_plan_id; 47 | self.next_plan_id += 1; 48 | id 49 | } 50 | 51 | pub fn is_empty(&self) -> bool { 52 | self.reducer_queue.is_empty() && self.plan_queue.is_empty() 53 | } 54 | 55 | pub fn clear(&mut self) { 56 | self.reducer_queue.clear(); 57 | self.plan_queue.clear(); 58 | } 59 | 60 | pub fn set_next_plan_id(&mut self, next: u64) { 61 | self.next_plan_id = next; 62 | } 63 | 64 | pub fn next_plan_id(&self) -> u64 { 65 | self.next_plan_id 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/SafeUpgradePc@1" 6 | }, 7 | { 8 | "name": "demo/SafeUpgradeState@1" 9 | }, 10 | { 11 | "name": "demo/SafeUpgradeEvent@1" 12 | }, 13 | { 14 | "name": "demo/UpgradeFetchRequest@1" 15 | }, 16 | { 17 | "name": "demo/SafeUpgradeResult@1" 18 | }, 19 | { 20 | "name": "sys/HttpRequestParams@1" 21 | }, 22 | { 23 | "name": "sys/HttpRequestReceipt@1" 24 | } 25 | ], 26 | "modules": [ 27 | { 28 | "name": "demo/SafeUpgrade@1" 29 | } 30 | ], 31 | "plans": [ 32 | { 33 | "name": "demo/fetch_plan@1" 34 | } 35 | ], 36 | "caps": [ 37 | { 38 | "name": "demo/http_fetch_cap@1" 39 | } 40 | ], 41 | "policies": [ 42 | { 43 | "name": "demo/http-policy@1" 44 | } 45 | ], 46 | "defaults": { 47 | "policy": "demo/http-policy@1", 48 | "cap_grants": [ 49 | { 50 | "name": "cap_http_fetch", 51 | "cap": "demo/http_fetch_cap@1", 52 | "params": { 53 | "record": { 54 | "hosts": { 55 | "set": [ 56 | { 57 | "text": "example.com" 58 | } 59 | ] 60 | }, 61 | "methods": { 62 | "set": [ 63 | { 64 | "text": "GET" 65 | } 66 | ] 67 | } 68 | } 69 | } 70 | } 71 | ] 72 | }, 73 | "module_bindings": {}, 74 | "routing": { 75 | "events": [ 76 | { 77 | "event": "demo/SafeUpgradeEvent@1", 78 | "reducer": "demo/SafeUpgrade@1" 79 | } 80 | ], 81 | "inboxes": [] 82 | }, 83 | "triggers": [ 84 | { 85 | "event": "demo/UpgradeFetchRequest@1", 86 | "plan": "demo/fetch_plan@1", 87 | "correlate_by": "request_id" 88 | } 89 | ], 90 | "air_version": "1", 91 | "effects": [ 92 | { 93 | "name": "sys/http.request@1" 94 | } 95 | ] 96 | } 97 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/reducer.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::PathBuf; 3 | use std::sync::Arc; 4 | 5 | use aos_air_types::{DefModule, Name}; 6 | use aos_cbor::Hash; 7 | use aos_store::Store; 8 | use aos_wasm::ReducerRuntime; 9 | use aos_wasm_abi::{ReducerInput, ReducerOutput}; 10 | 11 | use crate::error::KernelError; 12 | 13 | pub struct ReducerRegistry { 14 | store: Arc, 15 | runtime: ReducerRuntime, 16 | modules: HashMap, 17 | } 18 | 19 | struct ReducerModule { 20 | module: Arc, 21 | } 22 | 23 | impl ReducerRegistry { 24 | pub fn new(store: Arc, module_cache_dir: Option) -> Result { 25 | Ok(Self { 26 | store, 27 | runtime: ReducerRuntime::new_with_disk_cache(module_cache_dir) 28 | .map_err(KernelError::Wasm)?, 29 | modules: HashMap::new(), 30 | }) 31 | } 32 | 33 | pub fn ensure_loaded(&mut self, name: &str, module_def: &DefModule) -> Result<(), KernelError> { 34 | if self.modules.contains_key(name) { 35 | return Ok(()); 36 | } 37 | let wasm_hash = Hash::from_hex_str(module_def.wasm_hash.as_str()) 38 | .map_err(|err| KernelError::Manifest(err.to_string()))?; 39 | let bytes: Vec = self.store.get_blob(wasm_hash)?; 40 | let compiled = self 41 | .runtime 42 | .cached_module(&bytes) 43 | .map_err(KernelError::Wasm)?; 44 | self.modules 45 | .insert(name.to_string(), ReducerModule { module: compiled }); 46 | Ok(()) 47 | } 48 | 49 | pub fn invoke(&self, name: &str, input: &ReducerInput) -> Result { 50 | let module = self 51 | .modules 52 | .get(name) 53 | .ok_or_else(|| KernelError::ReducerNotFound(name.to_string()))?; 54 | let output = self 55 | .runtime 56 | .run_compiled(&module.module, input) 57 | .map_err(KernelError::Wasm)?; 58 | Ok(output) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "$kind": "defschema", "name": "notes/NoteKey@1", "type": { "text": {} } }, 3 | { "$kind": "defschema", "name": "notes/NoteStarted@1", "type": { "record": { 4 | "note_id": { "text": {} }, 5 | "title": { "text": {} }, 6 | "author": { "text": {} }, 7 | "created_at_ns": { "nat": {} } 8 | } } }, 9 | { "$kind": "defschema", "name": "notes/NoteAppended@1", "type": { "record": { 10 | "note_id": { "text": {} }, 11 | "line": { "text": {} } 12 | } } }, 13 | { "$kind": "defschema", "name": "notes/NoteFinalized@1", "type": { "record": { 14 | "note_id": { "text": {} }, 15 | "finalized_at_ns": { "nat": {} } 16 | } } }, 17 | { "$kind": "defschema", "name": "notes/NoteArchived@1", "type": { "record": { 18 | "note_id": { "text": {} }, 19 | "report_hash": { "hash": {} } 20 | } } }, 21 | { "$kind": "defschema", "name": "notes/NoteState@1", "type": { "record": { 22 | "pc": { "variant": { 23 | "Draft": { "record": {} }, 24 | "Finalized": { "record": {} }, 25 | "Archived": { "record": {} } 26 | } }, 27 | "title": { "text": {} }, 28 | "author": { "text": {} }, 29 | "lines": { "list": { "text": {} } }, 30 | "created_at_ns": { "nat": {} }, 31 | "finalized_at_ns": { "option": { "nat": {} } }, 32 | "report_hash": { "option": { "hash": {} } } 33 | } } }, 34 | { "$kind": "defschema", "name": "notes/NoteEvent@1", "type": { "variant": { 35 | "Start": { "ref": "notes/NoteStarted@1" }, 36 | "Append": { "ref": "notes/NoteAppended@1" }, 37 | "Finalize": { "ref": "notes/NoteFinalized@1" }, 38 | "Archived": { "ref": "notes/NoteArchived@1" } 39 | } } }, 40 | { "$kind": "defschema", "name": "notes/SnapshotRequested@1", "type": { "record": { 41 | "note_id": { "text": {} }, 42 | "report_hash": { "hash": {} }, 43 | "namespace": { "text": {} }, 44 | "object_path": { "text": {} }, 45 | "title": { "text": {} }, 46 | "author": { "text": {} }, 47 | "line_count": { "nat": {} }, 48 | "finalized_at_ns": { "nat": {} }, 49 | "created_at": { "time": {} } 50 | } } } 51 | ] 52 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "air_version": "1", 4 | "schemas": [ 5 | { 6 | "name": "demo/StartWork@1" 7 | }, 8 | { 9 | "name": "demo/WorkRequested@1" 10 | }, 11 | { 12 | "name": "demo/WorkOk@1" 13 | }, 14 | { 15 | "name": "demo/WorkErr@1" 16 | }, 17 | { 18 | "name": "demo/RetryState@1" 19 | }, 20 | { 21 | "name": "demo/RetryEvent@1" 22 | }, 23 | { 24 | "name": "sys/TimerSetParams@1" 25 | }, 26 | { 27 | "name": "sys/TimerSetReceipt@1" 28 | }, 29 | { 30 | "name": "sys/TimerFired@1" 31 | }, 32 | { 33 | "name": "sys/HttpRequestParams@1" 34 | }, 35 | { 36 | "name": "sys/HttpRequestReceipt@1" 37 | } 38 | ], 39 | "modules": [ 40 | { 41 | "name": "demo/RetrySM@1" 42 | } 43 | ], 44 | "plans": [ 45 | { 46 | "name": "demo/WorkPlan@1" 47 | } 48 | ], 49 | "effects": [ 50 | { 51 | "name": "sys/http.request@1" 52 | }, 53 | { 54 | "name": "sys/timer.set@1" 55 | } 56 | ], 57 | "caps": [ 58 | { 59 | "name": "sys/timer@1" 60 | } 61 | ], 62 | "policies": [ 63 | { 64 | "name": "demo/default_policy@1" 65 | } 66 | ], 67 | "defaults": { 68 | "policy": "demo/default_policy@1", 69 | "cap_grants": [ 70 | { 71 | "name": "timer_grant", 72 | "cap": "sys/timer@1", 73 | "params": { 74 | "record": {} 75 | } 76 | } 77 | ] 78 | }, 79 | "module_bindings": { 80 | "demo/RetrySM@1": { 81 | "slots": { 82 | "timer": "timer_grant" 83 | } 84 | } 85 | }, 86 | "routing": { 87 | "events": [ 88 | { 89 | "event": "demo/RetryEvent@1", 90 | "reducer": "demo/RetrySM@1" 91 | } 92 | ], 93 | "inboxes": [] 94 | }, 95 | "triggers": [ 96 | { 97 | "event": "demo/WorkRequested@1", 98 | "plan": "demo/WorkPlan@1" 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/head.rs: -------------------------------------------------------------------------------- 1 | //! `aos journal head` command. 2 | 3 | use anyhow::Result; 4 | use aos_host::control::RequestEnvelope; 5 | use aos_kernel::journal::fs::FsJournal; 6 | 7 | use crate::opts::{Mode, WorldOpts, resolve_dirs}; 8 | use crate::output::print_success; 9 | 10 | use super::{should_use_control, try_control_client}; 11 | 12 | pub async fn cmd_head(opts: &WorldOpts) -> Result<()> { 13 | let dirs = resolve_dirs(opts)?; 14 | 15 | // Try daemon first 16 | if should_use_control(opts) { 17 | if let Some(mut client) = try_control_client(&dirs).await { 18 | let req = RequestEnvelope { 19 | v: 1, 20 | id: "cli-head".into(), 21 | cmd: "journal-head".into(), 22 | payload: serde_json::json!({}), 23 | }; 24 | let resp = client.request(&req).await?; 25 | if !resp.ok { 26 | anyhow::bail!("journal-head failed: {:?}", resp.error); 27 | } 28 | if let Some(result) = resp.result { 29 | let data = result.get("head").cloned().unwrap_or(result); 30 | return print_success(opts, data, None, vec![]); 31 | } 32 | return print_success( 33 | opts, 34 | serde_json::json!(null), 35 | None, 36 | vec!["missing head result".into()], 37 | ); 38 | } else if matches!(opts.mode, Mode::Daemon) { 39 | anyhow::bail!( 40 | "daemon mode requested but no control socket at {}", 41 | dirs.control_socket.display() 42 | ); 43 | } else if !opts.quiet { 44 | // fall through to local 45 | } 46 | } 47 | 48 | // Fall back to reading from store directly 49 | let head = FsJournal::head(&dirs.store_root)?; 50 | print_success( 51 | opts, 52 | serde_json::json!(head), 53 | None, 54 | if opts.quiet { 55 | vec![] 56 | } else { 57 | vec!["daemon unavailable; using journal fs head".into()] 58 | }, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /crates/aos-host/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Shared utilities for working with manifests and world directories. 2 | 3 | use std::fs; 4 | use std::path::Path; 5 | 6 | use anyhow::{Context, Result}; 7 | use aos_air_types::{DefModule, HashRef}; 8 | use aos_kernel::LoadedManifest; 9 | 10 | use crate::manifest_loader::ZERO_HASH_SENTINEL; 11 | 12 | /// Check if a module has a placeholder hash that should be patched. 13 | pub fn is_placeholder_hash(module: &DefModule) -> bool { 14 | module.wasm_hash.as_str() == ZERO_HASH_SENTINEL 15 | } 16 | 17 | /// Patch modules in a loaded manifest based on a predicate. 18 | /// 19 | /// For each module where `predicate(name, module)` returns true, the module's 20 | /// `wasm_hash` is replaced with the provided hash. Returns the count of modules patched. 21 | /// 22 | /// # Examples 23 | /// 24 | /// Patch all modules with placeholder hashes: 25 | /// ```ignore 26 | /// let count = patch_modules(&mut loaded, &hash, |_, m| is_placeholder_hash(m)); 27 | /// ``` 28 | /// 29 | /// Patch a specific module by name: 30 | /// ```ignore 31 | /// let count = patch_modules(&mut loaded, &hash, |name, _| name == "demo/MySM@1"); 32 | /// ``` 33 | pub fn patch_modules( 34 | loaded: &mut LoadedManifest, 35 | wasm_hash: &HashRef, 36 | predicate: impl Fn(&str, &DefModule) -> bool, 37 | ) -> usize { 38 | let mut count = 0; 39 | for (name, module) in loaded.modules.iter_mut() { 40 | if predicate(name.as_str(), module) { 41 | module.wasm_hash = wasm_hash.clone(); 42 | count += 1; 43 | } 44 | } 45 | count 46 | } 47 | 48 | /// Check if any modules in the manifest have placeholder hashes. 49 | pub fn has_placeholder_modules(loaded: &LoadedManifest) -> bool { 50 | loaded.modules.values().any(is_placeholder_hash) 51 | } 52 | 53 | /// Remove the journal directory if it exists. 54 | /// 55 | /// The journal is located at `/.aos/journal/`. 56 | pub fn reset_journal(world_root: &Path) -> Result<()> { 57 | let journal_dir = world_root.join(".aos/journal"); 58 | if journal_dir.exists() { 59 | fs::remove_dir_all(&journal_dir).context("remove journal directory")?; 60 | } 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "air_version": "1", 4 | "schemas": [ 5 | { "name": "notes/NoteKey@1" }, 6 | { "name": "notes/NoteStarted@1" }, 7 | { "name": "notes/NoteAppended@1" }, 8 | { "name": "notes/NoteFinalized@1" }, 9 | { "name": "notes/NoteArchived@1" }, 10 | { "name": "notes/NoteState@1" }, 11 | { "name": "notes/NoteEvent@1" }, 12 | { "name": "notes/SnapshotRequested@1" }, 13 | { "name": "sys/ObjectKey@1" }, 14 | { "name": "sys/ObjectMeta@1" }, 15 | { "name": "sys/ObjectVersions@1" }, 16 | { "name": "sys/ObjectRegistered@1" }, 17 | { "name": "sys/BlobPutParams@1" }, 18 | { "name": "sys/BlobPutReceipt@1" }, 19 | { "name": "sys/BlobGetParams@1" }, 20 | { "name": "sys/BlobGetReceipt@1" } 21 | ], 22 | "modules": [ 23 | { "name": "notes/NotebookSM@1" }, 24 | { "name": "sys/ObjectCatalog@1" } 25 | ], 26 | "plans": [ 27 | { "name": "notes/SnapshotPlan@1" } 28 | ], 29 | "effects": [ 30 | { "name": "sys/blob.put@1" } 31 | ], 32 | "caps": [ 33 | { "name": "notes/blob_cap@1" }, 34 | { "name": "sys/blob@1" }, 35 | { "name": "sys/query@1" } 36 | ], 37 | "policies": [ 38 | { "name": "notes/allow_all@1" } 39 | ], 40 | "defaults": { 41 | "policy": "notes/allow_all@1", 42 | "cap_grants": [ 43 | { "name": "cap_blob", "cap": "notes/blob_cap@1", "params": { "record": {} } }, 44 | { "name": "cap_sys_blob", "cap": "sys/blob@1", "params": { "record": {} } }, 45 | { "name": "cap_query", "cap": "sys/query@1", "params": { "record": { "scope": { "text": "*" } } } } 46 | ] 47 | }, 48 | "module_bindings": { 49 | "notes/NotebookSM@1": { "slots": {} }, 50 | "sys/ObjectCatalog@1": { "slots": {} } 51 | }, 52 | "routing": { 53 | "events": [ 54 | { "event": "notes/NoteEvent@1", "reducer": "notes/NotebookSM@1", "key_field": "$value.note_id" }, 55 | { "event": "sys/ObjectRegistered@1", "reducer": "sys/ObjectCatalog@1", "key_field": "meta.name" } 56 | ], 57 | "inboxes": [] 58 | }, 59 | "triggers": [ 60 | { "event": "notes/SnapshotRequested@1", "plan": "notes/SnapshotPlan@1", "correlate_by": "note_id" } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /spec/schemas/defpolicy.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "$id": "https://aos.dev/air/v1/defpolicy.schema.json", 4 | "title": "AIR v1 defpolicy", 5 | "description": "Policy rule pack for allow/deny decisions at effect enqueue time. v1: origin-aware matching, no approvals or rate limits.", 6 | "type": "object", 7 | "properties": { 8 | "$kind": { "const": "defpolicy" }, 9 | "name": { "$ref": "common.schema.json#/$defs/Name" }, 10 | "rules": { 11 | "type": "array", 12 | "items": { "$ref": "#/$defs/Rule" } 13 | } 14 | }, 15 | "required": ["$kind","name","rules"], 16 | "additionalProperties": false, 17 | "$defs": { 18 | "Rule": { 19 | "type": "object", 20 | "properties": { 21 | "when": { "$ref": "#/$defs/Match" }, 22 | "decision": { 23 | "type": "string", 24 | "enum": ["allow","deny"], 25 | "description": "v1: allow or deny only. require_approval reserved for v1.1+" 26 | } 27 | }, 28 | "required": ["when","decision"], 29 | "additionalProperties": false 30 | }, 31 | "Match": { 32 | "type": "object", 33 | "description": "Match conditions for policy rules. All fields optional; first rule matching all provided conditions wins.", 34 | "properties": { 35 | "effect_kind": { 36 | "$ref": "common.schema.json#/$defs/EffectKind", 37 | "description": "Match effect kind (namespaced string; built-in catalog documented in spec/03-air.md, custom kinds allowed when registered by adapters)" 38 | }, 39 | "cap_name": { 40 | "$ref": "common.schema.json#/$defs/CapGrantName", 41 | "description": "Match capability grant name" 42 | }, 43 | "origin_kind": { 44 | "type": "string", 45 | "enum": ["plan", "reducer"], 46 | "description": "Match whether effect originates from a plan or reducer" 47 | }, 48 | "origin_name": { 49 | "$ref": "common.schema.json#/$defs/Name", 50 | "description": "Match specific plan or reducer name" 51 | } 52 | }, 53 | "additionalProperties": false 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/aos-examples/src/counter.rs: -------------------------------------------------------------------------------- 1 | //! Counter demo wired up through AIR JSON assets and the reducer harness. 2 | //! 3 | //! This is a minimal example with no micro-effects, showing how to load 4 | //! a manifest from AIR JSON assets and drive a reducer through events. 5 | 6 | use std::path::Path; 7 | 8 | use anyhow::Result; 9 | use aos_wasm_sdk::aos_variant; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::example_host::{ExampleHost, HarnessConfig}; 13 | 14 | const REDUCER_NAME: &str = "demo/CounterSM@1"; 15 | const EVENT_SCHEMA: &str = "demo/CounterEvent@1"; 16 | const TARGET_COUNT: u64 = 3; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] 19 | struct CounterState { 20 | pc: CounterPc, 21 | remaining: u64, 22 | } 23 | 24 | aos_variant! { 25 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 26 | enum CounterPc { 27 | Idle, 28 | Counting, 29 | Done, 30 | } 31 | } 32 | 33 | impl Default for CounterPc { 34 | fn default() -> Self { 35 | CounterPc::Idle 36 | } 37 | } 38 | 39 | aos_variant! { 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | enum CounterEvent { 42 | Start { target: u64 }, 43 | Tick, 44 | } 45 | } 46 | 47 | pub fn run(example_root: &Path) -> Result<()> { 48 | let mut host = ExampleHost::prepare(HarnessConfig { 49 | example_root, 50 | assets_root: None, 51 | reducer_name: REDUCER_NAME, 52 | event_schema: EVENT_SCHEMA, 53 | module_crate: "examples/00-counter/reducer", 54 | })?; 55 | 56 | println!("→ Counter demo (target {TARGET_COUNT})"); 57 | println!(" start (target {TARGET_COUNT})"); 58 | host.send_event(&CounterEvent::Start { 59 | target: TARGET_COUNT, 60 | })?; 61 | 62 | for tick in 1..=TARGET_COUNT { 63 | host.send_event(&CounterEvent::Tick)?; 64 | println!(" tick #{tick}"); 65 | } 66 | 67 | host.run_cycle_batch()?; 68 | 69 | let final_state: CounterState = host.read_state()?; 70 | println!( 71 | " final state: pc={:?}, remaining={}", 72 | final_state.pc, final_state.remaining 73 | ); 74 | 75 | host.finish()?.verify_replay()?; 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v1/fetch_plan.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/fetch_plan@1", 5 | "input": "demo/UpgradeFetchRequest@1", 6 | "output": "demo/SafeUpgradeResult@1", 7 | "steps": [ 8 | { 9 | "id": "emit_primary", 10 | "op": "emit_effect", 11 | "kind": "http.request", 12 | "params": { 13 | "method": "GET", 14 | "url": "https://example.com/data.json", 15 | "headers": { "x-idempotency-key": "safe-upgrade-v1-primary" }, 16 | "body_ref": null 17 | }, 18 | "cap": "cap_http_fetch", 19 | "bind": { "effect_id_as": "primary_req" } 20 | }, 21 | { 22 | "id": "await_primary", 23 | "op": "await_receipt", 24 | "for": { "ref": "@var:primary_req" }, 25 | "bind": { "as": "primary_receipt" } 26 | }, 27 | { 28 | "id": "capture_primary_status", 29 | "op": "assign", 30 | "expr": { "int": 200 }, 31 | "bind": { "as": "primary_status" } 32 | }, 33 | { 34 | "id": "raise_complete", 35 | "op": "raise_event", 36 | "event": "demo/SafeUpgradeEvent@1", 37 | "value": { 38 | "variant": { 39 | "tag": "NotifyComplete", 40 | "value": { 41 | "record": { 42 | "primary_status": { "ref": "@var:primary_status" }, 43 | "follow_status": { "int": -1 }, 44 | "request_count": { "nat": 1 } 45 | } 46 | } 47 | } 48 | } 49 | }, 50 | { 51 | "id": "finish", 52 | "op": "end", 53 | "result": { 54 | "record": { 55 | "primary_status": { "ref": "@var:primary_status" }, 56 | "follow_status": { "int": -1 } 57 | } 58 | } 59 | } 60 | ], 61 | "edges": [ 62 | { "from": "emit_primary", "to": "await_primary" }, 63 | { "from": "await_primary", "to": "capture_primary_status" }, 64 | { "from": "capture_primary_status", "to": "raise_complete" }, 65 | { "from": "raise_complete", "to": "finish" } 66 | ], 67 | "required_caps": ["cap_http_fetch"], 68 | "allowed_effects": ["http.request"] 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /examples/04-aggregator/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/AggregatorPc@1" 6 | }, 7 | { 8 | "name": "demo/AggregatorState@1" 9 | }, 10 | { 11 | "name": "demo/AggregatorEvent@1" 12 | }, 13 | { 14 | "name": "demo/AggregateRequested@1" 15 | }, 16 | { 17 | "name": "demo/AggregationTarget@1" 18 | }, 19 | { 20 | "name": "demo/AggregateResponse@1" 21 | }, 22 | { 23 | "name": "sys/HttpRequestParams@1" 24 | }, 25 | { 26 | "name": "sys/HttpRequestReceipt@1" 27 | } 28 | ], 29 | "modules": [ 30 | { 31 | "name": "demo/Aggregator@1" 32 | } 33 | ], 34 | "plans": [ 35 | { 36 | "name": "demo/aggregator_plan@1" 37 | } 38 | ], 39 | "caps": [ 40 | { 41 | "name": "demo/http_aggregate_cap@1" 42 | } 43 | ], 44 | "policies": [ 45 | { 46 | "name": "demo/http-aggregate-policy@1" 47 | } 48 | ], 49 | "defaults": { 50 | "policy": "demo/http-aggregate-policy@1", 51 | "cap_grants": [ 52 | { 53 | "name": "cap_http_aggregate", 54 | "cap": "demo/http_aggregate_cap@1", 55 | "params": { 56 | "record": { 57 | "hosts": { 58 | "set": [ 59 | { 60 | "text": "example.com" 61 | } 62 | ] 63 | }, 64 | "verbs": { 65 | "set": [ 66 | { 67 | "text": "GET" 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | } 74 | ] 75 | }, 76 | "module_bindings": {}, 77 | "routing": { 78 | "events": [ 79 | { 80 | "event": "demo/AggregatorEvent@1", 81 | "reducer": "demo/Aggregator@1" 82 | } 83 | ], 84 | "inboxes": [] 85 | }, 86 | "triggers": [ 87 | { 88 | "event": "demo/AggregateRequested@1", 89 | "plan": "demo/aggregator_plan@1", 90 | "correlate_by": "request_id" 91 | } 92 | ], 93 | "air_version": "1", 94 | "effects": [ 95 | { 96 | "name": "sys/http.request@1" 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /examples/08-retry-backoff/README.md: -------------------------------------------------------------------------------- 1 | # Example 08 — Retry with Exponential Backoff (Reducer-driven) 2 | 3 | A runnable blueprint for reducer-driven retries: reducer owns attempt counting and timers; plan just tries the work and reports back. 4 | 5 | ## What it does 6 | - `StartWork` event kicks the reducer. 7 | - Reducer emits `WorkRequested` intent and tracks `attempt = 1` with config `max_attempts`, `base_delay_ms`, `anchor now_ns`. 8 | - Trigger starts `WorkPlan`, which (in this minimal example) always reports a transient failure (`WorkErr transient=true`). 9 | - Reducer schedules `timer.set` with exponential backoff (`base_delay_ms * 2^(attempt-1)`) until `max_attempts` is hit, then marks `Failed`. A `WorkOk` event would mark `Done` immediately. 10 | 11 | ## Layout 12 | ``` 13 | examples/08-retry-backoff/ 14 | air/ 15 | schemas.air.json # StartWork, WorkRequested, WorkOk, WorkErr, RetryEvent, RetryState 16 | module.air.json # defmodule demo/RetrySM@1 (reducer) 17 | plans.air.json # defplan demo/WorkPlan@1 (raises WorkErr) 18 | capabilities.air.json # timer cap 19 | policies.air.json # allow-all policy 20 | manifest.air.json # wires routing + trigger + cap grant 21 | reducer/ 22 | Cargo.toml 23 | src/lib.rs # reducer state machine and backoff logic 24 | ``` 25 | 26 | ## How backoff is computed 27 | ``` 28 | delay_ms = base_delay_ms * 2^(attempt-1) 29 | delay_ns = delay_ms * 1_000_000 30 | deliver_at_ns = anchor_ns + delay_ns 31 | ``` 32 | The timer key is set to `req_id` to ease correlation/diagnostics. 33 | 34 | ## To run/build the reducer 35 | ``` 36 | cargo build -p retry_sm --release --target wasm32-unknown-unknown 37 | ``` 38 | (You can swap the placeholder `wasm_hash` in `module.air.json` with the built artifact's hash.) 39 | 40 | ## To make the plan succeed 41 | Replace the single `raise_event` in `plans.air.json` with your real effect/receipt handling that raises `WorkOk` when successful and `WorkErr` with `transient=false` on terminal failure. 42 | 43 | ## Why reducer-driven? 44 | - Retry policy lives in deterministic state; journal shows every attempt and timer. 45 | - Plans stay thin and audited; no retry state in external orchestration. 46 | - Works with reducer micro-effect `timer.set`, avoiding heavy effects in reducers. 47 | -------------------------------------------------------------------------------- /examples/09-worldfs-lab/air/plans.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "notes/SnapshotPlan@1", 5 | "input": "notes/SnapshotRequested@1", 6 | "steps": [ 7 | { 8 | "id": "put_blob", 9 | "op": "emit_effect", 10 | "kind": "blob.put", 11 | "params": { 12 | "record": { 13 | "namespace": { "ref": "@plan.input.namespace" }, 14 | "blob_ref": { "ref": "@plan.input.report_hash" } 15 | } 16 | }, 17 | "cap": "cap_blob", 18 | "bind": { "effect_id_as": "blob_put_id" } 19 | }, 20 | { 21 | "id": "await_blob", 22 | "op": "await_receipt", 23 | "for": { "ref": "@var:blob_put_id" }, 24 | "bind": { "as": "blob_put_receipt" } 25 | }, 26 | { 27 | "id": "register_obj", 28 | "op": "raise_event", 29 | "event": "sys/ObjectRegistered@1", 30 | "value": { 31 | "record": { 32 | "meta": { 33 | "record": { 34 | "name": { "ref": "@plan.input.object_path" }, 35 | "kind": { "text": "note.report" }, 36 | "hash": { "ref": "@plan.input.report_hash" }, 37 | "tags": { "set": [ { "text": "report" }, { "text": "worldfs" } ] }, 38 | "created_at": { "ref": "@plan.input.created_at" }, 39 | "owner": { "text": "notes/notebook@1" } 40 | } 41 | } 42 | } 43 | } 44 | }, 45 | { 46 | "id": "archive_note", 47 | "op": "raise_event", 48 | "event": "notes/NoteEvent@1", 49 | "value": { 50 | "variant": { 51 | "tag": "Archived", 52 | "value": { 53 | "record": { 54 | "note_id": { "ref": "@plan.input.note_id" }, 55 | "report_hash": { "ref": "@plan.input.report_hash" } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | { "id": "finish", "op": "end" } 62 | ], 63 | "edges": [ 64 | { "from": "put_blob", "to": "await_blob" }, 65 | { "from": "await_blob", "to": "register_obj" }, 66 | { "from": "register_obj", "to": "archive_note" }, 67 | { "from": "archive_note", "to": "finish" } 68 | ], 69 | "required_caps": ["cap_blob"], 70 | "allowed_effects": ["blob.put"] 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/notify_plan.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/notify_plan@1", 5 | "input": "demo/NotifyRequested@1", 6 | "steps": [ 7 | { 8 | "id": "emit_notify", 9 | "op": "emit_effect", 10 | "kind": "http.request", 11 | "params": { 12 | "record": { 13 | "method": { 14 | "op": "get", 15 | "args": [ 16 | { "ref": "@plan.input.target" }, 17 | { "text": "method" } 18 | ] 19 | }, 20 | "url": { 21 | "op": "get", 22 | "args": [ 23 | { "ref": "@plan.input.target" }, 24 | { "text": "url" } 25 | ] 26 | }, 27 | "headers": { "map": [] }, 28 | "body_ref": { "null": {} } 29 | } 30 | }, 31 | "cap": "cap_http_chain", 32 | "bind": { "effect_id_as": "notify_req" } 33 | }, 34 | { 35 | "id": "await_notify", 36 | "op": "await_receipt", 37 | "for": { "ref": "@var:notify_req" }, 38 | "bind": { "as": "notify_receipt" } 39 | }, 40 | { 41 | "id": "raise_result", 42 | "op": "raise_event", 43 | "event": "demo/ChainEvent@1", 44 | "value": { 45 | "variant": { 46 | "tag": "NotifyCompleted", 47 | "value": { 48 | "record": { 49 | "request_id": { 50 | "op": "get", 51 | "args": [ 52 | { "ref": "@plan.input" }, 53 | { "text": "request_id" } 54 | ] 55 | }, 56 | "status": { 57 | "op": "get", 58 | "args": [ 59 | { "ref": "@var:notify_receipt" }, 60 | { "text": "status" } 61 | ] 62 | } 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | { "id": "finish", "op": "end" } 69 | ], 70 | "edges": [ 71 | { "from": "emit_notify", "to": "await_notify" }, 72 | { "from": "await_notify", "to": "raise_result" }, 73 | { "from": "raise_result", "to": "finish" } 74 | ], 75 | "required_caps": ["cap_http_chain"], 76 | "allowed_effects": ["http.request"] 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/refund_plan.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/refund_plan@1", 5 | "input": "demo/RefundRequested@1", 6 | "steps": [ 7 | { 8 | "id": "emit_refund", 9 | "op": "emit_effect", 10 | "kind": "http.request", 11 | "params": { 12 | "record": { 13 | "method": { 14 | "op": "get", 15 | "args": [ 16 | { "ref": "@plan.input.target" }, 17 | { "text": "method" } 18 | ] 19 | }, 20 | "url": { 21 | "op": "get", 22 | "args": [ 23 | { "ref": "@plan.input.target" }, 24 | { "text": "url" } 25 | ] 26 | }, 27 | "headers": { "map": [] }, 28 | "body_ref": { "null": {} } 29 | } 30 | }, 31 | "cap": "cap_http_chain", 32 | "bind": { "effect_id_as": "refund_req" } 33 | }, 34 | { 35 | "id": "await_refund", 36 | "op": "await_receipt", 37 | "for": { "ref": "@var:refund_req" }, 38 | "bind": { "as": "refund_receipt" } 39 | }, 40 | { 41 | "id": "raise_result", 42 | "op": "raise_event", 43 | "event": "demo/ChainEvent@1", 44 | "value": { 45 | "variant": { 46 | "tag": "RefundCompleted", 47 | "value": { 48 | "record": { 49 | "request_id": { 50 | "op": "get", 51 | "args": [ 52 | { "ref": "@plan.input" }, 53 | { "text": "request_id" } 54 | ] 55 | }, 56 | "status": { 57 | "op": "get", 58 | "args": [ 59 | { "ref": "@var:refund_receipt" }, 60 | { "text": "status" } 61 | ] 62 | } 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | { "id": "finish", "op": "end" } 69 | ], 70 | "edges": [ 71 | { "from": "emit_refund", "to": "await_refund" }, 72 | { "from": "await_refund", "to": "raise_result" }, 73 | { "from": "raise_result", "to": "finish" } 74 | ], 75 | "required_caps": ["cap_http_chain"], 76 | "allowed_effects": ["http.request"] 77 | } 78 | ] 79 | -------------------------------------------------------------------------------- /crates/aos-cli/tests/obj_cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use std::fs; 3 | use tempfile::TempDir; 4 | 5 | #[test] 6 | fn obj_ls_without_daemon_returns_empty_with_notice() { 7 | let tmp = TempDir::new().expect("tmpdir"); 8 | let world = tmp.path(); 9 | fs::create_dir_all(world.join(".aos")).unwrap(); 10 | fs::create_dir_all(world.join("air")).unwrap(); 11 | 12 | let assert = std::process::Command::new(assert_cmd::cargo::cargo_bin!("aos")) 13 | .current_dir(world) 14 | .args(["--world", world.to_str().unwrap(), "obj", "ls", "--json"]) 15 | .assert() 16 | .success(); 17 | 18 | let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); 19 | let json: serde_json::Value = serde_json::from_str(&output).expect("json"); 20 | 21 | let arr = json["data"].as_array().cloned().unwrap_or_default(); 22 | assert_eq!(arr.len(), 0, "objects list should be empty without daemon"); 23 | let warnings = json["warnings"].as_array().cloned().unwrap_or_default(); 24 | assert!( 25 | warnings.iter().any(|w| w 26 | .as_str() 27 | .unwrap_or_default() 28 | .contains("object listing requires daemon")), 29 | "expected daemon notice" 30 | ); 31 | } 32 | 33 | #[test] 34 | fn obj_ls_versions_flag_ignored_in_batch_with_warning() { 35 | let tmp = TempDir::new().expect("tmpdir"); 36 | let world = tmp.path(); 37 | fs::create_dir_all(world.join(".aos")).unwrap(); 38 | fs::create_dir_all(world.join("air")).unwrap(); 39 | 40 | let assert = std::process::Command::new(assert_cmd::cargo::cargo_bin!("aos")) 41 | .current_dir(world) 42 | .args([ 43 | "--world", 44 | world.to_str().unwrap(), 45 | "obj", 46 | "ls", 47 | "--versions", 48 | "--json", 49 | ]) 50 | .assert() 51 | .success(); 52 | 53 | let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); 54 | let json: serde_json::Value = serde_json::from_str(&output).expect("json"); 55 | let warnings = json["warnings"].as_array().cloned().unwrap_or_default(); 56 | assert!( 57 | warnings.iter().any(|w| w 58 | .as_str() 59 | .unwrap_or_default() 60 | .contains("filters require daemon-side catalog")), 61 | "expected warning about versions filter" 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/fetch_plan.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/fetch_plan@1", 5 | "input": "demo/FetchRequest@1", 6 | "steps": [ 7 | { 8 | "id": "emit_http", 9 | "op": "emit_effect", 10 | "kind": "http.request", 11 | "params": { 12 | "method": "GET", 13 | "url": "https://example.com/data.json", 14 | "headers": {}, 15 | "body_ref": null 16 | }, 17 | "cap": "cap_http_fetch", 18 | "bind": { "effect_id_as": "http_req" } 19 | }, 20 | { 21 | "id": "await_http", 22 | "op": "await_receipt", 23 | "for": { "ref": "@var:http_req" }, 24 | "bind": { "as": "http_receipt" } 25 | }, 26 | { 27 | "id": "capture_status", 28 | "op": "assign", 29 | "expr": { 30 | "op": "get", 31 | "args": [ 32 | { "ref": "@var:http_receipt" }, 33 | { "text": "status" } 34 | ] 35 | }, 36 | "bind": { "as": "receipt_status" } 37 | }, 38 | { 39 | "id": "capture_body", 40 | "op": "assign", 41 | "expr": { 42 | "op": "get", 43 | "args": [ 44 | { "ref": "@var:http_receipt" }, 45 | { "text": "body_preview" } 46 | ] 47 | }, 48 | "bind": { "as": "receipt_body" } 49 | }, 50 | { 51 | "id": "raise_result", 52 | "op": "raise_event", 53 | "event": "demo/FetchNotifyEvent@1", 54 | "value": { 55 | "variant": { 56 | "tag": "NotifyComplete", 57 | "value": { 58 | "record": { 59 | "status": { "ref": "@var:receipt_status" }, 60 | "body_preview": { "ref": "@var:receipt_body" } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | { 67 | "id": "finish", 68 | "op": "end" 69 | } 70 | ], 71 | "edges": [ 72 | { "from": "emit_http", "to": "await_http" }, 73 | { "from": "await_http", "to": "capture_status" }, 74 | { "from": "capture_status", "to": "capture_body" }, 75 | { "from": "capture_body", "to": "raise_result" }, 76 | { "from": "raise_result", "to": "finish" } 77 | ], 78 | "required_caps": ["cap_http_fetch"], 79 | "allowed_effects": ["http.request"] 80 | } 81 | ] 82 | -------------------------------------------------------------------------------- /crates/aos-cli/src/commands/info.rs: -------------------------------------------------------------------------------- 1 | //! `aos status` command. 2 | 3 | use anyhow::{Context, Result}; 4 | use aos_host::manifest_loader; 5 | use aos_store::FsStore; 6 | use serde_json; 7 | 8 | use crate::opts::{WorldOpts, resolve_dirs}; 9 | use crate::output::print_success; 10 | 11 | pub async fn cmd_info(opts: &WorldOpts) -> Result<()> { 12 | let dirs = resolve_dirs(opts)?; 13 | let mut warnings = vec![]; 14 | 15 | // Check if store exists 16 | let store_path = dirs.store_root.join(".aos/store"); 17 | if !store_path.exists() { 18 | return print_success( 19 | opts, 20 | serde_json::json!({ 21 | "world": dirs.world, 22 | "air": dirs.air_dir, 23 | "reducer": dirs.reducer_dir, 24 | "store": dirs.store_root, 25 | "status": "not-initialized", 26 | }), 27 | None, 28 | warnings, 29 | ); 30 | } 31 | 32 | // Try to load manifest 33 | let store = std::sync::Arc::new(FsStore::open(&dirs.store_root).context("open store")?); 34 | let manifest_info = match manifest_loader::load_from_assets(store.clone(), &dirs.air_dir) { 35 | Ok(Some(loaded)) => serde_json::json!({ 36 | "schemas": loaded.manifest.schemas.len(), 37 | "modules": loaded.manifest.modules.len(), 38 | "plans": loaded.manifest.plans.len(), 39 | "effects": loaded.manifest.effects.len(), 40 | "triggers": loaded.manifest.triggers.len(), 41 | }), 42 | Ok(None) => { 43 | warnings.push("no manifest found in AIR directory".into()); 44 | serde_json::json!(null) 45 | } 46 | Err(e) => { 47 | warnings.push(format!("failed to load manifest: {e}")); 48 | serde_json::json!(null) 49 | } 50 | }; 51 | 52 | let daemon = if dirs.control_socket.exists() { 53 | serde_json::json!({ "running": true, "socket": dirs.control_socket }) 54 | } else { 55 | serde_json::json!({ "running": false }) 56 | }; 57 | 58 | print_success( 59 | opts, 60 | serde_json::json!({ 61 | "world": dirs.world, 62 | "air": dirs.air_dir, 63 | "reducer": dirs.reducer_dir, 64 | "store": dirs.store_root, 65 | "manifest": manifest_info, 66 | "daemon": daemon, 67 | }), 68 | None, 69 | warnings, 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/catalog.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | 3 | use crate::{CapType, DefEffect, EffectKind, OriginScope, SchemaRef}; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct EffectCatalogEntry { 7 | pub kind: EffectKind, 8 | pub cap_type: CapType, 9 | pub params_schema: SchemaRef, 10 | pub receipt_schema: SchemaRef, 11 | pub origin_scope: OriginScope, 12 | } 13 | 14 | #[derive(Debug, Clone, Default)] 15 | pub struct EffectCatalog { 16 | by_kind: IndexMap, 17 | } 18 | 19 | impl EffectCatalog { 20 | pub fn new() -> Self { 21 | Self { 22 | by_kind: IndexMap::new(), 23 | } 24 | } 25 | 26 | /// Builds a catalog from a list of `defeffect` nodes. Duplicate kinds keep the first definition. 27 | pub fn from_defs(defs: I) -> Self 28 | where 29 | I: IntoIterator, 30 | { 31 | let mut catalog = EffectCatalog::new(); 32 | for def in defs { 33 | let key = def.kind.as_str().to_string(); 34 | if catalog.by_kind.contains_key(&key) { 35 | continue; 36 | } 37 | catalog.by_kind.insert( 38 | key, 39 | EffectCatalogEntry { 40 | kind: def.kind.clone(), 41 | cap_type: def.cap_type.clone(), 42 | params_schema: def.params_schema.clone(), 43 | receipt_schema: def.receipt_schema.clone(), 44 | origin_scope: def.origin_scope, 45 | }, 46 | ); 47 | } 48 | catalog 49 | } 50 | 51 | pub fn get(&self, kind: &EffectKind) -> Option<&EffectCatalogEntry> { 52 | self.by_kind.get(kind.as_str()) 53 | } 54 | 55 | pub fn params_schema(&self, kind: &EffectKind) -> Option<&SchemaRef> { 56 | self.get(kind).map(|e| &e.params_schema) 57 | } 58 | 59 | pub fn receipt_schema(&self, kind: &EffectKind) -> Option<&SchemaRef> { 60 | self.get(kind).map(|e| &e.receipt_schema) 61 | } 62 | 63 | pub fn cap_type(&self, kind: &EffectKind) -> Option<&CapType> { 64 | self.get(kind).map(|e| &e.cap_type) 65 | } 66 | 67 | pub fn origin_scope(&self, kind: &EffectKind) -> Option { 68 | self.get(kind).map(|e| e.origin_scope) 69 | } 70 | 71 | pub fn kinds(&self) -> impl Iterator { 72 | self.by_kind.values().map(|e| &e.kind) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /crates/aos-cli/src/output.rs: -------------------------------------------------------------------------------- 1 | //! Shared output helpers for human and JSON modes. 2 | //! 3 | //! Human mode prints primary data to stdout and meta/notices to stderr. 4 | //! JSON mode wraps responses in `{ data, meta?, warnings? }` and respects 5 | //! `--pretty`, `--no-meta`, and `--quiet`. 6 | 7 | use std::io::Write; 8 | 9 | use anyhow::Result; 10 | use serde_json::{Value, json}; 11 | 12 | use crate::opts::WorldOpts; 13 | 14 | pub fn print_success( 15 | opts: &WorldOpts, 16 | data: Value, 17 | meta: Option, 18 | mut warnings: Vec, 19 | ) -> Result<()> { 20 | if opts.quiet { 21 | warnings.clear(); 22 | } 23 | if opts.pretty || opts.json { 24 | print_json(opts, data, meta, warnings) 25 | } else { 26 | print_human(opts, data, meta, warnings) 27 | } 28 | } 29 | 30 | fn print_json( 31 | opts: &WorldOpts, 32 | data: Value, 33 | meta: Option, 34 | warnings: Vec, 35 | ) -> Result<()> { 36 | let mut root = json!({ "data": data }); 37 | if !opts.no_meta { 38 | if let Some(m) = meta { 39 | root.as_object_mut().unwrap().insert("meta".into(), m); 40 | } 41 | } 42 | if !warnings.is_empty() { 43 | root.as_object_mut().unwrap().insert( 44 | "warnings".into(), 45 | warnings.into_iter().map(Value::String).collect(), 46 | ); 47 | } 48 | if opts.pretty { 49 | println!("{}", serde_json::to_string_pretty(&root)?); 50 | } else { 51 | println!("{}", serde_json::to_string(&root)?); 52 | } 53 | Ok(()) 54 | } 55 | 56 | fn print_human( 57 | opts: &WorldOpts, 58 | data: Value, 59 | meta: Option, 60 | warnings: Vec, 61 | ) -> Result<()> { 62 | if let Some(m) = meta { 63 | if !opts.no_meta && opts.json { 64 | // Only emit meta in human mode when explicitly in JSON output. 65 | let mut stderr = std::io::stderr(); 66 | writeln!(stderr, "meta: {}", serde_json::to_string_pretty(&m)?)?; 67 | } 68 | } 69 | for w in warnings { 70 | let mut stderr = std::io::stderr(); 71 | writeln!(stderr, "notice: {}", w)?; 72 | } 73 | print_value(data)?; 74 | Ok(()) 75 | } 76 | 77 | fn print_value(value: Value) -> Result<()> { 78 | match value { 79 | Value::String(s) => println!("{s}"), 80 | other => println!("{}", serde_json::to_string_pretty(&other)?), 81 | } 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/journal/mem.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use super::{Journal, JournalEntry, JournalError, JournalKind, JournalSeq, OwnedJournalEntry}; 4 | 5 | /// Simple in-memory journal useful for unit tests and TestWorld scenarios. 6 | #[derive(Debug, Default, Clone)] 7 | pub struct MemJournal { 8 | entries: Arc>>, 9 | } 10 | 11 | impl MemJournal { 12 | pub fn new() -> Self { 13 | Self { 14 | entries: Arc::new(Mutex::new(Vec::new())), 15 | } 16 | } 17 | 18 | pub fn from_entries(entries: &[OwnedJournalEntry]) -> Self { 19 | Self { 20 | entries: Arc::new(Mutex::new(entries.to_vec())), 21 | } 22 | } 23 | 24 | pub fn entries(&self) -> Vec { 25 | self.entries.lock().unwrap().clone() 26 | } 27 | } 28 | 29 | impl Journal for MemJournal { 30 | fn append(&mut self, entry: JournalEntry<'_>) -> Result { 31 | let mut guard = self.entries.lock().unwrap(); 32 | let seq = guard.len() as JournalSeq; 33 | guard.push(OwnedJournalEntry { 34 | seq, 35 | kind: entry.kind, 36 | payload: entry.payload.to_vec(), 37 | }); 38 | Ok(seq) 39 | } 40 | 41 | fn load_from(&self, from: JournalSeq) -> Result, JournalError> { 42 | Ok(self 43 | .entries() 44 | .into_iter() 45 | .filter(|entry| entry.seq >= from) 46 | .collect()) 47 | } 48 | 49 | fn next_seq(&self) -> JournalSeq { 50 | self.entries.lock().unwrap().len() as JournalSeq 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | 58 | #[test] 59 | fn append_and_load_round_trip() { 60 | let mut journal = MemJournal::new(); 61 | journal 62 | .append(JournalEntry::new(JournalKind::DomainEvent, b"first")) 63 | .unwrap(); 64 | journal 65 | .append(JournalEntry::new(JournalKind::EffectIntent, b"second")) 66 | .unwrap(); 67 | 68 | let all = journal.load_from(0).unwrap(); 69 | assert_eq!(all.len(), 2); 70 | assert_eq!(all[0].seq, 0); 71 | assert_eq!(all[0].payload, b"first"); 72 | assert_eq!(all[1].seq, 1); 73 | assert_eq!(all[1].kind, JournalKind::EffectIntent); 74 | 75 | let tail = journal.load_from(1).unwrap(); 76 | assert_eq!(tail.len(), 1); 77 | assert_eq!(tail[0].payload, b"second"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/03-fetch-notify/air/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/FetchNotifyPc@1" 6 | }, 7 | { 8 | "name": "demo/FetchNotifyState@1" 9 | }, 10 | { 11 | "name": "demo/FetchNotifyEvent@1" 12 | }, 13 | { 14 | "name": "demo/FetchRequest@1" 15 | }, 16 | { 17 | "name": "sys/BlobPutParams@1" 18 | }, 19 | { 20 | "name": "sys/BlobPutReceipt@1" 21 | }, 22 | { 23 | "name": "sys/BlobGetParams@1" 24 | }, 25 | { 26 | "name": "sys/BlobGetReceipt@1" 27 | }, 28 | { 29 | "name": "sys/BlobPutResult@1" 30 | }, 31 | { 32 | "name": "sys/BlobGetResult@1" 33 | }, 34 | { 35 | "name": "sys/HttpRequestParams@1" 36 | }, 37 | { 38 | "name": "sys/HttpRequestReceipt@1" 39 | } 40 | ], 41 | "modules": [ 42 | { 43 | "name": "demo/FetchNotify@1" 44 | } 45 | ], 46 | "plans": [ 47 | { 48 | "name": "demo/fetch_plan@1" 49 | } 50 | ], 51 | "caps": [ 52 | { 53 | "name": "demo/http_fetch_cap@1" 54 | } 55 | ], 56 | "policies": [ 57 | { 58 | "name": "demo/http-policy@1" 59 | } 60 | ], 61 | "defaults": { 62 | "policy": "demo/http-policy@1", 63 | "cap_grants": [ 64 | { 65 | "name": "cap_http_fetch", 66 | "cap": "demo/http_fetch_cap@1", 67 | "params": { 68 | "record": { 69 | "hosts": { 70 | "set": [ 71 | { 72 | "text": "example.com" 73 | } 74 | ] 75 | }, 76 | "verbs": { 77 | "set": [ 78 | { 79 | "text": "GET" 80 | } 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | ] 87 | }, 88 | "module_bindings": {}, 89 | "routing": { 90 | "events": [ 91 | { 92 | "event": "demo/FetchNotifyEvent@1", 93 | "reducer": "demo/FetchNotify@1" 94 | } 95 | ], 96 | "inboxes": [] 97 | }, 98 | "triggers": [ 99 | { 100 | "event": "demo/FetchRequest@1", 101 | "plan": "demo/fetch_plan@1", 102 | "correlate_by": "request_id" 103 | } 104 | ], 105 | "air_version": "1", 106 | "effects": [ 107 | { 108 | "name": "sys/http.request@1" 109 | }, 110 | { 111 | "name": "sys/blob.put@1" 112 | }, 113 | { 114 | "name": "sys/blob.get@1" 115 | } 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /examples/04-aggregator/air/schemas.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defschema", 4 | "name": "demo/AggregatorPc@1", 5 | "type": { 6 | "variant": { 7 | "Idle": { "unit": {} }, 8 | "Running": { "unit": {} }, 9 | "Done": { "unit": {} } 10 | } 11 | } 12 | }, 13 | { 14 | "$kind": "defschema", 15 | "name": "demo/AggregatorState@1", 16 | "type": { 17 | "record": { 18 | "pc": { "ref": "demo/AggregatorPc@1" }, 19 | "next_request_id": { "nat": {} }, 20 | "pending_request": { "option": { "nat": {} } }, 21 | "current_topic": { "option": { "text": {} } }, 22 | "pending_targets": { "list": { "text": {} } }, 23 | "last_responses": { "list": { "ref": "demo/AggregateResponse@1" } } 24 | } 25 | } 26 | }, 27 | { 28 | "$kind": "defschema", 29 | "name": "demo/AggregationTarget@1", 30 | "type": { 31 | "record": { 32 | "name": { "text": {} }, 33 | "url": { "text": {} }, 34 | "method": { "text": {} } 35 | } 36 | } 37 | }, 38 | { 39 | "$kind": "defschema", 40 | "name": "demo/AggregateResponse@1", 41 | "type": { 42 | "record": { 43 | "source": { "text": {} }, 44 | "status": { "int": {} }, 45 | "body_preview": { "text": {} } 46 | } 47 | } 48 | }, 49 | { 50 | "$kind": "defschema", 51 | "name": "demo/AggregatorEvent@1", 52 | "type": { 53 | "variant": { 54 | "Start": { 55 | "record": { 56 | "topic": { "text": {} }, 57 | "primary": { "ref": "demo/AggregationTarget@1" }, 58 | "secondary": { "ref": "demo/AggregationTarget@1" }, 59 | "tertiary": { "ref": "demo/AggregationTarget@1" } 60 | } 61 | }, 62 | "AggregateComplete": { 63 | "record": { 64 | "request_id": { "nat": {} }, 65 | "topic": { "text": {} }, 66 | "primary": { "ref": "demo/AggregateResponse@1" }, 67 | "secondary": { "ref": "demo/AggregateResponse@1" }, 68 | "tertiary": { "ref": "demo/AggregateResponse@1" } 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | { 75 | "$kind": "defschema", 76 | "name": "demo/AggregateRequested@1", 77 | "type": { 78 | "record": { 79 | "request_id": { "nat": {} }, 80 | "topic": { "text": {} }, 81 | "primary": { "ref": "demo/AggregationTarget@1" }, 82 | "secondary": { "ref": "demo/AggregationTarget@1" }, 83 | "tertiary": { "ref": "demo/AggregationTarget@1" } 84 | } 85 | } 86 | } 87 | ] 88 | -------------------------------------------------------------------------------- /examples/05-chain-comp/air/charge_plan.air.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "$kind": "defplan", 4 | "name": "demo/charge_plan@1", 5 | "input": "demo/ChargeRequested@1", 6 | "steps": [ 7 | { 8 | "id": "emit_charge", 9 | "op": "emit_effect", 10 | "kind": "http.request", 11 | "params": { 12 | "record": { 13 | "method": { 14 | "op": "get", 15 | "args": [ 16 | { "ref": "@plan.input.target" }, 17 | { "text": "method" } 18 | ] 19 | }, 20 | "url": { 21 | "op": "get", 22 | "args": [ 23 | { "ref": "@plan.input.target" }, 24 | { "text": "url" } 25 | ] 26 | }, 27 | "headers": { "map": [] }, 28 | "body_ref": { "null": {} } 29 | } 30 | }, 31 | "cap": "cap_http_chain", 32 | "bind": { "effect_id_as": "charge_req" } 33 | }, 34 | { 35 | "id": "await_charge", 36 | "op": "await_receipt", 37 | "for": { "ref": "@var:charge_req" }, 38 | "bind": { "as": "charge_receipt" } 39 | }, 40 | { 41 | "id": "raise_result", 42 | "op": "raise_event", 43 | "event": "demo/ChainEvent@1", 44 | "value": { 45 | "variant": { 46 | "tag": "ChargeCompleted", 47 | "value": { 48 | "record": { 49 | "request_id": { 50 | "op": "get", 51 | "args": [ 52 | { "ref": "@plan.input" }, 53 | { "text": "request_id" } 54 | ] 55 | }, 56 | "status": { 57 | "op": "get", 58 | "args": [ 59 | { "ref": "@var:charge_receipt" }, 60 | { "text": "status" } 61 | ] 62 | }, 63 | "body_preview": { 64 | "op": "get", 65 | "args": [ 66 | { "ref": "@var:charge_receipt" }, 67 | { "text": "body_preview" } 68 | ] 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | { 76 | "id": "finish", 77 | "op": "end" 78 | } 79 | ], 80 | "edges": [ 81 | { "from": "emit_charge", "to": "await_charge" }, 82 | { "from": "await_charge", "to": "raise_result" }, 83 | { "from": "raise_result", "to": "finish" } 84 | ], 85 | "required_caps": ["cap_http_chain"], 86 | "allowed_effects": ["http.request"] 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /crates/aos-examples/tests/cli.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::prelude::*; 2 | use predicates::prelude::*; 3 | use std::process::Command; 4 | 5 | const CLI_SMOKE_TESTS: &[&str] = &[ 6 | "counter", 7 | "hello-timer", 8 | "blob-echo", 9 | "fetch-notify", 10 | "aggregator", 11 | "chain-comp", 12 | "safe-upgrade", 13 | "llm-summarizer", 14 | ]; 15 | 16 | #[test] 17 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 18 | fn counter_cli_runs() { 19 | run_cli_smoke("counter"); 20 | } 21 | 22 | #[test] 23 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 24 | fn hello_timer_cli_runs() { 25 | run_cli_smoke("hello-timer"); 26 | } 27 | 28 | #[test] 29 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 30 | fn blob_echo_cli_runs() { 31 | run_cli_smoke("blob-echo"); 32 | } 33 | 34 | #[test] 35 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 36 | fn fetch_notify_cli_runs() { 37 | run_cli_smoke("fetch-notify"); 38 | } 39 | 40 | #[test] 41 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 42 | fn aggregator_cli_runs() { 43 | run_cli_smoke("aggregator"); 44 | } 45 | 46 | #[test] 47 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 48 | fn chain_comp_cli_runs() { 49 | run_cli_smoke("chain-comp"); 50 | } 51 | 52 | #[test] 53 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 54 | fn safe_upgrade_cli_runs() { 55 | run_cli_smoke("safe-upgrade"); 56 | } 57 | 58 | #[test] 59 | #[ignore = "CLI smoke tests are opt-in to keep default test runs fast"] 60 | fn llm_summarizer_cli_runs() { 61 | run_cli_smoke("llm-summarizer"); 62 | } 63 | 64 | fn run_cli_smoke(subcommand: &str) { 65 | if !wasm_target_installed() { 66 | eprintln!("skipping {subcommand} CLI test (missing wasm target)"); 67 | return; 68 | } 69 | assert!( 70 | CLI_SMOKE_TESTS.contains(&subcommand), 71 | "unknown CLI example '{subcommand}'" 72 | ); 73 | run_cli_example(subcommand, &format!("({subcommand})")); 74 | } 75 | 76 | fn run_cli_example(subcommand: &str, expected_snippet: &str) { 77 | let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("aos-examples")); 78 | cmd.arg(subcommand).env("RUST_LOG", "error"); 79 | cmd.assert() 80 | .success() 81 | .stdout(predicate::str::contains(expected_snippet)); 82 | } 83 | 84 | fn wasm_target_installed() -> bool { 85 | Command::new("rustup") 86 | .args(["target", "list", "--installed"]) 87 | .output() 88 | .ok() 89 | .map(|out| String::from_utf8_lossy(&out.stdout).contains("wasm32-unknown-unknown")) 90 | .unwrap_or(false) 91 | } 92 | -------------------------------------------------------------------------------- /crates/aos-host/tests/daemon_integration.rs: -------------------------------------------------------------------------------- 1 | //! Integration test covering the daemon timer path end-to-end. 2 | 3 | use std::sync::Arc; 4 | use std::time::Duration; 5 | 6 | use aos_host::config::HostConfig; 7 | use helpers::fixtures::{self, START_SCHEMA, TestStore}; 8 | use aos_host::modes::daemon::{ControlMsg, WorldDaemon}; 9 | use aos_host::{ExternalEvent, WorldHost}; 10 | use aos_kernel::Kernel; 11 | use aos_kernel::journal::mem::MemJournal; 12 | use tokio::sync::{broadcast, mpsc, oneshot}; 13 | 14 | // Re-use shared helpers defined for other integration tests. 15 | #[path = "helpers.rs"] 16 | mod helpers; 17 | 18 | use helpers::timer_manifest; 19 | 20 | /// Ensure a reducer-emitted `timer.set` is scheduled, fired by the daemon, 21 | /// and routed to the handler reducer. 22 | #[tokio::test] 23 | async fn daemon_fires_timer_and_routes_event() { 24 | // Build in-memory world with timer-emitting reducer + handler. 25 | let store: Arc = fixtures::new_mem_store(); 26 | let manifest = timer_manifest(&store); 27 | let kernel = 28 | Kernel::from_loaded_manifest(store.clone(), manifest, Box::new(MemJournal::new())).unwrap(); 29 | let host = WorldHost::from_kernel(kernel, store.clone(), HostConfig::default()); 30 | 31 | let (control_tx, control_rx) = mpsc::channel(8); 32 | let (shutdown_tx, shutdown_rx) = broadcast::channel(1); 33 | 34 | // Spawn daemon; it returns itself so we can inspect final state. 35 | let mut daemon = WorldDaemon::new(host, control_rx, shutdown_rx, None); 36 | let handle = tokio::spawn(async move { (daemon.run().await, daemon) }); 37 | 38 | // Kick off the reducer that emits timer.set. 39 | let start_value = serde_cbor::to_vec(&serde_json::json!({ 40 | "id": "t1" 41 | })) 42 | .unwrap(); 43 | let (resp_tx, resp_rx) = oneshot::channel(); 44 | control_tx 45 | .send(ControlMsg::EventSend { 46 | event: ExternalEvent::DomainEvent { 47 | schema: START_SCHEMA.into(), 48 | value: start_value, 49 | key: None, 50 | }, 51 | resp: resp_tx, 52 | }) 53 | .await 54 | .unwrap(); 55 | let _ = resp_rx.await; 56 | 57 | // Allow the daemon loop to schedule and fire the timer. 58 | tokio::time::sleep(Duration::from_millis(50)).await; 59 | 60 | // Request shutdown to let the daemon exit cleanly. 61 | shutdown_tx.send(()).unwrap(); 62 | 63 | let (result, daemon) = handle.await.unwrap(); 64 | result.unwrap(); 65 | 66 | // Timer handler should have been invoked, setting its stub state to 0xCC. 67 | let state = daemon 68 | .host() 69 | .kernel() 70 | .reducer_state("com.acme/TimerHandler@1"); 71 | assert_eq!(state, Some(vec![0xCC])); 72 | } 73 | -------------------------------------------------------------------------------- /crates/aos-wasm-build/src/backends/rust.rs: -------------------------------------------------------------------------------- 1 | use crate::artifact::BuildArtifact; 2 | use crate::builder::BuildRequest; 3 | use crate::error::BuildError; 4 | use crate::hash::WasmDigest; 5 | use crate::util::resolve_cargo; 6 | use std::fs; 7 | use std::path::{Path, PathBuf}; 8 | use std::process::Command; 9 | use tempfile::TempDir; 10 | 11 | pub struct RustBackend; 12 | 13 | impl RustBackend { 14 | pub fn new() -> Self { 15 | Self 16 | } 17 | 18 | fn run_cargo(&self, request: &BuildRequest, temp_out: &TempDir) -> Result { 19 | let cargo = resolve_cargo().map_err(|e| BuildError::CargoNotFound(e.to_string()))?; 20 | let mut cmd = Command::new(cargo); 21 | cmd.current_dir(&request.source_dir); 22 | cmd.arg("build"); 23 | cmd.arg("--target"); 24 | cmd.arg(&request.config.toolchain.target); 25 | if request.config.release { 26 | cmd.arg("--release"); 27 | } 28 | cmd.env("CARGO_TARGET_DIR", temp_out.path()); 29 | let output = cmd 30 | .output() 31 | .map_err(|e| BuildError::BuildFailed(e.to_string()))?; 32 | if !output.status.success() { 33 | let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); 34 | return Err(BuildError::BuildFailed(stderr)); 35 | } 36 | let profile_dir = temp_out.path().join(&request.config.toolchain.target).join( 37 | if request.config.release { 38 | "release" 39 | } else { 40 | "debug" 41 | }, 42 | ); 43 | let wasm_path = find_wasm_artifact(&profile_dir)?; 44 | Ok(wasm_path) 45 | } 46 | } 47 | 48 | impl crate::backends::ModuleCompiler for RustBackend { 49 | fn compile(&self, request: BuildRequest) -> Result { 50 | let temp_out = TempDir::new().map_err(BuildError::Io)?; 51 | let wasm_path = self.run_cargo(&request, &temp_out)?; 52 | let wasm_bytes = fs::read(&wasm_path).map_err(BuildError::Io)?; 53 | let digest = WasmDigest::of_bytes(&wasm_bytes); 54 | Ok(BuildArtifact { 55 | wasm_bytes, 56 | wasm_hash: digest, 57 | build_log: None, 58 | }) 59 | } 60 | } 61 | 62 | fn find_wasm_artifact(dir: &Path) -> Result { 63 | let mut candidates = Vec::new(); 64 | for entry in fs::read_dir(dir).map_err(BuildError::Io)? { 65 | let entry = entry.map_err(BuildError::Io)?; 66 | let path = entry.path(); 67 | if path.extension().and_then(|ext| ext.to_str()) == Some("wasm") { 68 | candidates.push(path); 69 | } 70 | } 71 | candidates 72 | .into_iter() 73 | .next() 74 | .ok_or_else(|| BuildError::ArtifactNotFound(dir.to_path_buf())) 75 | } 76 | -------------------------------------------------------------------------------- /examples/06-safe-upgrade/air.v2/manifest.air.json: -------------------------------------------------------------------------------- 1 | { 2 | "$kind": "manifest", 3 | "schemas": [ 4 | { 5 | "name": "demo/SafeUpgradePc@1" 6 | }, 7 | { 8 | "name": "demo/SafeUpgradeState@1" 9 | }, 10 | { 11 | "name": "demo/SafeUpgradeEvent@1" 12 | }, 13 | { 14 | "name": "demo/UpgradeFetchRequest@1" 15 | }, 16 | { 17 | "name": "demo/SafeUpgradeResult@1" 18 | }, 19 | { 20 | "name": "sys/HttpRequestParams@1" 21 | }, 22 | { 23 | "name": "sys/HttpRequestReceipt@1" 24 | } 25 | ], 26 | "modules": [ 27 | { 28 | "name": "demo/SafeUpgrade@1" 29 | } 30 | ], 31 | "plans": [ 32 | { 33 | "name": "demo/fetch_plan@2" 34 | } 35 | ], 36 | "caps": [ 37 | { 38 | "name": "demo/http_fetch_cap@1" 39 | }, 40 | { 41 | "name": "demo/http_followup_cap@1" 42 | } 43 | ], 44 | "policies": [ 45 | { 46 | "name": "demo/http-policy@2" 47 | } 48 | ], 49 | "defaults": { 50 | "policy": "demo/http-policy@2", 51 | "cap_grants": [ 52 | { 53 | "name": "cap_http_fetch", 54 | "cap": "demo/http_fetch_cap@1", 55 | "params": { 56 | "record": { 57 | "hosts": { 58 | "set": [ 59 | { 60 | "text": "example.com" 61 | } 62 | ] 63 | }, 64 | "methods": { 65 | "set": [ 66 | { 67 | "text": "GET" 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | }, 74 | { 75 | "name": "cap_http_followup", 76 | "cap": "demo/http_followup_cap@1", 77 | "params": { 78 | "record": { 79 | "hosts": { 80 | "set": [ 81 | { 82 | "text": "example.com" 83 | } 84 | ] 85 | }, 86 | "methods": { 87 | "set": [ 88 | { 89 | "text": "GET" 90 | } 91 | ] 92 | } 93 | } 94 | } 95 | } 96 | ] 97 | }, 98 | "module_bindings": {}, 99 | "routing": { 100 | "events": [ 101 | { 102 | "event": "demo/SafeUpgradeEvent@1", 103 | "reducer": "demo/SafeUpgrade@1" 104 | } 105 | ], 106 | "inboxes": [] 107 | }, 108 | "triggers": [ 109 | { 110 | "event": "demo/UpgradeFetchRequest@1", 111 | "plan": "demo/fetch_plan@2", 112 | "correlate_by": "request_id" 113 | } 114 | ], 115 | "air_version": "1", 116 | "effects": [ 117 | { 118 | "name": "sys/http.request@1" 119 | } 120 | ] 121 | } 122 | -------------------------------------------------------------------------------- /crates/aos-cli/tests/patch_dir.rs: -------------------------------------------------------------------------------- 1 | use aos_cbor::Hash; 2 | use assert_cmd::prelude::*; 3 | use std::fs; 4 | use tempfile::tempdir; 5 | 6 | // Uses the built binary to exercise --patch-dir dry-run and inspect output. 7 | #[test] 8 | fn patch_dir_dry_run_emits_patchdoc_with_base_and_refs() { 9 | let tmp = tempdir().expect("tmpdir"); 10 | let world = tmp.path().join("world"); 11 | fs::create_dir_all(world.join(".aos")).unwrap(); 12 | fs::create_dir_all(world.join("air")).unwrap(); 13 | 14 | // Write a minimal manifest and a defschema into air/ so load_from_assets can find them. 15 | fs::write( 16 | world.join("air/manifest.air.json"), 17 | r#"{ 18 | "$kind":"manifest", 19 | "air_version":"1", 20 | "schemas": [ { "name": "com.acme/Added@1", "hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" } ], 21 | "modules": [], 22 | "plans": [], 23 | "effects": [], 24 | "caps": [], 25 | "policies": [], 26 | "secrets": [], 27 | "routing": null, 28 | "triggers": [] 29 | }"#, 30 | ) 31 | .unwrap(); 32 | fs::write( 33 | world.join("air/defs.schema.json"), 34 | r#"[ { "$kind":"defschema", "name":"com.acme/Added@1", "type": { "bool": {} } } ]"#, 35 | ) 36 | .unwrap(); 37 | 38 | // Create a dummy current manifest in the store to derive base hash (world/.aos/manifest.air.cbor). 39 | let manifest_bytes = fs::read(world.join("air/manifest.air.json")).unwrap(); 40 | // For tests we can just store the JSON bytes; the real CLI patches from the store. 41 | fs::write(world.join(".aos/manifest.air.cbor"), &manifest_bytes).unwrap(); 42 | let base_hash = Hash::of_bytes(&manifest_bytes).to_hex(); 43 | 44 | // Run CLI in dry-run mode and capture stdout. 45 | let mut cmd = std::process::Command::new(assert_cmd::cargo::cargo_bin!("aos")); 46 | cmd.current_dir(&world) 47 | .arg("gov") 48 | .arg("propose") 49 | .arg("--patch-dir") 50 | .arg("air") 51 | .arg("--dry-run"); 52 | let output = cmd.assert().success().get_output().stdout.clone(); 53 | let text = String::from_utf8(output).unwrap(); 54 | 55 | // Parse and inspect the emitted PatchDocument JSON. 56 | let doc: serde_json::Value = serde_json::from_str(&text).expect("json"); 57 | assert_eq!( 58 | doc["base_manifest_hash"].as_str().unwrap(), 59 | base_hash, 60 | "base hash should match current manifest" 61 | ); 62 | let patches = doc["patches"].as_array().unwrap(); 63 | assert!( 64 | patches.iter().any(|p| p.get("add_def").is_some()), 65 | "should include add_def for schema" 66 | ); 67 | assert!( 68 | patches.iter().any(|p| p.get("set_manifest_refs").is_some()), 69 | "should include set_manifest_refs" 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /crates/aos-examples/src/fetch_notify.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::{Result, anyhow}; 4 | use aos_wasm_sdk::aos_variant; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::example_host::{ExampleHost, HarnessConfig}; 8 | use aos_host::adapters::mock::{MockHttpHarness, MockHttpResponse}; 9 | 10 | const REDUCER_NAME: &str = "demo/FetchNotify@1"; 11 | const EVENT_SCHEMA: &str = "demo/FetchNotifyEvent@1"; 12 | const MODULE_PATH: &str = "examples/03-fetch-notify/reducer"; 13 | aos_variant! { 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | enum FetchEventEnvelope { 16 | Start { url: String, method: String }, 17 | NotifyComplete { status: i64, body_preview: String }, 18 | } 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | struct FetchStateView { 23 | pc: FetchPcView, 24 | next_request_id: u64, 25 | pending_request: Option, 26 | last_status: Option, 27 | last_body_preview: Option, 28 | } 29 | 30 | aos_variant! { 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | enum FetchPcView { 33 | Idle, 34 | Fetching, 35 | Done, 36 | } 37 | } 38 | 39 | pub fn run(example_root: &Path) -> Result<()> { 40 | let mut host = ExampleHost::prepare(HarnessConfig { 41 | example_root, 42 | assets_root: None, 43 | reducer_name: REDUCER_NAME, 44 | event_schema: EVENT_SCHEMA, 45 | module_crate: MODULE_PATH, 46 | })?; 47 | 48 | println!("→ Fetch & Notify demo"); 49 | let start_event = FetchEventEnvelope::Start { 50 | url: "https://example.com/data.json".into(), 51 | method: "GET".into(), 52 | }; 53 | if let FetchEventEnvelope::Start { url, method } = &start_event { 54 | println!(" start fetch → url={url} method={method}"); 55 | } 56 | host.send_event(&start_event)?; 57 | 58 | let mut http = MockHttpHarness::new(); 59 | let requests = http.collect_requests(host.kernel_mut())?; 60 | if requests.len() != 1 { 61 | return Err(anyhow!( 62 | "fetch-notify demo expected a single http request, got {}", 63 | requests.len() 64 | )); 65 | } 66 | let request = requests.into_iter().next().expect("one request"); 67 | println!( 68 | " http.request {} {}", 69 | request.params.method, request.params.url 70 | ); 71 | let body = format!( 72 | "{{\"url\":\"{}\",\"method\":\"{}\",\"demo\":true}}", 73 | request.params.url, request.params.method 74 | ); 75 | http.respond_with( 76 | host.kernel_mut(), 77 | request, 78 | MockHttpResponse::json(200, body), 79 | )?; 80 | 81 | let state: FetchStateView = host.read_state()?; 82 | println!( 83 | " completed: pc={:?} status={:?} preview={:?}", 84 | state.pc, state.last_status, state.last_body_preview 85 | ); 86 | 87 | host.finish()?.verify_replay()?; 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /crates/aos-host/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::Duration; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct HostConfig { 6 | pub effect_timeout: Duration, 7 | /// Optional directory for kernel module cache; if None, kernel chooses default. 8 | pub module_cache_dir: Option, 9 | /// Whether to load modules eagerly on startup. 10 | pub eager_module_load: bool, 11 | /// Allow placeholder secrets when no resolver is configured. 12 | pub allow_placeholder_secrets: bool, 13 | /// HTTP adapter configuration (always present). 14 | pub http: HttpAdapterConfig, 15 | /// LLM adapter configuration (None disables registration). 16 | pub llm: Option, 17 | } 18 | 19 | impl Default for HostConfig { 20 | fn default() -> Self { 21 | Self { 22 | effect_timeout: Duration::from_secs(30), 23 | module_cache_dir: None, 24 | eager_module_load: false, 25 | allow_placeholder_secrets: false, 26 | http: HttpAdapterConfig::default(), 27 | llm: LlmAdapterConfig::from_env().ok(), 28 | } 29 | } 30 | } 31 | 32 | impl HostConfig { 33 | /// Build HostConfig using environment defaults (currently same as Default). 34 | pub fn from_env() -> Self { 35 | Self::default() 36 | } 37 | } 38 | 39 | /// Configuration for the HTTP adapter. 40 | #[derive(Debug, Clone)] 41 | pub struct HttpAdapterConfig { 42 | /// Default timeout for requests. 43 | pub timeout: Duration, 44 | /// Maximum response body size in bytes. 45 | pub max_body_size: usize, 46 | } 47 | 48 | impl Default for HttpAdapterConfig { 49 | fn default() -> Self { 50 | Self { 51 | timeout: Duration::from_secs(30), 52 | max_body_size: 10 * 1024 * 1024, // 10MB 53 | } 54 | } 55 | } 56 | 57 | /// Per-provider configuration for LLM adapter. 58 | #[derive(Debug, Clone)] 59 | pub struct ProviderConfig { 60 | pub base_url: String, 61 | pub timeout: Duration, 62 | } 63 | 64 | /// Configuration for the LLM adapter. 65 | #[derive(Debug, Clone)] 66 | pub struct LlmAdapterConfig { 67 | pub providers: HashMap, 68 | pub default_provider: String, 69 | } 70 | 71 | impl LlmAdapterConfig { 72 | /// Build provider map from environment; returns error only on malformed input. 73 | pub fn from_env() -> Result { 74 | let base_url = 75 | std::env::var("OPENAI_BASE_URL").unwrap_or_else(|_| "https://api.openai.com/v1".into()); 76 | 77 | let mut providers = HashMap::new(); 78 | providers.insert( 79 | "openai".into(), 80 | ProviderConfig { 81 | base_url, 82 | timeout: Duration::from_secs(120), 83 | }, 84 | ); 85 | 86 | Ok(Self { 87 | providers, 88 | default_provider: "openai".into(), 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/aos-host/src/adapters/stub.rs: -------------------------------------------------------------------------------- 1 | use aos_effects::{EffectIntent, EffectReceipt, ReceiptStatus}; 2 | use async_trait::async_trait; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::traits::AsyncEffectAdapter; 6 | 7 | macro_rules! stub_adapter { 8 | ($name:ident, $kind:expr) => { 9 | pub struct $name; 10 | 11 | #[async_trait] 12 | impl AsyncEffectAdapter for $name { 13 | fn kind(&self) -> &str { 14 | $kind 15 | } 16 | 17 | async fn execute( 18 | &self, 19 | intent: &EffectIntent, 20 | ) -> anyhow::Result { 21 | Ok(EffectReceipt { 22 | intent_hash: intent.intent_hash, 23 | adapter_id: $kind.to_string(), 24 | status: ReceiptStatus::Ok, 25 | // CBOR empty map 0xa0 = {} - valid payload for most adapters 26 | payload_cbor: vec![0xa0], 27 | cost_cents: Some(0), 28 | signature: vec![0; 64], 29 | }) 30 | } 31 | } 32 | }; 33 | } 34 | 35 | stub_adapter!(StubHttpAdapter, "http.request"); 36 | stub_adapter!(StubLlmAdapter, "llm.generate"); 37 | stub_adapter!(StubBlobAdapter, "blob.put"); 38 | stub_adapter!(StubBlobGetAdapter, "blob.get"); 39 | 40 | // Timer adapter requires a specific receipt format 41 | #[derive(Debug, Clone, Serialize, Deserialize)] 42 | struct TimerSetParams { 43 | deliver_at_ns: u64, 44 | #[serde(default, skip_serializing_if = "Option::is_none")] 45 | key: Option, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | struct TimerSetReceipt { 50 | delivered_at_ns: u64, 51 | #[serde(default, skip_serializing_if = "Option::is_none")] 52 | key: Option, 53 | } 54 | 55 | pub struct StubTimerAdapter; 56 | 57 | #[async_trait] 58 | impl AsyncEffectAdapter for StubTimerAdapter { 59 | fn kind(&self) -> &str { 60 | "timer.set" 61 | } 62 | 63 | async fn execute(&self, intent: &EffectIntent) -> anyhow::Result { 64 | // Parse the requested timer params to get deliver_at_ns 65 | let params: TimerSetParams = 66 | serde_cbor::from_slice(&intent.params_cbor).unwrap_or(TimerSetParams { 67 | deliver_at_ns: 0, 68 | key: None, 69 | }); 70 | 71 | // Create a receipt that says the timer fired at the requested time 72 | let receipt_payload = TimerSetReceipt { 73 | delivered_at_ns: params.deliver_at_ns, 74 | key: params.key, 75 | }; 76 | 77 | Ok(EffectReceipt { 78 | intent_hash: intent.intent_hash, 79 | adapter_id: "timer.set".to_string(), 80 | status: ReceiptStatus::Ok, 81 | payload_cbor: serde_cbor::to_vec(&receipt_payload)?, 82 | cost_cents: Some(0), 83 | signature: vec![0; 64], 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use aos_air_types::{ 4 | AirNode, DefCap, DefEffect, DefModule, DefPlan, DefPolicy, DefSchema, Manifest, Name, 5 | SecretDecl, catalog::EffectCatalog, validate_manifest, 6 | }; 7 | use aos_store::{Catalog, Store, load_manifest_from_path}; 8 | 9 | use crate::error::KernelError; 10 | 11 | pub struct LoadedManifest { 12 | pub manifest: Manifest, 13 | pub secrets: Vec, 14 | pub modules: HashMap, 15 | pub plans: HashMap, 16 | pub effects: HashMap, 17 | pub caps: HashMap, 18 | pub policies: HashMap, 19 | pub schemas: HashMap, 20 | pub effect_catalog: EffectCatalog, 21 | } 22 | 23 | pub struct ManifestLoader; 24 | 25 | impl ManifestLoader { 26 | pub fn load_from_path( 27 | store: &S, 28 | path: impl AsRef, 29 | ) -> Result { 30 | let catalog = load_manifest_from_path(store, path)?; 31 | Self::from_catalog(catalog) 32 | } 33 | 34 | fn from_catalog(catalog: Catalog) -> Result { 35 | let mut modules = HashMap::new(); 36 | let mut plans = HashMap::new(); 37 | let mut effects = HashMap::new(); 38 | let mut caps = HashMap::new(); 39 | let mut policies = HashMap::new(); 40 | let mut schemas = HashMap::new(); 41 | for (name, entry) in catalog.nodes { 42 | match entry.node { 43 | AirNode::Defmodule(module) => { 44 | modules.insert(name, module); 45 | } 46 | AirNode::Defplan(plan) => { 47 | plans.insert(name, plan); 48 | } 49 | AirNode::Defcap(cap) => { 50 | caps.insert(name, cap); 51 | } 52 | AirNode::Defpolicy(policy) => { 53 | policies.insert(name, policy); 54 | } 55 | AirNode::Defeffect(effect) => { 56 | effects.insert(name, effect); 57 | } 58 | AirNode::Defschema(schema) => { 59 | schemas.insert(name, schema); 60 | } 61 | _ => {} 62 | } 63 | } 64 | let manifest = catalog.manifest; 65 | validate_manifest( 66 | &manifest, &modules, &schemas, &plans, &effects, &caps, &policies, 67 | ) 68 | .map_err(|e| KernelError::ManifestValidation(e.to_string()))?; 69 | let effect_catalog = EffectCatalog::from_defs(effects.values().cloned()); 70 | Ok(LoadedManifest { 71 | manifest, 72 | secrets: catalog.resolved_secrets, 73 | modules, 74 | plans, 75 | effects, 76 | caps, 77 | policies, 78 | schemas, 79 | effect_catalog, 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/aos-kernel/src/query.rs: -------------------------------------------------------------------------------- 1 | use aos_air_types::Manifest; 2 | use aos_cbor::Hash; 3 | 4 | use crate::error::KernelError; 5 | use crate::journal::JournalSeq; 6 | 7 | /// Caller’s freshness preference for read-only queries. 8 | #[derive(Clone, Debug, PartialEq, Eq)] 9 | pub enum Consistency { 10 | /// Return the latest available state (may be slightly stale if replay is running). 11 | Head, 12 | /// Require the snapshot/journal to be exactly this height. 13 | Exact(JournalSeq), 14 | /// Serve the newest available state at or above this height. 15 | AtLeast(JournalSeq), 16 | } 17 | 18 | /// Metadata attached to every read response so callers can reason about what they saw. 19 | #[derive(Clone, Debug, PartialEq, Eq)] 20 | pub struct ReadMeta { 21 | pub journal_height: JournalSeq, 22 | pub snapshot_hash: Option, 23 | pub manifest_hash: Hash, 24 | } 25 | 26 | /// Envelope for read responses. 27 | #[derive(Clone, Debug, PartialEq, Eq)] 28 | pub struct StateRead { 29 | pub meta: ReadMeta, 30 | pub value: T, 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | 37 | #[test] 38 | fn consistency_variants() { 39 | let c1 = Consistency::Head; 40 | let c2 = Consistency::Exact(5); 41 | let c3 = Consistency::AtLeast(10); 42 | assert!(matches!(c1, Consistency::Head)); 43 | assert!(matches!(c2, Consistency::Exact(5))); 44 | assert!(matches!(c3, Consistency::AtLeast(10))); 45 | } 46 | 47 | #[test] 48 | fn read_meta_fields() { 49 | let h = Hash::of_bytes(b"abc"); 50 | let meta = ReadMeta { 51 | journal_height: 7, 52 | snapshot_hash: Some(h), 53 | manifest_hash: h, 54 | }; 55 | assert_eq!(meta.journal_height, 7); 56 | assert_eq!(meta.snapshot_hash, Some(h)); 57 | assert_eq!(meta.manifest_hash, h); 58 | } 59 | 60 | #[test] 61 | fn state_read_wraps_value() { 62 | let meta = ReadMeta { 63 | journal_height: 0, 64 | snapshot_hash: None, 65 | manifest_hash: Hash::of_bytes(b"m"), 66 | }; 67 | let sr = StateRead { 68 | meta: meta.clone(), 69 | value: Some(vec![1, 2, 3]), 70 | }; 71 | assert_eq!(sr.meta.journal_height, 0); 72 | assert_eq!(sr.value, Some(vec![1, 2, 3])); 73 | } 74 | } 75 | 76 | /// Read-only surface exposed by the kernel for observational queries. 77 | pub trait StateReader { 78 | /// Fetch reducer state (non-keyed or keyed cell) according to consistency preference. 79 | fn get_reducer_state( 80 | &self, 81 | module: &str, 82 | key: Option<&[u8]>, 83 | consistency: Consistency, 84 | ) -> Result>>, KernelError>; 85 | 86 | /// Fetch the manifest for inspection. 87 | fn get_manifest(&self, consistency: Consistency) -> Result, KernelError>; 88 | 89 | /// Return only consistency metadata (height + hashes). 90 | fn get_journal_head(&self) -> ReadMeta; 91 | } 92 | -------------------------------------------------------------------------------- /crates/aos-host/src/adapters/registry.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::Duration; 3 | 4 | use aos_effects::{EffectIntent, EffectReceipt, ReceiptStatus}; 5 | use tokio::time::timeout; 6 | 7 | use super::traits::AsyncEffectAdapter; 8 | 9 | #[derive(Clone)] 10 | pub struct AdapterRegistryConfig { 11 | pub effect_timeout: Duration, 12 | } 13 | 14 | impl Default for AdapterRegistryConfig { 15 | fn default() -> Self { 16 | Self { 17 | effect_timeout: Duration::from_secs(30), 18 | } 19 | } 20 | } 21 | 22 | pub struct AdapterRegistry { 23 | adapters: HashMap>, 24 | config: AdapterRegistryConfig, 25 | } 26 | 27 | impl AdapterRegistry { 28 | pub fn new(config: AdapterRegistryConfig) -> Self { 29 | Self { 30 | adapters: HashMap::new(), 31 | config, 32 | } 33 | } 34 | 35 | pub fn register(&mut self, adapter: Box) { 36 | self.adapters.insert(adapter.kind().to_string(), adapter); 37 | } 38 | 39 | pub fn get(&self, kind: &str) -> Option<&dyn AsyncEffectAdapter> { 40 | self.adapters.get(kind).map(|b| b.as_ref()) 41 | } 42 | 43 | pub async fn execute_batch(&self, intents: Vec) -> Vec { 44 | let mut receipts = Vec::with_capacity(intents.len()); 45 | 46 | for intent in intents { 47 | let receipt = match self.get(intent.kind.as_str()) { 48 | Some(adapter) => { 49 | match timeout(self.config.effect_timeout, adapter.execute(&intent)).await { 50 | Ok(Ok(receipt)) => receipt, 51 | Ok(Err(_err)) => EffectReceipt { 52 | intent_hash: intent.intent_hash, 53 | adapter_id: adapter.kind().to_string(), 54 | status: ReceiptStatus::Error, 55 | payload_cbor: vec![], 56 | cost_cents: None, 57 | signature: vec![], 58 | }, 59 | Err(_) => EffectReceipt { 60 | intent_hash: intent.intent_hash, 61 | adapter_id: adapter.kind().to_string(), 62 | status: ReceiptStatus::Timeout, 63 | payload_cbor: vec![], 64 | cost_cents: None, 65 | signature: vec![], 66 | }, 67 | } 68 | } 69 | None => EffectReceipt { 70 | intent_hash: intent.intent_hash, 71 | adapter_id: "adapter.missing".into(), 72 | status: ReceiptStatus::Error, 73 | payload_cbor: vec![], 74 | cost_cents: None, 75 | signature: vec![], 76 | }, 77 | }; 78 | receipts.push(receipt); 79 | } 80 | 81 | receipts 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/01-hello-timer/reducer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(improper_ctypes_definitions)] 2 | #![no_std] 3 | 4 | extern crate alloc; 5 | 6 | use alloc::string::String; 7 | use aos_wasm_sdk::{aos_reducer, aos_variant, aos_event_union, ReduceError, Reducer, ReducerCtx, TimerSetParams, Value}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | aos_reducer!(TimerSm); 11 | 12 | #[derive(Default)] 13 | struct TimerSm; 14 | 15 | impl Reducer for TimerSm { 16 | type State = TimerState; 17 | type Event = TimerEvent; 18 | type Ann = Value; 19 | 20 | fn reduce( 21 | &mut self, 22 | event: Self::Event, 23 | ctx: &mut ReducerCtx, 24 | ) -> Result<(), ReduceError> { 25 | match event { 26 | TimerEvent::Start(start) => handle_start(ctx, start), 27 | TimerEvent::Fired(_fired) => handle_timer_fired(ctx), 28 | } 29 | Ok(()) 30 | } 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 34 | struct TimerState { 35 | pc: TimerPc, 36 | key: Option, 37 | deadline_ns: Option, 38 | fired_key: Option, 39 | } 40 | 41 | aos_variant! { 42 | #[derive(Debug, Clone, Serialize, Deserialize)] 43 | enum TimerPc { 44 | Idle, 45 | Awaiting, 46 | Done, 47 | TimedOut, 48 | } 49 | } 50 | 51 | impl Default for TimerPc { 52 | fn default() -> Self { 53 | TimerPc::Idle 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, Serialize, Deserialize)] 58 | struct StartEvent { 59 | deliver_at_ns: u64, 60 | key: Option, 61 | } 62 | 63 | #[derive(Debug, Clone, Serialize, Deserialize)] 64 | struct TimerFiredEvent { 65 | requested: TimerSetParams, 66 | } 67 | 68 | aos_event_union! { 69 | #[derive(Debug, Clone, Serialize)] 70 | enum TimerEvent { 71 | Start(StartEvent), 72 | Fired(TimerFiredEvent) 73 | } 74 | } 75 | 76 | fn handle_start(ctx: &mut ReducerCtx, event: StartEvent) { 77 | if matches!(ctx.state.pc, TimerPc::Idle | TimerPc::Done | TimerPc::TimedOut) { 78 | ctx.state.pc = if event.deliver_at_ns == 0 { 79 | TimerPc::Done 80 | } else { 81 | TimerPc::Awaiting 82 | }; 83 | ctx.state.key = event.key.clone(); 84 | ctx.state.deadline_ns = Some(event.deliver_at_ns); 85 | ctx.state.fired_key = None; 86 | 87 | if let (TimerPc::Awaiting, Some(deadline)) = (&ctx.state.pc, ctx.state.deadline_ns) { 88 | let params = TimerSetParams { 89 | deliver_at_ns: deadline, 90 | key: ctx.state.key.clone(), 91 | }; 92 | ctx.effects().timer_set(¶ms, "timer"); 93 | } 94 | } 95 | } 96 | 97 | fn handle_timer_fired(ctx: &mut ReducerCtx) { 98 | if !matches!(ctx.state.pc, TimerPc::Awaiting) { 99 | return; 100 | } 101 | if ctx.state.deadline_ns.is_some() { 102 | ctx.state.pc = TimerPc::Done; 103 | ctx.state.fired_key = ctx.state.key.clone(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /crates/aos-air-types/src/tests/policies.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use std::panic::{self, AssertUnwindSafe}; 3 | 4 | use super::assert_json_schema; 5 | use crate::{DefPolicy, PolicyDecision, PolicyMatch, PolicyRule}; 6 | 7 | #[test] 8 | fn parses_policy_rules() { 9 | let policy_json = json!({ 10 | "$kind": "defpolicy", 11 | "name": "com.acme/policy@1", 12 | "rules": [ 13 | { 14 | "when": {"effect_kind": "http.request", "origin_kind": "plan"}, 15 | "decision": "allow" 16 | } 17 | ] 18 | }); 19 | assert_json_schema(crate::schemas::DEFPOLICY, &policy_json); 20 | let policy: DefPolicy = serde_json::from_value(policy_json).expect("policy json"); 21 | assert_eq!(policy.rules.len(), 1); 22 | assert!(matches!(policy.rules[0].decision, PolicyDecision::Allow)); 23 | } 24 | 25 | #[test] 26 | fn rule_without_decision_errors() { 27 | let bad_rule = json!({ 28 | "when": {"effect_kind": "http.request"} 29 | }); 30 | assert!(serde_json::from_value::(bad_rule).is_err()); 31 | } 32 | 33 | #[test] 34 | fn policy_match_serializes_round_trip() { 35 | let r#match = PolicyMatch { 36 | effect_kind: Some(crate::EffectKind::http_request()), 37 | cap_name: None, 38 | origin_kind: Some(crate::OriginKind::Plan), 39 | origin_name: Some("com.acme/Plan@1".into()), 40 | }; 41 | let json = serde_json::to_value(&r#match).expect("serialize"); 42 | let round_trip: PolicyMatch = serde_json::from_value(json).expect("deserialize"); 43 | assert_eq!(round_trip.origin_kind, Some(crate::OriginKind::Plan)); 44 | } 45 | 46 | #[test] 47 | fn policy_supports_multiple_rules_and_filters() { 48 | let policy_json = json!({ 49 | "$kind": "defpolicy", 50 | "name": "com.acme/policy@2", 51 | "rules": [ 52 | { 53 | "when": { 54 | "effect_kind": "http.request", 55 | "cap_name": "cap_http", 56 | "origin_kind": "plan", 57 | "origin_name": "com.acme/Plan@1" 58 | }, 59 | "decision": "allow" 60 | }, 61 | { 62 | "when": { 63 | "effect_kind": "llm.generate" 64 | }, 65 | "decision": "deny" 66 | } 67 | ] 68 | }); 69 | assert_json_schema(crate::schemas::DEFPOLICY, &policy_json); 70 | let policy: DefPolicy = serde_json::from_value(policy_json).expect("policy json"); 71 | assert_eq!(policy.rules.len(), 2); 72 | assert!(matches!(policy.rules[1].decision, PolicyDecision::Deny)); 73 | } 74 | 75 | #[test] 76 | fn rule_without_when_is_rejected_by_schema() { 77 | let rule = json!({ "decision": "allow" }); 78 | assert!(serde_json::from_value::(rule.clone()).is_err()); 79 | let policy_json = json!({ 80 | "$kind": "defpolicy", 81 | "name": "com.acme/policy@3", 82 | "rules": [rule] 83 | }); 84 | assert!( 85 | panic::catch_unwind(AssertUnwindSafe(|| assert_json_schema( 86 | crate::schemas::DEFPOLICY, 87 | &policy_json 88 | ))) 89 | .is_err() 90 | ); 91 | } 92 | --------------------------------------------------------------------------------