Skip to content
apcore logo

apcore

PythonLicenseOpenSSF Best Practices

Build once, invoke by Code or AI. Every call validated, authorized, and evidenced.

A governed runtime for agent-callable capabilities — schema, ACL, approval, and audit enforced at every call.

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 Pydantic models (with automatic validation) or plain JSON Schema dicts
  • 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 decorator -- Turn plain functions into fully schema-aware modules with zero boilerplate
  • 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 -- Seamless sync and async module execution
  • Safety guards -- Call depth limits, circular call detection, frequency throttling
  • Approval system -- Pluggable approval gate (Step 5) with sync/async handlers, Phase B resume, and audit events; execution-time ExecutionPolicy overrides (#76) can force/exempt approval on already-registered modules, gate destructive ops, and fail closed under strict
  • 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
  • Circuit breakers -- CircuitBreakerMiddleware for per-(module_id, caller_id) OPEN/CLOSED/HALF_OPEN protection; CircuitBreakerWrapper (from apcore.events.circuit_breaker import CircuitBreakerWrapper) for per-subscriber event delivery resilience
  • Multi-class module discovery -- Opt-in @multi_class decorator (from apcore.registry.multi_class import multi_class) for multiple Module classes per file with snake_case ID derivation (PROTOCOL_SPEC §2.1.1)
  • Pluggable stores -- TaskStore for async task persistence; ObservabilityStore for error/metric backends; AuditStore for control-module audit trails
  • Prometheus / K8s integration -- PrometheusExporter (from apcore.observability import PrometheusExporter) with /metrics, /healthz, /readyz endpoints; UsageCollector and MetricsCollector emit standard Prometheus gauge/counter metrics

API Overview

Core

ClassDescription
APCoreHigh-level client -- register modules, call, stream, validate
RegistryModule storage -- discover, register, get, list
ExecutorExecution engine -- call with middleware pipeline, ACL, approval
ContextRequest context -- trace ID, identity, call chain, cancel token
ConfigConfiguration -- load from YAML, get/set values, namespace-partitioned Config Bus
IdentityCaller identity -- id, type, roles, attributes
FunctionModuleWrapped function module created by @module decorator

Access Control & Approval

ClassDescription
ACLAccess control -- rule-based caller/target authorization
ApprovalHandlerPluggable approval gate protocol
AlwaysDenyHandler / AutoApproveHandler / CallbackApprovalHandlerBuilt-in approval handlers
ExecutionPolicy / PolicyRuleExecution-time governance overrides (#76) -- force/exempt approval, gate_destructive, strict fail-closed

Middleware

ClassDescription
MiddlewarePipeline hooks -- before/after/on_error interception
BeforeMiddleware / AfterMiddlewareSingle-phase middleware adapters
LoggingMiddlewareStructured logging middleware
RetryMiddlewareAutomatic retry with backoff
ErrorHistoryMiddlewareRecords errors into ErrorHistory
PlatformNotifyMiddlewareEmits events on error rate/latency spikes
ObsLoggingMiddlewareObservability-aware structured logging middleware
CircuitBreakerMiddlewareCircuit breaker keyed on (module_id, caller_id) (OPEN/CLOSED/HALF_OPEN)

Schema

ClassDescription
SchemaLoaderLoad schemas from YAML or native types
SchemaValidatorValidate data against schemas
SchemaExporterExport schemas for MCP, OpenAI, Anthropic, generic
RefResolverResolve $ref references in JSON Schema

Observability

ClassDescription
TracingMiddlewareDistributed tracing with span export
BatchSpanProcessorNon-blocking OTEL span export with configurable queue (from apcore.observability import BatchSpanProcessor)
MetricsMiddleware / MetricsCollectorCall count, latency, error rate metrics
ContextLoggerContext-aware structured logging with RedactionConfig
ErrorHistoryMin-heap error ring buffer with SHA-256 fingerprint deduplication
UsageCollectorPer-module usage statistics, trends, and Prometheus export
UsageMiddlewarePer-call usage tracking middleware
TraceContextW3C Trace Context propagation (traceparent/tracestate)
InMemoryExporterSpan exporter that stores spans in memory
StdoutExporterSpan exporter that writes spans to stdout
OTLPExporterSpan exporter using OpenTelemetry Protocol
PrometheusExporterHTTP server for /metrics, /healthz, /readyz endpoints (from apcore.observability import PrometheusExporter)

Events & Extensions

ClassDescription
EventEmitterEvent system -- subscribe, emit, flush
WebhookSubscriber / A2ASubscriberBuilt-in event subscribers
ExtensionManagerUnified extension point management
AsyncTaskManagerBackground module execution with pluggable TaskStore, retry, and reaper
TaskStore / InMemoryTaskStorePluggable async task persistence backend
RetryPolicy / BackoffStrategyDeprecated (0.21.0) — per-task retry configuration. Use RetryConfig for cross-language field-name parity.
CancelTokenCooperative cancellation token
BindingLoaderLoad modules from YAML binding files
ErrorCodeRegistryCentral registry for structured error codes
ErrorFormatterRegistrySurface-specific error formatter registry (MCP, A2A, CLI adapters)

Cross-Language Parity Notes

The Python, TypeScript, and Rust SDKs share one verified contract. As of v0.22.0, the hardening features that originally landed Python-first are at parity across all three SDKs — including AsyncTaskManager, ExtensionManager, CircuitBreakerMiddleware, TaskStore/InMemoryTaskStore (pluggable interface + in-memory backend; Redis/SQL backends are shipped by none, by design), and PrometheusExporter. version_hint negotiation is honored by all three executors. As of v0.23.0, AI error-recovery metadata (per-code user_fixable defaults plus filled ai_guidance) is also at parity across all three SDKs, verified by the shared error_recovery_metadata.json conformance fixture.

Intentional cross-language differences (e.g. Python's synchronous call() ergonomics, the legacy/deprecated RetryPolicy/BackoffStrategy aliases that exist only in Python, and the module-level convenience functions) are documented inline in the relevant feature specs as "Cross-language note" admonitions in the canonical protocol repo (apcore/docs/features). Anything that differs across SDKs without such a note is drift, not a feature.

Configuration

Config Bus and Namespace Registration

Config doubles as an ecosystem-level Config Bus. Any package can register a named namespace with optional JSON Schema validation, env prefix, and default values:

fromapcoreimportConfig# Register a namespace (class-level, shared across all Config instances)Config.register_namespace(
"my_plugin",
schema={"type": "object", "properties": {"timeout_ms": {"type": "integer"}}},
env_prefix="MY_PLUGIN__",
defaults={"timeout_ms": 5000},
)
# Load config as usualconfig=Config.load("project.yaml")
# Namespace-aware accesstimeout=config.get("my_plugin.timeout_ms") # dot-path with namespace resolutionsubtree=config.namespace("my_plugin") # full subtree as dict# Typed accessconfig.get_typed("my_plugin.timeout_ms", int)
# Mount an external source (no unified YAML required)config.mount("my_plugin", from_dict={"timeout_ms": 3000})
# Introspect registered namespacesnames=Config.registered_namespaces()

Built-in Namespaces

apcore pre-registers three namespaces that promote its existing flat config keys:

NamespaceEnv prefixKeys
observabilityAPCORE_OBSERVABILITYtracing, metrics, logging, error_history, platform_notify
obsAPCORE_OBSredaction.regex_patterns, redaction.sensitive_keys, redaction.replacement
sys_modulesAPCORE_SYSevents.thresholds.error_rate, events.thresholds.latency_p99_ms

Environment Variable Conventions

PatternWhen to useExample
APCORE_KEY_NAMEOverride a flat top-level apcore key (existing convention)APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000
APCORE_NAMESPACE prefixOverride keys inside a registered namespaceAPCORE_OBSERVABILITY_TRACING_ENABLED=true
Custom prefix declared in register_namespaceThird-party packages with their own prefixMY_PLUGIN__TIMEOUT_MS=3000

The longest-prefix-match dispatch algorithm ensures that APCORE_OBSERVABILITY_TRACING_ENABLED routes to the observability namespace (not to a core flat key). Within each namespace, a single _ maps to . and __ maps to a literal _.

New Error Codes (0.15.0)

CodeMeaning
CONFIG_NAMESPACE_DUPLICATEA namespace with this name is already registered
CONFIG_NAMESPACE_RESERVEDThe namespace name is reserved (_config, apcore)
CONFIG_ENV_PREFIX_CONFLICTTwo namespaces share the same env prefix
CONFIG_MOUNT_ERRORFailed to load or parse a mounted config source
CONFIG_BIND_ERRORFailed to deserialize a namespace subtree into the requested type
ERROR_FORMATTER_DUPLICATEA formatter for this surface is already registered

Event Type Names

Canonical event type names use dot-namespaced identifiers. apcore.* is reserved for core framework events; adapter packages use their own prefix (e.g., apcore-mcp.*).

Canonical nameEmitted by
apcore.module.toggledsystem.control.toggle_feature
apcore.health.recoveredPlatformNotifyMiddleware (error rate recovery)
apcore.config.updatedsystem.control.update_config
apcore.module.reloadedsystem.control.reload_module

v0.18.0 — legacy aliases removed. Listeners that previously subscribed to module_health_changed or config_changed will no longer receive events. Migrate subscriptions to the canonical names above.

Documentation

For full documentation, including Quick Start guides for both Python and TypeScript, visit: https://aiperceivable.github.io/apcore/getting-started/

Requirements

  • Python >= 3.11

Installation

pip install apcore

Development

pip install -e ".[dev]"

Quick Start

Simple usage (Global Client)

For simple scripts or prototypes, you can use the global apcore functions:

importapcore@apcore.module(id="math.add", description="Add two integers")defadd(a: int, b: int) ->int:
returna+b# Directly call itresult=apcore.call("math.add", {"a": 10, "b": 5})
print(result) # {'result': 15}

Simplified Client (Recommended)

The APCore client provides a unified entry point that manages everything for you:

fromapcoreimportAPCoreclient=APCore()
@client.module(id="math.add", description="Add two integers")defadd(a: int, b: int) ->int:
returna+b# Call the moduleresult=client.call("math.add", {"a": 10, "b": 5})
print(result) # {'result': 15}

Advanced: Define a module with a class

frompydanticimportBaseModelfromapcoreimportContext, APCoreclient=APCore()
classGreetInput(BaseModel):
name: strclassGreetOutput(BaseModel):
message: strclassGreetModule:
input_schema=GreetInputoutput_schema=GreetOutputdescription="Greet a user"defexecute(self, inputs: dict, context: Context) ->dict:
return {"message": f"Hello, {inputs['name']}!"}
client.register("greet", GreetModule())
result=client.call("greet", {"name": "Alice"})
# {"message": "Hello, Alice!"}

Alternative: Define schemas with plain dicts

If you prefer not to use Pydantic, pass raw JSON Schema dicts directly:

fromapcoreimportAPCoreclient=APCore()
classWeatherModule:
input_schema= {"type": "object", "properties": {"city": {"type": "string"}}}
output_schema= {"type": "object", "properties": {"temp": {"type": "number"}}}
description="Get current temperature"defexecute(self, inputs: dict, context=None) ->dict:
return {"temp": 22.5}
client.register("weather", WeatherModule())
result=client.call("weather", {"city": "Tokyo"})
# {"temp": 22.5}

Note: Dict schemas skip Pydantic input validation. Use Pydantic models when you need automatic type coercion and validation, or validate inside execute().

Add middleware

fromapcoreimportLoggingMiddleware, TracingMiddlewareclient.use(LoggingMiddleware())
client.use(TracingMiddleware())

Access control

fromapcoreimportACL, ACLRule, Executor, Registryregistry=Registry()
acl=ACL(rules=[
ACLRule(callers=["admin.*"], targets=["*"], effect="allow", description="Admins can call anything"),
ACLRule(callers=["*"], targets=["admin.*"], effect="deny", description="Others cannot call admin modules"),
])
executor=Executor(registry=registry, acl=acl)

Examples

The examples/ directory contains runnable demos:


simple_client — APCore client with decorator-based modules

Initializes an APCore client, registers modules with @client.module(), and calls them directly.

fromapcoreimportAPCoreclient=APCore()
@client.module(id="math.add", description="Add two integers")defadd(a: int, b: int) ->int:
returna+bresult=client.call("math.add", {"a": 10, "b": 5})
print(result) # {'result': 15}@client.module(id="greet")defgreet(name: str, greeting: str="Hello") ->dict:
return {"message": f"{greeting}, {name}!"}
result=client.call("greet", {"name": "Alice"})
print(result) # {'message': 'Hello, Alice!'}

global_client — Minimal global client usage

No explicit initialization needed — use the default global client directly.

importapcore@apcore.module(id="math.add")defadd(a: int, b: int) ->int:
returna+bresult=apcore.call("math.add", {"a": 10, "b": 5})
print(result) # {'result': 15}

greet — Duck-typed module with Pydantic schemas

Demonstrates the class-based module interface with Pydantic BaseModel for input/output schemas.

frompydanticimportBaseModelclassGreetInput(BaseModel):
name: strclassGreetOutput(BaseModel):
message: strclassGreetModule:
input_schema=GreetInputoutput_schema=GreetOutputdescription="Greet a user by name"defexecute(self, inputs: dict, context) ->dict:
name=inputs["name"]
return {"message": f"Hello, {name}!"}

get_user — Readonly module with ModuleAnnotations

Demonstrates behavioral annotations (readonly, idempotent) and simulated database lookup.

frompydanticimportBaseModelfromapcore.moduleimportModuleAnnotationsclassGetUserInput(BaseModel):
user_id: strclassGetUserOutput(BaseModel):
id: strname: stremail: strclassGetUserModule:
input_schema=GetUserInputoutput_schema=GetUserOutputdescription="Get user details by ID"annotations=ModuleAnnotations(readonly=True, idempotent=True)
_users= {
"user-1": {"id": "user-1", "name": "Alice", "email": "alice@example.com"},
"user-2": {"id": "user-2", "name": "Bob", "email": "bob@example.com"},
}
defexecute(self, inputs: dict, context) ->dict:
user_id=inputs["user_id"]
user=self._users.get(user_id)
ifuserisNone:
return {"id": user_id, "name": "Unknown", "email": "unknown@example.com"}
returndict(user)

send_email — Destructive module with sensitive fields and ContextLogger

Shows x-sensitive on schema fields (for log redaction), ModuleAnnotations with metadata, ModuleExample for AI-perceivable documentation, and ContextLogger usage.

frompydanticimportBaseModel, Fieldfromapcore.moduleimportModuleAnnotations, ModuleExamplefromapcore.observabilityimportContextLoggerclassSendEmailInput(BaseModel):
to: strsubject: strbody: strapi_key: str=Field(..., json_schema_extra={"x-sensitive": True})
classSendEmailOutput(BaseModel):
status: strmessage_id: strclassSendEmailModule:
input_schema=SendEmailInputoutput_schema=SendEmailOutputdescription="Send an email message"tags= ["email", "communication", "external"]
version="1.2.0"metadata= {"provider": "example-smtp", "max_retries": 3}
annotations=ModuleAnnotations(destructive=True, idempotent=False, open_world=True)
examples= [
ModuleExample(
title="Send a welcome email",
inputs={"to": "user@example.com", "subject": "Welcome!", "body": "...", "api_key": "sk-xxx"},
output={"status": "sent", "message_id": "msg-12345"},
description="Sends a welcome email to a new user.",
),
]
defexecute(self, inputs: dict, context) ->dict:
logger=ContextLogger.from_context(context, name="send_email")
logger.info("Sending email", extra={"to": inputs["to"], "subject": inputs["subject"]})
message_id=f"msg-{hash(inputs['to']) %100000:05d}"logger.info("Email sent successfully", extra={"message_id": message_id})
return {"status": "sent", "message_id": message_id}

decorated_add@module decorator for simple functions

fromapcore.decoratorimportmodule@module(description="Add two integers", tags=["math", "utility"])defadd(a: int, b: int) ->int:
returna+b

Development

Run tests

pytest

Run tests with coverage

pytest --cov=src/apcore --cov-report=html

Lint and format

ruff check --fix src/ tests/
ruff format src/ tests/

Type check

mypy src/ tests/

📄 License

Apache-2.0

🔗 Links

Releases

Packages

Contributors

Languages