axum-apcore 0.2.0

Axum integration for the apcore AI-Perceivable Core ecosystem
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
# axum-apcore

![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg)
![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)

> **Expose Axum routes as AI-perceivable modules.**

Axum integration for the [apcore](https://github.com/aiperceivable/apcore-rust) AI-Perceivable Core ecosystem.

**axum-apcore** automatically scans your Axum routes and exposes them as apcore modules — with full execution, context mapping, MCP serving, and OpenAI tool export. Define your routes once, and both code and AI can discover, understand, and invoke them through enforced schemas and behavioral annotations.

## Features

- **Route scanning** — Discover Axum routes via the native metadata registry or OpenAPI (utoipa) specs
- **`ap_handler!` macro** — Register route metadata at compile time with zero boilerplate
- **`ApContext` extractor** — Extract apcore `Context` (identity, trace context) from Axum requests via `FromRequestParts`
- **Module execution** — Call any registered module programmatically with `call()`, `stream()`, or `cancellable_call()`
- **Async task management** — Submit background tasks with status tracking, cancellation, and cleanup
- **MCP server** — Serve registered modules as MCP tools (stdio, streamable-http, SSE transports)
- **OpenAI export** — Export modules as OpenAI-compatible tool definitions
- **Access control** — Enforce caller authorization via an apcore ACL file (`APCORE_ACL_PATH`)
- **YAML bindings** — Auto-discover modules from YAML binding files
- **CLI** — Scan, serve, export, manage tasks, list/describe modules, shell completions, man pages, and module scaffolding
- **HTTP proxy CLI** — Generate a full CLI where each command forwards requests to the running API via HTTP (`create_cli()`)
- **Configuration**`APCORE_*` environment variables for all settings
- **Observability** — Tracing middleware, metrics collection, and structured logging via `tracing`

## API Overview

**Core**

| Type | Description |
|------|-------------|
| `AxumApcore` | Unified entry point — init, scan, register, call, stream, export |
| `ApcoreSettings` | Configuration from `APCORE_*` env vars with validation |
| `ApContext` | Axum extractor for apcore `Context<Value>` |
| `RequestIdentity` | Identity struct for auth middleware to inject into request extensions |
| `AxumContextFactory` | Creates apcore contexts from Axum request parts |

**Scanning**

| Type | Description |
|------|-------------|
| `NativeAxumScanner` | Scans routes from the compile-time metadata registry |
| `OpenAPIScanner` | Scans routes from a utoipa-generated OpenAPI spec (requires `openapi` feature) |
| `ap_handler!` | Macro for declarative route metadata registration |

**Engine**

| Type | Description |
|------|-------------|
| `AxumRegistryWriter` | Registers scanned modules into apcore's Registry |
| `AxumDiscoverer` | Discovers modules from YAML binding files |
| `AxumModuleValidator` | Validates module IDs against apcore constraints |
| `TaskManager` | Async task submission, tracking, and cancellation |

## Requirements

- Rust >= 1.75
- Tokio async runtime
- Axum 0.8+

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
axum-apcore = "0.1"
axum = { version = "0.8", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
tracing-subscriber = "0.3"
```

### Feature flags

| Feature | Description | Dependencies |
|---------|-------------|-------------|
| `cli` | CLI commands + HTTP proxy CLI + shell completions | `clap`, `clap_complete`, `apcore-cli`, `apcore-toolkit/http-proxy` |
| `mcp` | MCP server and OpenAI tools export | `apcore-mcp` |
| `openapi` | OpenAPI spec scanning via utoipa | `utoipa` |
| `all` | Enable all optional features ||

```toml
axum-apcore = { version = "0.1", features = ["all"] }
```

## Quick Start

### Register routes and call modules

```rust
use axum::{routing::get, Router, Json};
use axum_apcore::{ap_handler, AxumApcore, ApContext};
use serde_json::{json, Value};
use std::sync::Arc;

// Define a handler
async fn get_user(
    ApContext(ctx): ApContext,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Json<Value> {
    Json(json!({"id": id, "name": "Alice", "trace_id": ctx.trace_id}))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    // Register route metadata
    ap_handler! {
        method: "GET",
        path: "/api/users/:id",
        handler: get_user,
        description: "Get a user by ID",
        tags: ["users"],
        input_schema: json!({"type": "object", "properties": {"id": {"type": "string"}}}),
        output_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}),
    }

    // Initialize
    let apcore = Arc::new(AxumApcore::new());
    let router = Router::new()
        .route("/api/users/{id}", get(get_user))
        .with_state(apcore.clone());

    apcore.init_app(&router).await.unwrap();

    // List discovered modules
    println!("{:?}", apcore.list_modules());
    // => ["users.get_user.get"]

    // Start the server
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, router).await.unwrap();
}
```

### Register executable handlers

```rust
use axum_apcore::{AxumApcore, Context, ModuleError};
use serde_json::{json, Value};
use std::sync::Arc;

