<div align="center">
  <img src="https://raw.githubusercontent.com/aiperceivable/apcore/main/apcore-logo.svg" alt="apcore logo" width="200"/>
</div>

# apcore

![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg)
![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12294/badge)](https://www.bestpractices.dev/projects/12294)

> **Build once, invoke by Code or AI.**

A schema-enforced module standard for the AI-Perceivable era.

**apcore** is an AI-Perceivable module standard that makes every interface naturally perceivable and understandable by AI through enforced Schema definitions and behavioral annotations. It provides strict type safety, access control, middleware pipelines, and built-in observability — enabling you to define modules with structured input/output schemas that are easily consumed by both code and AI.

## Features

- **Schema-driven modules** — Define input/output contracts using `schemars`-derived types with automatic validation
- **Execution Pipeline** — Context creation, call chain guard, ACL enforcement, approval gate, middleware before, validation, execution, output validation, middleware after, and return — with step metadata (`match_modules`, `ignore_errors`, `pure`, `timeout_ms`) and YAML pipeline configuration
- **`Module` trait** — Implement the `Module` trait to create fully schema-aware modules
- **YAML bindings** — Register modules declaratively without modifying source code
- **Access control (ACL)** — Pattern-based, first-match-wins rules with wildcard support
- **Middleware system** — Composable before/after hooks with error recovery
- **Observability** — Tracing (spans), metrics collection, and structured context logging
- **Async support** — Built on `tokio` for seamless async module execution
- **Safety guards** — Call depth limits, circular call detection, frequency throttling
- **Approval system** — Pluggable approval gate with async handlers, Phase B resume, and audit events
- **Behavioral annotations** — Declare module traits (readonly, destructive, idempotent, cacheable, paginated, streaming) for AI-aware orchestration
- **W3C Trace Context** — `traceparent` header injection/extraction for distributed tracing interop

## Cross-Language Feature Parity

The Rust SDK tracks the apcore protocol spec and ships full feature parity
with the Python and TypeScript SDKs:

| Feature | Python | TypeScript | Rust |
|---------|:------:|:----------:|:----:|
| `AsyncTaskManager` (background task execution) | Yes | Yes | Yes |
| `ExtensionManager` / `ExtensionPoint` (plugin registry) | Yes | Yes | Yes |

Both were reintroduced in 0.19.0 with full `Executor` integration. See the
[`AsyncTaskManager`](./src/async_task.rs) and [`ExtensionManager`](./src/extensions.rs)
source, plus the corresponding tests at `tests/test_async_task.rs` and
`tests/test_extensions.rs`.

## API Overview

**Core**

| Type | Description |
|------|-------------|
| `APCore` | High-level client — register modules, call, stream, validate |
| `Registry` | Module storage — discover, register, get, list (`watch()` is a no-op stub; use `reload()` or `discover()` instead) |
| `Executor` | Execution engine — call with middleware pipeline, ACL, approval |
| `Context` | Request context — trace ID, identity, call chain, cancel token |
| `Config` | Configuration — from_defaults with env overrides, load YAML/JSON, get/set dot-path, validate, reload |
| `Identity` | Caller identity — id, type, roles, attributes |
| `Module` | Core trait for implementing schema-aware modules |

**Access Control & Approval**

| Type | Description |
|------|-------------|
| `ACL` | Access control — rule-based caller/target authorization |
| `ApprovalHandler` | Pluggable approval gate trait |
| `AlwaysDenyHandler` / `AutoApproveHandler` | Built-in approval handlers |

**Middleware**

| Type | Description |
|------|-------------|
| `Middleware` | Pipeline hooks — before/after/on_error interception |
| `BeforeMiddleware` / `AfterMiddleware` | Single-phase middleware adapters |
| `ObsLoggingMiddleware` | Structured logging middleware |
| `RetryMiddleware` | Automatic retry with backoff |
| `ErrorHistoryMiddleware` | Records errors into `ErrorHistory` |
| `PlatformNotifyMiddleware` | Emits events on error rate/latency spikes |

**Schema**

| Type | Description |
|------|-------------|
| `SchemaLoader` | Load schemas from YAML or native types |
| `SchemaValidator` | Validate data against schemas |
| `SchemaExporter` | Export schemas for MCP, OpenAI, Anthropic, generic |
| `RefResolver` | Resolve `$ref` references in JSON Schema |

**Observability**

| Type | Description |
|------|-------------|
| `TracingMiddleware` | Distributed tracing with span export |
| `MetricsMiddleware` / `MetricsCollector` | Call count, latency, error rate metrics |
| `ContextLogger` | Context-aware structured logging |
| `ErrorHistory` | Ring buffer of recent errors with deduplication |
| `UsageCollector` | Per-module usage statistics and trends |

**Events & Utilities**

| Type | Description |
|------|-------------|
| `EventEmitter` | Event system — subscribe, unsubscribe, emit, emit_filtered, flush |
| `WebhookSubscriber` | Built-in event subscriber |
| `CancelToken` | Cooperative cancellation token |
| `BindingLoader` | Load modules from YAML binding files |

> See [Cross-Language Feature Parity](#cross-language-feature-parity) — as of 0.19.0,
> `ExtensionManager` and `AsyncTaskManager` are available in all three SDKs.

## Documentation

For full documentation, including Quick Start guides for Python and Rust, visit:
**[https://aiperceivable.github.io/apcore/getting-started.html](https://aiperceivable.github.io/apcore/getting-started.html)**

## Requirements

- Rust >= 1.75
- Tokio async runtime

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
apcore = "0.19"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
```

## Quick Start

### Simple client

```rust
use apcore::APCore;
use apcore::module::Module;
use apcore::context::Context;
use serde_json::{json, Value};

struct AddModule;

#[async_trait::async_trait]
impl Module for AddModule {
    fn description(&self) -> &str { "Add two integers" }

    fn input_schema(&self) -> Value {
        json!({"type": "object", "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}, "required": ["a", "b"]})
    }
    fn output_schema(&self) -> Value {
        json!({"type": "object", "properties": {"result": {"type": "integer"}}})
    }

    async fn execute(
        &self,
        input: Value,
        _ctx: &Context<Value>,
    ) -> Result<Value, apcore::errors::ModuleError> {
        let a = input["a"].as_i64().unwrap_or(0);
        let b = input["b"].as_i64().unwrap_or(0);
        Ok(json!({ "result": a + b }))
    }
}

#[tokio::main]
async fn main() {
    let mut client = APCore::new();
    client.register("math.add", Box::new(AddModule)).unwrap();

    let result = client
        .call("math.add", json!({"a": 10, "b": 5}), None, None)
        .await
        .unwrap();
    println!("{}", result); // {"result": 15}
}
```

### With configuration

```rust
use apcore::APCore;

#[tokio::main]
async fn main() {
    // Load directly from file path
    let client = APCore::from_path("apcore.yaml").unwrap();

    // Or load and modify config before constructing
    // let config = Config::from_yaml_file(Path::new("apcore.yaml")).unwrap();
    // let client = APCore::with_config(config);
}
```

### Module with typed schemas

```rust
use apcore::module::{Module, ModuleAnnotations};
use apcore::context::Context;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

#[derive(Serialize, Deserialize)]
struct GetUserInput {
    user_id: String,
}

#[derive(Serialize, Deserialize)]
struct GetUserOutput {
    id: String,
    name: String,
    email: String,
}

struct GetUserModule;

#[async_trait::async_trait]
impl Module for GetUserModule {
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "user_id": { "type": "string" }
            },
            "required": ["user_id"]
        })
    }

    fn output_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "id": { "type": "string" },
                "name": { "type": "string" },
                "email": { "type": "string" }
            }
        })
    }

    fn description(&self) -> &str { "Get user details by ID" }

    // Annotations (readonly, idempotent, etc.) are set on
    // ModuleDescriptor when registering the module with the registry.

    async fn execute(
        &self,
        input: Value,
        _ctx: &Context<Value>,
    ) -> Result<Value, apcore::errors::ModuleError> {
        let req: GetUserInput = serde_json::from_value(input)?;
        let user = match req.user_id.as_str() {
            "user-1" => GetUserOutput { id: "user-1".into(), name: "Alice".into(), email: "alice@example.com".into() },
            "user-2" => GetUserOutput { id: "user-2".into(), name: "Bob".into(),   email: "bob@example.com".into() },
            id       => GetUserOutput { id: id.into(),       name: "Unknown".into(), email: "unknown@example.com".into() },
        };
        Ok(serde_json::to_value(user)?)
    }
}
```

### Add middleware

```rust
use apcore::observability::{ContextLogger, ObsLoggingMiddleware};

