Skip to content

Commit 5a10422

Browse files
committed
feat: Add comprehensive documentation for new features
- Introduced Decorator and YAML Bindings documentation outlining the `@module` decorator, automatic Pydantic model generation, and YAML binding file loading. - Added Middleware System documentation detailing the composable middleware pipeline, lifecycle phases, and built-in middleware like `LoggingMiddleware`. - Created Observability System documentation covering distributed tracing, metrics collection, and structured logging with OpenTelemetry integration. - Developed Module Registry and Discovery System documentation explaining the 8-step discovery pipeline, module registration, and event system. - Added Schema System documentation detailing schema loading, validation, `$ref` resolution, and export capabilities for structured module interfaces.
1 parent 5f4430c commit 5a10422

File tree

7 files changed

+867
-0
lines changed

7 files changed

+867
-0
lines changed

docs/features/acl-system.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Access Control System
2+
3+
## Overview
4+
5+
Pattern-based Access Control List (ACL) with first-match-wins evaluation for module access control. The system enforces which callers may invoke which target modules, using wildcard patterns, special identity patterns (`@external`, `@system`), and optional conditions based on identity type, roles, and call depth. Configuration can be loaded from YAML files and hot-reloaded at runtime.
6+
7+
## Requirements
8+
9+
- Implement first-match-wins rule evaluation: rules are evaluated in order, and the first rule whose patterns match the caller and target determines the access decision (allow or deny).
10+
- Support wildcard patterns for caller and target matching (e.g., `admin.*`, `*`), delegating to a shared pattern-matching utility.
11+
- Handle special patterns: `@external` matches calls with no caller (external entry points), and `@system` matches calls where the execution context has a system-type identity.
12+
- Support conditional rules with `identity_types` (identity type must be in list), `roles` (at least one role must overlap), and `max_call_depth` (call chain length must not exceed threshold).
13+
- Provide `default_effect` fallback (allow or deny) when no rule matches.
14+
- Load ACL configuration from YAML files via `ACL.load()`, with strict validation of structure and rule fields.
15+
- Support runtime rule management: `add_rule()` inserts at highest priority (position 0), `remove_rule()` removes by caller/target pattern match.
16+
- Support hot reload from the original YAML file via `reload()`.
17+
- All public methods must be thread-safe.
18+
19+
## Technical Design
20+
21+
### Architecture
22+
23+
The ACL system consists of two primary components: the `ACLRule` dataclass representing individual rules, and the `ACL` class that manages a rule list and evaluates access decisions.
24+
25+
#### Rule Evaluation
26+
27+
```
28+
check(caller_id, target_id, context)
29+
|
30+
+--> effective_caller = "@external" if caller_id is None else caller_id
31+
|
32+
+--> for each rule in rules (first-match-wins):
33+
| 1. Test caller patterns (OR logic: any pattern matching is sufficient)
34+
| 2. Test target patterns (OR logic)
35+
| 3. Test conditions (AND logic: all conditions must pass)
36+
| 4. If all pass -> return rule.effect == "allow"
37+
|
38+
+--> No rule matched -> return default_effect == "allow"
39+
```
40+
41+
#### Pattern Matching
42+
43+
Pattern matching is handled at two levels:
44+
- **Special patterns** (`@external`, `@system`) are resolved directly in `ACL._match_pattern()` using caller identity and context.
45+
- **All other patterns** (exact strings, wildcard `*`, prefix wildcards like `executor.*`) are delegated to the foundation `match_pattern()` utility in `utils/pattern.py`, which implements Algorithm A08 with support for `*` wildcards matching any character sequence including dots.
46+
47+
#### Conditional Rules
48+
49+
When a rule has a `conditions` dict, all specified conditions must be satisfied (AND logic):
50+
- `identity_types`: Context identity's type must be in the provided list.
51+
- `roles`: At least one of the context identity's roles must overlap with the condition's role list (set intersection).
52+
- `max_call_depth`: The length of `context.call_chain` must not exceed the threshold.
53+
54+
If no context is provided but conditions are present, the rule does not match.
55+
56+
### Components
57+
58+
- **`ACLRule`** -- Dataclass with fields: `callers` (list of patterns), `targets` (list of patterns), `effect` ("allow" or "deny"), optional `description`, and optional `conditions` dict.
59+
- **`ACL`** -- Main class managing an ordered rule list. Provides `check()`, `add_rule()`, `remove_rule()`, `reload()`, and the `ACL.load()` classmethod for YAML loading. All public methods are protected by `threading.Lock`.
60+
- **`match_pattern()`** -- Wildcard pattern matcher in `utils/pattern.py`. Supports `*` as a wildcard matching any character sequence. Handles prefix, suffix, and infix wildcards via segment splitting.
61+
62+
### Thread Safety
63+
64+
The `ACL` class uses an internal `threading.Lock` on all public methods. The `check()` method copies the rule list and default effect under the lock, then performs evaluation outside the lock. `add_rule()`, `remove_rule()`, and `reload()` all hold the lock for the duration of their mutations.
65+
66+
### YAML Configuration Format
67+
68+
```yaml
69+
version: "1.0"
70+
default_effect: deny
71+
rules:
72+
- callers: ["api.*"]
73+
targets: ["db.*"]
74+
effect: allow
75+
description: "API modules can access database modules"
76+
- callers: ["@external"]
77+
targets: ["public.*"]
78+
effect: allow
79+
- callers: ["*"]
80+
targets: ["admin.*"]
81+
effect: deny
82+
conditions:
83+
identity_types: ["service"]
84+
roles: ["admin"]
85+
max_call_depth: 5
86+
```
87+
88+
## Key Files
89+
90+
| File | Lines | Purpose |
91+
|------|-------|---------|
92+
| `src/apcore/acl.py` | 279 | `ACLRule` dataclass and `ACL` class with pattern matching, YAML loading, and runtime management |
93+
| `src/apcore/utils/pattern.py` | 46 | `match_pattern()` wildcard utility (Algorithm A08) |
94+
95+
## Dependencies
96+
97+
### Internal
98+
- `apcore.context.Context` -- Provides `identity`, `call_chain`, and other context fields for conditional rule evaluation.
99+
- `apcore.context.Identity` -- Dataclass with `id`, `type`, and `roles` fields used by `@system` pattern and condition checks.
100+
- `apcore.errors.ACLRuleError` -- Raised for invalid ACL configuration (bad YAML structure, missing keys, invalid effect values).
101+
- `apcore.errors.ConfigNotFoundError` -- Raised when the YAML file path does not exist.
102+
- `apcore.utils.pattern.match_pattern` -- Foundation wildcard matching for non-special patterns.
103+
104+
### External
105+
- `yaml` (PyYAML) -- YAML parsing for configuration loading.
106+
- `threading` (stdlib) -- Lock for thread-safe access to the rule list.
107+
- `os` (stdlib) -- File existence checks in `ACL.load()`.
108+
- `logging` (stdlib) -- Debug-level logging of access decisions.
109+
110+
## Testing Strategy
111+
112+
### Unit Tests (`tests/test_acl.py`)
113+
114+
- **Pattern matching**: Tests for `@external` matching None callers (and not matching string callers), `@system` matching system-type identities (and failing for None or non-system identities), exact patterns, wildcard `*`, and prefix wildcards like `executor.*`.
115+
- **First-match-wins evaluation**: Verifies that the first matching allow returns True, first matching deny returns False, and that rule order takes precedence over specificity.
116+
- **Default effect**: Tests both `default_effect="deny"` and `default_effect="allow"` when no rule matches.
117+
- **YAML loading**: Validates correct loading of rules with descriptions and conditions, and error handling for missing files (`ConfigNotFoundError`), invalid YAML, missing `rules` key, non-list `rules`, missing required keys (`callers`, `targets`, `effect`), invalid effect values, and non-list `callers`.
118+
- **Conditional rules**: Tests `identity_types` matching and failing, `roles` intersection matching and failing, `max_call_depth` within and exceeding limits, and conditions failing when context or identity is None.
119+
- **Runtime modification**: `add_rule()` inserts at position 0, `remove_rule()` returns True/False, `reload()` re-reads the YAML file and updates rules.
120+
- **Context interaction**: Verifies `caller_id=None` maps to `@external`, and context is forwarded to conditional evaluation.
121+
- **Thread safety**: Concurrent `check()` calls (10 threads x 200 iterations) with no errors, and concurrent `add_rule()` + `check()` with no corruption.
122+
123+
### Integration Tests (`tests/integration/test_acl_enforcement.py`)
124+
- End-to-end tests exercising ACL enforcement through the `Executor` pipeline.