async fn get_user_handler(input: Value, _ctx: &Context<Value>) -> Result<Value, ModuleError> {
    let id = input["id"].as_str().unwrap_or("unknown");
    Ok(json!({"id": id, "name": "Alice"}))
}

#[tokio::main]
async fn main() {
    let apcore = AxumApcore::new();

    // Register a callable handler
    apcore.register_handler(
        "axum::get_user",
        Arc::new(|input, ctx| Box::pin(get_user_handler(input, ctx))),
    );

    // After init_app(), call modules programmatically
    let result = apcore
        .call_anonymous("users.get_user.get", json!({"id": "1"}))
        .await
        .unwrap();

    println!("{result}"); // {"id":"1","name":"Alice"}
}
```

### OpenAPI scanning

```rust
use axum_apcore::OpenAPIScanner;
use serde_json::json;

let scanner = OpenAPIScanner::new();
let spec = json!({
    "openapi": "3.1.0",
    "info": {"title": "My API", "version": "1.0.0"},
    "paths": {
        "/users/{id}": {
            "get": {
                "operationId": "get_user_get",
                "summary": "Get user",
                "tags": ["users"],
                "responses": {"200": {"description": "OK"}}
            }
        }
    }
});

let modules = scanner.scan_spec(&spec, None, None).unwrap();
println!("{}", modules[0].module_id); // "users.get_user.get"
```

### Cancellable execution with timeout

```rust
use std::time::Duration;

let result = apcore
    .cancellable_call("users.get_user.get", json!({"id": "1"}), None, Duration::from_secs(5))
    .await;
```

### Background tasks

```rust
// Submit a task
let task_id = apcore.submit_task("users.get_user.get", json!({"id": "1"})).unwrap();

// Check status
let info = apcore.get_task_status(&task_id);

// Get result when complete
let result = apcore.get_task_result(&task_id);

// Cancel a running task
apcore.cancel_task(&task_id);
```

### HTTP proxy CLI

Generate a CLI where each command forwards requests to the running API via HTTP:

```rust
use axum_apcore::{AxumApcore, CreateCliConfig};

let apcore = AxumApcore::new();
let config = CreateCliConfig {
    prog_name: "myapp-cli".into(),
    base_url: "http://localhost:3000".into(),
    ..Default::default()
};