client.use_middleware(Box::new(ObsLoggingMiddleware::new(ContextLogger::new("app"))));
// TracingMiddleware requires a SpanExporter — see observability docs
```

### Access control

```rust
use apcore::acl::{ACL, ACLRule};

let acl = ACL::new(vec![
    ACLRule { callers: vec!["admin.*".into()], targets: vec!["*".into()], effect: "allow".into(), description: Some("Admins can call anything".into()), conditions: None },
    ACLRule { callers: vec!["*".into()], targets: vec!["admin.*".into()], effect: "deny".into(), description: Some("Others cannot call admin modules".into()), conditions: None },
], "deny", None);
```

### YAML bindings

Register modules without touching Rust source — define a `binding.yaml`:

```yaml
bindings:
  - module_id: "utils.format_date"
    target: "format_date::format_date_string"
    description: "Format a date string into a specified format"
    tags: ["utility", "date"]
    version: "1.0.0"
    input_schema:
      type: object
      properties:
        date_string:   { type: string }
        output_format: { type: string }
      required: [date_string, output_format]
    output_schema:
      type: object
      properties:
        formatted: { type: string }
      required: [formatted]
```

Load it at runtime:

```rust
use apcore::bindings::BindingLoader;

let mut loader = BindingLoader::new();
loader.load_from_yaml(std::path::Path::new("binding.yaml")).unwrap();
```

### Annotation overlay (cross-SDK difference)

The Python and TypeScript SDKs support `*_meta.yaml` sidecar files that override
code-defined module annotations at load time (PROTOCOL_SPEC.md  §4.13: field-level
merge, YAML wins over code). The Rust SDK **does not** implement this feature.

This is a deliberate design choice:

- **Spec §4.13 is conditional**: it mandates field-level merge only "*when both* YAML
  metadata file and code define Annotations". If the SDK never loads YAML metadata
  files, the rule is never triggered.
- **Rust favours explicit configuration**: annotations are declared via
  `ModuleAnnotations` in code and are type-checked at compile time. YAML-based
  override introduces implicit, late-bound behavior that conflicts with Rust's
  "explicit > implicit" philosophy.
- **No user demand**: as of v0.18.0 there are zero issues or RFCs requesting YAML
  annotation overlays for the Rust SDK.

If you need runtime-configurable annotations (e.g., ops teams toggling `readonly` or
`requires_approval` without recompiling), you can load a YAML/JSON file yourself and
construct `ModuleAnnotations` via `serde`:

```rust
let yaml: serde_json::Value = serde_yaml_ng::from_reader(file)?;
let annotations: ModuleAnnotations = serde_json::from_value(yaml)?;
let descriptor = ModuleDescriptor { annotations: Some(annotations), ..default_descriptor };
registry.register("my.module", module, descriptor)?;
```

## Examples

The `examples/` directory contains runnable demos. Run any example with:

```bash
cargo run --example simple_client
cargo run --example greet
cargo run --example get_user
cargo run --example send_email
cargo run --example cancel_token
```

---

### `simple_client` — Implement `Module` and execute directly

Defines two modules (`AddModule`, `GreetModule`), builds an `Identity` + `Context`, and calls them directly without a registry.

```rust
use apcore::context::{Context, Identity};
use apcore::errors::ModuleError;
use apcore::module::Module;
use async_trait::async_trait;
use serde_json::{json, Value};
use std::collections::HashMap;