docs/features/core-executor.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Core Execution Engine
2+
3+
## Overview
4+
5+
The Core Execution Engine is the central orchestration component of apcore. It processes module calls through a structured 10-step pipeline, handling everything from context creation and safety checks to module execution with timeout enforcement and result validation. The engine supports both synchronous and asynchronous execution paths, bridging between the two via threading and an async event loop bridge.
6+
7+
## Requirements
8+
9+
- Orchestrate module calls through a well-defined, sequential pipeline with clear separation of concerns at each step.
10+
- Enforce safety constraints including maximum call depth limits, circular call detection, and frequency throttling to prevent runaway or abusive execution.
11+
- Look up modules from the Registry and enforce access control lists (ACL) before execution.
12+
- Validate inputs and outputs using Pydantic models, with automatic redaction of fields marked as `x-sensitive`.
13+
- Support middleware chains that execute before and after the core module invocation, enabling cross-cutting concerns such as logging, metrics, and transformation.
14+
- Execute modules with configurable timeout enforcement, using daemon threads for synchronous modules and an async bridge for asynchronous modules.
15+
- Return structured results that include execution metadata and any errors encountered during the pipeline.
16+
17+
## Technical Design
18+
19+
### 10-Step Execution Pipeline
20+
21+
The executor processes every module call through the following pipeline:
22+
23+
1. **Context Creation** -- A `Context` object is constructed carrying the caller identity, call metadata, and any propagated state from parent calls. This context flows through every subsequent step.
24+
25+
2. **Safety Checks** -- Three safety mechanisms are evaluated before proceeding:
26+
- *Call depth check*: Rejects calls that exceed the configured maximum nesting depth, preventing unbounded recursion.
27+
- *Circular call detection*: Inspects the call chain recorded in the context to detect and reject circular module invocations.
28+
- *Frequency throttling*: Tracks call frequency per module and rejects calls that exceed the configured rate, protecting against tight-loop abuse.
29+
30+
3. **Module Lookup from Registry** -- The target module is resolved by name from the Registry. If the module is not found or not loaded, the pipeline terminates with a descriptive error.
31+
32+
4. **ACL Enforcement** -- The caller's `Identity` (extracted from the context) is checked against the module's access control list. Unauthorized calls are rejected before any execution occurs.
33+
34+
5. **Input Validation with Pydantic + Sensitive Field Redaction** -- The call's input payload is validated against the module's input schema (a dynamically generated Pydantic model). Fields annotated with `x-sensitive` are redacted from logs and error messages using the `redact_sensitive` utility.
35+
36+
6. **Middleware Before Chain** -- All registered "before" middleware functions are executed in order. Each middleware receives the context and validated input, and may modify or enrich them before the module runs.
37+
38+
7. **Module Execution with Timeout** -- The module's handler is invoked. Timeout enforcement is implemented via daemon threads for synchronous handlers and an async bridge for asynchronous handlers. If the handler exceeds the configured timeout, the call is cancelled and a timeout error is returned.
39+
40+
8. **Output Validation** -- The module's return value is validated against its output schema. Invalid output triggers an error rather than allowing malformed data to propagate.
41+
42+
9. **Middleware After Chain** -- All registered "after" middleware functions are executed in order with access to the context, input, and output. These may perform logging, transformation, or cleanup.
43+
44+
10. **Result Return** -- The final validated output (or error) is packaged into a structured result and returned to the caller.
45+
46+
### Key Classes
47+
48+
- **Executor** -- The main engine class that implements the 10-step pipeline. Manages middleware registration, timeout configuration, and the execution loop.
49+
- **Context** -- Immutable data class carrying call metadata: caller identity, call chain history, depth counter, and propagated key-value state.
50+
- **Identity** -- Represents the caller's identity for ACL enforcement. Carries roles, permissions, and an identifier.
51+
- **Config** -- Configuration data class holding executor-level settings such as max call depth, timeout defaults, and throttle limits.
52+
53+
### Sync/Async Bridge
54+
55+
The executor exposes both `execute()` (sync) and `execute_async()` (async) entry points. Internally:
56+
- Synchronous modules called from an async context are dispatched to a daemon thread via `asyncio.to_thread`.
57+
- Asynchronous modules called from a synchronous context are executed through a temporary event loop on a daemon thread.
58+
- An async module cache lock protects concurrent access to shared module state.
59+
60+
### Sensitive Field Redaction
61+
62+
The `redact_sensitive` utility walks the input/output dictionaries and replaces values of fields marked `x-sensitive: true` in the schema with a placeholder string. This ensures sensitive data never appears in logs or error reports.
63+
64+
### Validation
65+
66+
The `validate()` method on the executor provides a standalone validation path that runs input through the Pydantic model without executing the module, useful for pre-flight checks.
67+
68+
## Key Files
69+
70+
| File | Lines | Purpose |
71+
|------|-------|---------|
72+
| `executor.py` | 634 | Core execution engine implementing the 10-step pipeline |
73+
| `context.py` | 66 | Context and Identity data classes |
74+
| `config.py` | 29 | Executor configuration data class |
75+
| `errors.py` | 395 | Structured error types for every failure mode in the pipeline |
76+
77+
## Dependencies
78+
79+
### External
80+
- `pydantic>=2.0` -- Used for input/output schema validation, dynamic model generation, and field metadata.
81+
82+
### Internal
83+
- **Registry** -- Module lookup (step 3) depends on the Registry system to resolve module names to loaded module instances.
84+
- **Schema System** -- Input and output validation (steps 5 and 8) depend on the Schema System for Pydantic model generation from YAML schemas.
85+
86+
## Testing Strategy
87+
88+
- **Unit tests** cover each pipeline step in isolation, verifying that context creation, safety checks, ACL enforcement, validation, middleware chains, and result packaging all behave correctly for both success and failure cases.
89+
- **Timeout tests** verify that both synchronous and asynchronous modules are correctly cancelled when exceeding configured timeouts, and that daemon threads do not leak.
90+
- **Safety check tests** exercise call depth limits, circular detection with various call chain topologies, and frequency throttle edge cases.
91+
- **Redaction tests** confirm that `x-sensitive` fields are properly masked in logs and error messages while remaining intact in the actual data passed to the module.
92+
- **Integration tests** run full pipeline executions through the executor with real Registry and Schema instances to verify end-to-end behavior.
93+
- Test naming follows the `test_<unit>_<behavior>` convention.

0 commit comments

Comments
 (0)