let cmd = apcore.create_cli(&router, config).await?;
```

## CLI Reference

The CLI provides both framework-specific commands and apcore-cli commands (requires `cli` feature):

### Framework Commands

| Command | Description |
|---------|-------------|
| `scan` | Scan Axum routes and generate module definitions |
| `serve` | Start MCP server exposing registered modules |
| `export` | Export modules as OpenAI-compatible tool definitions |
| `tasks list` | List async tasks (with optional `--status` filter) |
| `tasks cancel` | Cancel a running task by ID |
| `tasks cleanup` | Clean up old completed tasks |

### `scan`

```bash
axum-apcore scan [OPTIONS]
```

| Option | Default | Description |
|--------|---------|-------------|
| `--source` | `native` | Scanner: `native` or `openapi` |
| `--output` | `registry` | Output: `registry` or `yaml` |
| `--dir` || Directory for YAML output |
| `--dry-run` || Preview without writing |
| `--include` || Regex: only include matching module IDs |
| `--exclude` || Regex: exclude matching module IDs |
| `--verify` || Verify registration after writing |

### `serve`

```bash
axum-apcore serve [OPTIONS]
```

| Option | Default | Description |
|--------|---------|-------------|
| `--transport` | from settings | Transport: `stdio`, `streamable-http`, `sse` |
| `--host` | from settings | Host to bind to |
| `--port` | from settings | Port to listen on |
| `--explorer` || Enable MCP explorer UI |
| `--jwt-secret` | `$APCORE_JWT_SECRET` | JWT secret for authentication |
| `--approval` | `auto` | Approval mode: `auto`, `deny`, `manual` |
| `--tags` || Comma-separated module tag filter |
| `--prefix` || Module ID prefix filter |

### `export`

```bash
axum-apcore export [OPTIONS]
```

| Option | Default | Description |
|--------|---------|-------------|
| `--format` | `openai-tools` | Export format |
| `--strict` || Use strict mode for OpenAI tools |
| `--embed-annotations` || Embed annotations in definitions |
| `--tags` || Comma-separated module tag filter |
| `--prefix` || Module ID prefix filter |

### apcore-cli Commands

These commands are delegated to the [apcore-cli](https://github.com/aiperceivable/apcore-cli-rust) library:

| Command | Description |
|---------|-------------|
| `list` | List available modules (with `--tag` filter, `--format` table/json) |
| `describe MODULE_ID` | Show schema and annotations for a module |
| `completion SHELL` | Generate shell completion script (bash, zsh, fish, elvish, powershell) |
| `man [COMMAND]` | Generate a roff man page |
| `init module MODULE_ID` | Scaffold a new module (with `--style`, `--dir`, `--description`) |

### Examples

```bash
# List all modules in table format
axum-apcore list

# List modules filtered by tag, output as JSON
axum-apcore list --tag users --format json

# Describe a specific module
axum-apcore describe users.get_user.get

# Generate bash completions
axum-apcore completion bash

# Generate man page for the program
axum-apcore man

# Scaffold a new module with binding style
axum-apcore init module users.create_user --style binding --description "Create a user"
```

## Configuration

All settings are read from environment variables with the `APCORE_` prefix:

| Variable | Default | Description |
|----------|---------|-------------|
| `APCORE_MODULE_DIR` | `apcore_modules` | Directory for YAML binding files |
| `APCORE_AUTO_DISCOVER` | `true` | Auto-discover modules on startup |
| `APCORE_BINDING_PATTERN` | `*.binding.yaml` | Glob pattern for binding files |
| `APCORE_SCANNER_SOURCE` | `native` | Scanner: `native` or `openapi` |
| `APCORE_SERVE_TRANSPORT` | `streamable-http` | MCP transport: `stdio`, `streamable-http`, `sse` |
| `APCORE_SERVE_HOST` | `127.0.0.1` | MCP server host |
| `APCORE_SERVE_PORT` | `9090` | MCP server port |
| `APCORE_SERVER_NAME` | `axum-apcore` | MCP server name |
| `APCORE_JWT_SECRET` || JWT secret for MCP auth |
| `APCORE_ACL_PATH` || Path to an ACL YAML file; enables module access control (see below) |
| `APCORE_TRACING` | `false` | Enable tracing middleware |
| `APCORE_METRICS` | `false` | Enable metrics collection |
| `APCORE_TASK_MAX_CONCURRENT` | `10` | Max concurrent background tasks |
| `APCORE_TASK_MAX_TASKS` | `100` | Max total tasks in queue |

### Access control (ACL)

Set `APCORE_ACL_PATH` (or `ApcoreSettings::acl_path`) to an ACL YAML file to have
apcore enforce caller authorization on every `call()` / `stream()`. The executor
runs apcore's `acl_check` pipeline step; a denied call returns
`ErrorCode::ACLDenied`, which maps to HTTP **403 Forbidden**.

ACL rules match on the **caller id**, which is apcore's module-level identity —
**not** the request's `RequestIdentity`/JWT subject:

- a top-level request (your HTTP route invoking a module) is checked as the
  `@external` caller;
- a module calling another module is checked under the **calling module's** id.

```yaml
# acl/global_acl.yaml
default_effect: allow
rules:
  - callers: ["@external"]          # all top-level (HTTP-originated) calls
    targets: ["admin.delete_user.delete"]
    effect: deny
    description: "External callers cannot reach the admin delete module."