struct AddModule;

#[async_trait]
impl Module for AddModule {
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "a": { "type": "integer" },
                "b": { "type": "integer" }
            },
            "required": ["a", "b"]
        })
    }
    fn output_schema(&self) -> Value {
        json!({ "type": "object", "properties": { "result": { "type": "integer" } } })
    }
    fn description(&self) -> &str { "Add two integers" }

    async fn execute(&self, input: Value, _ctx: &Context<Value>) -> Result<Value, ModuleError> {
        let a = input["a"].as_i64().unwrap_or(0);
        let b = input["b"].as_i64().unwrap_or(0);
        Ok(json!({ "result": a + b }))
    }
}

#[tokio::main]
async fn main() {
    let identity = Identity::new(
        "user-1".to_string(),
        "user".to_string(),
        vec!["user".to_string()],
        HashMap::new(),
    );
    let ctx: Context<Value> = Context::new(identity);
    let module = AddModule;

    let result = module.execute(json!({"a": 10, "b": 5}), &ctx).await.unwrap();
    println!("{result}"); // {"result":15}
}
```

---

### `greet` — Typed input/output with `serde` and default field values

Uses `#[serde(default)]` for optional fields and shows schema introspection and validation error handling.

```rust
use apcore::context::{Context, Identity};
use apcore::errors::ModuleError;
use apcore::module::Module;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;

#[derive(Debug, Serialize, Deserialize)]
struct GreetInput {
    name: String,
    #[serde(default = "default_greeting")]
    greeting: String,
}
fn default_greeting() -> String { "Hello".to_string() }

#[derive(Debug, Serialize, Deserialize)]
struct GreetOutput { message: String }

struct GreetModule;

#[async_trait]
impl Module for GreetModule {
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "name":     { "type": "string", "description": "Name of the person to greet" },
                "greeting": { "type": "string", "description": "Custom greeting prefix", "default": "Hello" }
            },
            "required": ["name"]
        })
    }
    fn output_schema(&self) -> Value {
        json!({ "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] })
    }
    fn description(&self) -> &str { "Greet a user by name" }

    async fn execute(&self, input: Value, _ctx: &Context<Value>) -> Result<Value, ModuleError> {
        let req: GreetInput = serde_json::from_value(input)
            .map_err(|e| ModuleError::new(apcore::errors::ErrorCode::GeneralInvalidInput, e.to_string()))?;
        Ok(serde_json::to_value(GreetOutput { message: format!("{}, {}!", req.greeting, req.name) }).unwrap())
    }
}

#[tokio::main]
async fn main() {
    let identity = Identity::new("agent-1".to_string(), "agent".to_string(), vec![], HashMap::new());
    let ctx: Context<Value> = Context::new(identity);
    let module = GreetModule;

    let out = module.execute(json!({"name": "Alice", "greeting": "Good morning"}), &ctx).await.unwrap();
    println!("{out}"); // {"message":"Good morning, Alice!"}

    let out = module.execute(json!({"name": "Bob"}), &ctx).await.unwrap();
    println!("{out}"); // {"message":"Hello, Bob!"}  ← default greeting applied

    // Schema introspection
    println!("{}", serde_json::to_string_pretty(&module.input_schema()).unwrap());

    // Missing required field → validation error
    let err = module.execute(json!({"greeting": "Hi"}), &ctx).await.unwrap_err();
    println!("Error: {err}");
}
```

