apcore 0.17.1

Schema-driven module standard for AI-perceivable interfaces
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
<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
- **Extension points** — Unified extension management for discoverers, middleware, ACL, approval handlers, span exporters, and module validators
- **Async task management** — Background module execution with status tracking, cancellation, and concurrency limiting
- **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

## API Overview

**Core**

| Type | Description |
|------|-------------|
| `APCore` | High-level client — register modules, call, stream, validate |
| `Registry` | Module storage — discover, register, get, list, watch |
| `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 & Extensions**

| Type | Description |
|------|-------------|
| `EventEmitter` | Event system — subscribe, unsubscribe, emit, emit_filtered, flush |
| `WebhookSubscriber` | Built-in event subscriber |
| `ExtensionManager` | Unified extension point management |
| `AsyncTaskManager` | Background module execution with status tracking |
| `CancelToken` | Cooperative cancellation token |
| `BindingLoader` | Load modules from YAML binding files |

## 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.16"
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, Config};
use std::path::Path;

#[tokio::main]
async fn main() {
    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");
```

### 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 loader = BindingLoader::new();
loader.load_from_file(std::path::Path::new("binding.yaml")).unwrap();
```

## 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 { id: "agent-1".to_string(), identity_type: "agent".to_string(), roles: vec![], attrs: 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` and `ModuleExample`

Demonstrates behavioral annotations (`readonly`, `idempotent`), `ModuleExample` for AI-perceivable documentation, and looking up records by ID.

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

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

fn get_user_examples() -> Vec<ModuleExample> {
    vec![ModuleExample {
        title: "Look up Alice".to_string(),
        description: Some("Returns Alice's profile".to_string()),
        inputs: json!({"user_id": "user-1"}),
        output: json!({"id": "user-1", "name": "Alice", "email": "alice@example.com"}),
    }]
}
```

```
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" }),
        ..Default::default()
    }]
}
```

---

### `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