```

```bash
APCORE_ACL_PATH=acl/global_acl.yaml cargo run --example handler_registration
```

Use ACL for module-level / external-entry authorization. For per-user HTTP authz
(roles, ownership), keep using the Axum middleware / JWT layer — that identity is
available on `ApContext` but is independent of the ACL caller id. A malformed or
missing ACL file is logged and treated as "no ACL" rather than crashing startup.

## Examples

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

```bash
cargo run --example basic
cargo run --example handler_registration
cargo run --example async_tasks
cargo run --example openapi_scanner --features openapi
cargo run --example mcp_server --features mcp
cargo run --example cli_proxy --features cli
```

| Example | Description |
|---------|-------------|
| `basic` | Full Axum app with `ap_handler!`, `ApContext` extractor, and server startup |
| `handler_registration` | Register executable handlers, call modules with `call()` and `call_anonymous()` |
| `async_tasks` | Submit background tasks, poll status, cancel, and list tasks |
| `openapi_scanner` | Scan a utoipa-generated OpenAPI spec with include/exclude filters |
| `mcp_server` | Create an MCP server and export OpenAI-compatible tool definitions |
| `cli_proxy` | Generate an HTTP proxy CLI with `create_cli()` |

## Tests

Run all tests (unit + integration):

```bash
cargo test --all-features
```

Run only unit tests:

```bash
cargo test --all-features --lib
```

Run only integration tests:

```bash
cargo test --all-features --test integration_test
```

Run a specific test by name:

```bash
cargo test test_e2e_register_scan_call
```

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/axum-apcore.git
cd axum-apcore
cargo build --all-features
```

### Quality gates

All of these must pass before committing:

```bash
cargo fmt --all -- --check          # Formatting
cargo clippy --all-targets --all-features -- -D warnings  # Lint
cargo build --all-features          # Full build
cargo test --all-features           # Tests (unit + integration)
cargo build --examples              # Example build
```

### Lint and format

```bash
cargo fmt           # Auto-format code
cargo clippy        # Lint with suggestions
```

### Build documentation

```bash
cargo doc --all-features --open
```

## Architecture

axum-apcore follows the same module structure as [fastapi-apcore](https://github.com/aiperceivable/fastapi-apcore):

| Rust Module | FastAPI Module | Purpose |
|-------------|----------------|---------|
| `client` | `client.py` | `AxumApcore` unified entry point |
| `config` | `engine/config.py` | `ApcoreSettings` from `APCORE_*` env vars |
| `context` | `engine/context.py` | `ApContext` extractor + `AxumContextFactory` |
| `scanner/native` | `scanners/native.py` | Route metadata registry scanning |
| `scanner/openapi` | `scanners/openapi.py` | utoipa OpenAPI spec scanning |
| `engine/registry` | `engine/registry.py` | Singleton Registry/Executor management |
| `engine/extensions` | `engine/extensions.py` | Discoverer + ModuleValidator |
| `engine/observability` | `engine/observability.py` | Tracing/metrics/logging setup |
| `engine/tasks` | `engine/tasks.py` | Async task management |
| `output/registry_writer` | `output/registry_writer.py` | ScannedModule → Registry registration |
| `cli` | `cli.py` | clap CLI (scan/serve/export/tasks) + apcore-cli (list/describe/completion/man/init) |

## License

Apache-2.0

## Links

- **Core SDK**: [aiperceivable/apcore-rust]https://github.com/aiperceivable/apcore-rust
- **Toolkit**: [aiperceivable/apcore-toolkit-rust]https://github.com/aiperceivable/apcore-toolkit-rust
- **Reference**: [aiperceivable/fastapi-apcore]https://github.com/aiperceivable/fastapi-apcore
- **Website**: [aiperceivable.com]https://aiperceivable.com
- **Issues**: [GitHub Issues]https://github.com/aiperceivable/axum-apcore/issues