---

### `get_user` — Readonly module with `ModuleAnnotations`

Demonstrates behavioral annotations (`readonly`, `idempotent`, `cacheable`), typed input/output schemas, and looking up records by ID.

```rust
use apcore::module::{Module, ModuleAnnotations};
// ...

fn get_user_annotations() -> ModuleAnnotations {
    ModuleAnnotations {
        readonly: true,
        idempotent: true,
        cacheable: true,
        cache_ttl: 60,
        ..Default::default()
    }
}
```

```
user-1: {"email":"alice@example.com","id":"user-1","name":"Alice"}
user-2: {"email":"bob@example.com","id":"user-2","name":"Bob"}
user-999: {"email":"unknown@example.com","id":"user-999","name":"Unknown"}
```

---

### `send_email` — Destructive module with sensitive fields

Shows `x-sensitive: true` on schema fields (for log redaction), `ModuleAnnotations` with metadata, and behavioral annotation for destructive operations.

```rust
fn input_schema(&self) -> Value {
    json!({
        "type": "object",
        "properties": {
            "to":      { "type": "string" },
            "subject": { "type": "string" },
            "body":    { "type": "string" },
            "api_key": { "type": "string", "x-sensitive": true }  // redacted in logs
        },
        "required": ["to", "subject", "body", "api_key"]
    })
}
```

```rust
fn send_email_annotations() -> ModuleAnnotations {
    ModuleAnnotations {
        destructive: true,
        requires_approval: true,
        ..Default::default()
    }
}

fn send_email_examples() -> Vec<ModuleExample> {
    vec![ModuleExample {
        title: "Send a welcome email".to_string(),
        inputs: json!({ "to": "user@example.com", "subject": "Welcome!", "body": "...", "api_key": "sk-xxx" }),
        output: json!({ "status": "sent", "message_id": "msg-12345" }),
        description: None,
    }]
}
```

---

### `cancel_token` — Cooperative cancellation during long-running execution

`CancelToken` is a cloneable, shared cancellation signal. Modules poll `token.is_cancelled()` between steps to stop early.

```rust
use apcore::cancel::CancelToken;

// Attach a token to the context
let mut ctx: Context<Value> = Context::new(identity);
let token = CancelToken::new();
ctx.cancel_token = Some(token.clone());

// Cancel from another task after 80 ms
tokio::spawn(async move {
    tokio::time::sleep(Duration::from_millis(80)).await;
    token.cancel();
});

// Module checks the token between steps
async fn execute(&self, input: Value, ctx: &Context<Value>) -> Result<Value, ModuleError> {
    for i in 0..steps {
        if let Some(t) = &ctx.cancel_token {
            if t.is_cancelled() {
                return Err(ModuleError::new(ErrorCode::ExecutionCancelled, format!("cancelled at step {i}")));
            }
        }
        tokio::time::sleep(Duration::from_millis(50)).await;
    }
    Ok(json!({ "completed_steps": steps }))
}
```

```
=== Run 1: normal execution ===
  [SlowModule] Executing step 0...
  [SlowModule] Executing step 1...
  [SlowModule] Executing step 2...
Result: {"completed_steps":3}

=== Run 2: cancelled mid-flight ===
  [SlowModule] Executing step 0...
  [SlowModule] Executing step 1...
  [main] Sending cancel signal…
  [SlowModule] Cancelled at step 2
Error (expected): Execution cancelled after 2 steps
```

## Tests

Run all tests:

```bash
cargo test
```

Run a specific test file:

```bash
cargo test --test test_cancel
cargo test --test test_errors
```

Run a specific test by name:

```bash
cargo test test_cancel_token
```

Run with output visible:

```bash
cargo test -- --nocapture
```

## Development

### Prerequisites

Install Rust via [rustup](https://rustup.rs):

```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

### Clone and build

```bash
git clone https://github.com/aiperceivable/apcore-rust.git
cd apcore-rust
cargo build
```

### Run tests

```bash
cargo test
```

### Run tests with output

```bash
cargo test -- --nocapture
```

### Run a specific test

```bash
cargo test test_cancel_token
```

### Lint and format

```bash
cargo fmt           # auto-format code
cargo clippy        # lint
```

### Build documentation

```bash
cargo doc --open
```

### Check without building

```bash
cargo check
```

## License

Apache-2.0

## Links

- **Documentation**: [https://aiperceivable.github.io/apcore/](https://aiperceivable.github.io/apcore/)
- **Website**: [aiperceivable.com](https://aiperceivable.com)
- **GitHub**: [aiperceivable/apcore-rust](https://github.com/aiperceivable/apcore-rust)
- **crates.io**: [apcore](https://crates.io/crates/apcore)
- **Issues**: [GitHub Issues](https://github.com/aiperceivable/apcore-rust/issues)
- **Discussions**: [GitHub Discussions](https://github.com/aiperceivable/apcore-rust/discussions)
