{"id":50543295,"url":"https://github.com/contextforge-org/cpex","last_synced_at":"2026-06-03T22:00:31.757Z","repository":{"id":334287660,"uuid":"1118936644","full_name":"contextforge-org/cpex","owner":"contextforge-org","description":"A composable enforcement framework for AI agents and toolchains","archived":false,"fork":false,"pushed_at":"2026-05-28T10:58:58.000Z","size":15732,"stargazers_count":9,"open_issues_count":23,"forks_count":7,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-28T12:25:00.343Z","etag":null,"topics":["a2a","agents","ai","extensibility","framework","hooks","library","llm","mcp","plugins","safety","security","tools"],"latest_commit_sha":null,"homepage":"https://contextforge-org.github.io/cpex","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/contextforge-org.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-18T13:54:14.000Z","updated_at":"2026-05-18T04:12:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/contextforge-org/cpex","commit_stats":null,"previous_names":["contextforge-org/contextforge-plugins-framework","contextforge-org/cpex"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/contextforge-org/cpex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contextforge-org%2Fcpex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contextforge-org%2Fcpex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contextforge-org%2Fcpex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contextforge-org%2Fcpex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/contextforge-org","download_url":"https://codeload.github.com/contextforge-org/cpex/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/contextforge-org%2Fcpex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33881107,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a2a","agents","ai","extensibility","framework","hooks","library","llm","mcp","plugins","safety","security","tools"],"created_at":"2026-06-03T22:00:21.924Z","updated_at":"2026-06-03T22:00:31.738Z","avatar_url":"https://github.com/contextforge-org.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv\u003e\n  \u003cimg alt=\"ContextForge Plugin Extensibility Framework (CPEX) logo\" src=\"https://github.com/contextforge-org/cpex/blob/main/docs/images/cpex_v1.png?raw=true\" height=100\"\u003e\n\u003c/div\u003e\n\n# CPEX — ContextForge Plugin Extensibility Framework\n\n\u003ci\u003eA composable enforcement framework for AI agents and toolchains.\u003c/i\u003e\n\n[![CI](https://github.com/contextforge-org/cpex/actions/workflows/ci.yml/badge.svg)](https://github.com/contextforge-org/cpex/actions/workflows/ci.yml)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)\n[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)\n[![PyPI](https://img.shields.io/pypi/v/cpex.svg?color=blue)](https://pypi.org/project/cpex)\n\n\u003e [**Read the project vision**](https://contextforge-org.github.io/cpex/docs/vision/) to learn why hooks, plugins, and policy are the path to agent security.\n\n## What's CPEX?\n\nCPEX lets you intercept, enforce, and extend application behavior through plugins without modifying core logic.\n\nDefine hook points in your application, write plugins that attach to them, and compose enforcement pipelines that run automatically.\n\n```python\nfrom cpex.framework import hook, Plugin, PluginResult\n\nclass RateLimitPlugin(Plugin):\n    @hook(\"tool_pre_invoke\")\n    async def check_rate_limit(self, payload, context):\n        if self.is_over_limit(context):\n            return PluginResult(\n                continue_processing=False,\n                violation=PluginViolation(reason=\"Rate limit exceeded\", code=\"RATE_LIMIT\")\n            )\n        return PluginResult(continue_processing=True)\n```\n\nRegister the plugin, and it runs at every hook invocation. No changes to your application logic.\n\n## Install\n\n```bash\npip install cpex\n```\n\n## Why CPEX?\n\nAI agents execute across trust domains, calling tools, accessing data, and delegating to other agents. Adding security, governance, or policy enforcement typically means embedding that logic directly into application code, leading to duplication, tight coupling, and drift.\n\nCPEX introduces **standardized interception hooks** between your application and its operations. Plugins attach to these hooks and run automatically, keeping enforcement logic separate from business logic.\n\n**What you can build with CPEX:**\n\n- **Security** — access control, prompt injection detection, data loss prevention\n- **Observability** — request tracing, audit logging, metrics collection\n- **Governance** — policy enforcement, compliance validation, approval workflows\n- **Reliability** — rate limiting, circuit breakers, response validation\n\nCPEX is designed for modern **AI and agent systems**, but works equally well for any application that needs **safe, modular extensibility**.\n\n## How It Works\n\nYour application defines **hooks** — named interception points before and after critical operations. Plugins register against these hooks and execute automatically when triggered.\n\n```\nApplication  →  Hook Point  →  Plugin Manager  →  Application (remaining processing)  →  Result\n                                     │\n                              ┌──────┼──────┐\n                              ▼      ▼      ▼\n                          Plugin  Plugin  Plugin\n```\n\nThe plugin manager handles registration, ordering, execution, timeouts, and error isolation. You get a deterministic pipeline with no surprises.\n\n## Core Concepts\n\n### Hooks\n\nA hook is a named interception point in your application. You define a hook where you want plugins to be able to run, then call it there.\n\n**Define hook models:**\n\n```python\nfrom cpex.framework import PluginPayload, PluginResult\n\nclass EmailPayload(PluginPayload):\n    recipient: str\n    subject: str\n    body: str\n\nEmailResult = PluginResult[EmailPayload]\n```\n\n**Register it:**\n\n```python\nfrom cpex.framework.hooks.registry import get_hook_registry\n\nregistry = get_hook_registry()\nregistry.register_hook(\"email_pre_send\", EmailPayload, EmailResult)\n```\n\n**Call the hook in your application:**\n\n```python\nasync def send_email(recipient: str, subject: str, body: str):\n    payload = EmailPayload(recipient=recipient, subject=subject, body=body)\n    context = GlobalContext(request_id=\"req-123\")\n\n    result, _ = await manager.invoke_hook(\"email_pre_send\", payload, context)\n\n    if not result.continue_processing:\n        raise PolicyError(result.violation.reason)\n\n    # proceed with sending\n    await smtp.send(payload.recipient, payload.subject, payload.body)\n```\n\nCPEX also ships with built-in hooks for common AI operations (`tool_pre_invoke`, `tool_post_invoke`, `prompt_pre_fetch`, `prompt_post_fetch`, `resource_pre_fetch`, `resource_post_fetch`, `agent_pre_invoke`, `agent_post_invoke`). These follow the same pattern and are ready to use without registration.\n\n### Plugins\n\nA plugin is a class that implements one or more hook handlers. Use the `@hook` decorator to attach a method to any hook by name:\n\n```python\nfrom cpex.framework import hook, Plugin, PluginViolation, PluginResult\n\nclass EmailFilterPlugin(Plugin):\n    @hook(\"email_pre_send\")\n    async def block_external_domains(self, payload: EmailPayload, context) -\u003e PluginResult:\n        allowed = self.config.config.get(\"allowed_domains\", [])\n        domain = payload.recipient.split(\"@\")[-1]\n\n        if allowed and domain not in allowed:\n            return PluginResult(\n                continue_processing=False,\n                violation=PluginViolation(\n                    reason=\"Domain not allowed\",\n                    code=\"DOMAIN_BLOCKED\",\n                    details={\"domain\": domain}\n                )\n            )\n\n        return PluginResult(continue_processing=True)\n```\n\nThe `@hook` decorator decouples method names from hook names, which is useful when a plugin handles multiple hooks or when names would otherwise conflict.\n\nFor built-in hooks, you can also use the naming convention directly (method name matches hook name) without a decorator:\n\n```python\nclass ContentFilterPlugin(Plugin):\n    async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -\u003e ToolPreInvokeResult:\n        blocked = self.config.config.get(\"blocked_tools\", [])\n        if payload.name in blocked:\n            return ToolPreInvokeResult(\n                continue_processing=False,\n                violation=PluginViolation(reason=\"Tool blocked by policy\", code=\"TOOL_BLOCKED\")\n            )\n        return ToolPreInvokeResult(continue_processing=True)\n```\n\nA plugin method can:\n\n- **Allow** execution to continue\n- **Block** execution with a violation\n- **Modify** the payload (using copy-on-write isolation)\n\n### Execution Modes\n\nPlugins run in phases in this order:\n\n```\nsequential → transform → audit → concurrent → fire_and_forget\n```\n\n| Mode | Execution | Can block? | Can modify? | State merged? | Use case |\n|------|-----------|:-----------:|:-----------:|:-------------:|---------|\n| `sequential` | Serial, chained | Yes | Yes | Yes | Policy enforcement + transformation |\n| `transform` | Serial, chained | No | Yes | Yes | Data transformation (redaction, rewriting) |\n| `audit` | Serial | No | No | No | Logging, monitoring, metrics |\n| `concurrent` | Parallel, fail-fast | Yes | No | Yes | Independent policy gates |\n| `fire_and_forget` | Background, after all phases | No | No | No | Telemetry, audit logs |\n| `disabled` | Not loaded | — | — | — | Plugin off |\n\n- **`sequential`** plugins are awaited one at a time in priority order. Each receives the chained output of the previous plugin. Can halt the pipeline and modify payloads. Use for enforcement + transformation.\n- **`transform`** plugins are awaited one at a time after all sequential plugins. Can modify payloads but blocking attempts are suppressed. Use for data transformation pipelines (PII redaction, prompt rewriting) that should not have policy-enforcement power.\n- **`audit`** plugins are awaited one at a time after transform. Observe-only: payload modifications are discarded and violations are logged but do not block. Use for monitoring, auditing, and gradual rollout of policies.\n- **`concurrent`** plugins are dispatched in parallel after audit. Can halt the pipeline (fail-fast on first blocking result) but payload modifications are discarded to avoid non-deterministic last-writer-wins races. Use for independent policy gates.\n- **`fire_and_forget`** plugins are dispatched as background tasks after all other phases. They receive an isolated snapshot. Cannot block or modify. Use for telemetry and async side effects.\n\nError handling is configured separately with `on_error`, independent of mode:\n\n| `on_error` | Behavior |\n|-----------|---------|\n| `fail` | Pipeline halts, error propagates (default) |\n| `ignore` | Error logged; pipeline continues |\n| `disable` | Error logged; plugin auto-disabled; pipeline continues |\n\n### Plugin Manager\n\nThe `PluginManager` orchestrates everything:\n\n```python\nfrom cpex.framework import PluginManager, GlobalContext\nfrom cpex.framework.hooks.tools import ToolPreInvokePayload\n\nmanager = PluginManager(\"plugins/config.yaml\")\nawait manager.initialize()\n\ncontext = GlobalContext(request_id=\"req-123\", user=\"alice\")\npayload = ToolPreInvokePayload(name=\"web_search\", args={\"query\": \"CPEX framework\"})\n\nresult, plugin_contexts = await manager.invoke_hook(\"tool_pre_invoke\", payload, context)\n\nif result.continue_processing:\n    # Proceed — use result.modified_payload if a plugin transformed it\n    pass\nelse:\n    # A plugin blocked execution\n    print(f\"Blocked: {result.violation.reason}\")\n```\n\n## Configuration\n\nPlugins are configured in YAML:\n\n```yaml\nplugin_dirs:\n  - ./plugins\n\nplugins:\n  - name: email_filter\n    kind: my_app.plugins.EmailFilterPlugin\n    version: 1.0.0\n    hooks:\n      - email_pre_send\n    mode: sequential\n    priority: 10\n    config:\n      allowed_domains:\n        - company.com\n        - partner.org\n```\n\n### Priority\n\nPlugins are scheduled by mode, and execute in priority order within each phase (lower number = higher priority). Use this to ensure enforcement runs before transformation, and transformation runs before logging.\n\n**Plugin Scheduling**\n\nAt each hook invocation, plugins are grouped and scheduled by execution mode, following a strict phase order:\n\n```\nsequential → transform → audit → concurrent → fire_and_forget\n```\n\nWithin `sequential`, `transform`, and `audit` phases, plugins execute in **priority order** (lower number = higher priority, e.g., `10` runs before `20`).\n\n### Conditions\n\nRestrict plugins to specific contexts:\n\n```yaml\nplugins:\n  - name: tenant_plugin\n    kind: my_app.plugins.TenantPlugin\n    hooks:\n      - tool_pre_invoke\n    mode: sequential\n    conditions:\n      - tenant_ids: [tenant-1, tenant-2]\n        server_ids: [server-prod]\n```\n\n## Testing\n\nPlugins are plain async classes — test them directly:\n\n```python\nimport pytest\nfrom cpex.framework import PluginConfig, GlobalContext, PluginContext\n\n@pytest.mark.asyncio\nasync def test_email_filter_blocks_external_domain():\n    config = PluginConfig(\n        name=\"test_filter\",\n        kind=\"my_app.plugins.EmailFilterPlugin\",\n        version=\"1.0.0\",\n        hooks=[\"email_pre_send\"],\n        config={\"allowed_domains\": [\"company.com\"]}\n    )\n    plugin = EmailFilterPlugin(config)\n\n    payload = EmailPayload(recipient=\"user@external.com\", subject=\"Hello\", body=\"...\")\n    context = PluginContext(global_context=GlobalContext(request_id=\"test-1\"))\n\n    result = await plugin.block_external_domains(payload, context)\n    assert result.continue_processing is False\n    assert result.violation.code == \"DOMAIN_BLOCKED\"\n```\n\n## External Plugins\n\nPlugins can run as standalone services, connected over MCP (Streamable HTTP), gRPC, or Unix domain sockets.\n\n```yaml\nplugins:\n  - name: remote_validator\n    kind: external\n    hooks:\n      - tool_pre_invoke\n    mode: sequential\n    mcp:\n      proto: STREAMABLEHTTP\n      url: https://plugin-server.example.com\n      tls:\n        certfile: /path/to/client-cert.pem\n        keyfile: /path/to/client-key.pem\n        ca_bundle: /path/to/ca-bundle.pem\n```\n\nBuild an external plugin server with the built-in `ExternalPluginServer`:\n\n```python\nfrom cpex.framework import ExternalPluginServer\n\nserver = ExternalPluginServer(plugins=[MyPlugin(config)])\nserver.run()\n```\n\n## Isolated plugins\n\nNative plugins can be run in a separate python virtual environment (venv) to prevent them from interfering with the host environment.  Plugin specific packages are automatically installed based on the contents of the supplied requirements_file.  \n\n```yaml\n  - name: \"test_plugin\"\n    kind: \"isolated_venv\"\n    version: \"0.1.0\"\n    hooks: [\"prompt_pre_fetch\", \"prompt_post_fetch\", \"tool_pre_invoke\", \"tool_post_invoke\"]\n    tags: [\"plugin\"]\n    mode: \"sequential\"\n    priority: 150\n    conditions:\n      # Apply to specific tools/servers\n      - server_ids: []  # Apply to all servers\n        tenant_ids: []  # Apply to all tenants\n    config:\n      # Plugin config dict passed to the plugin constructor\n      class_name: \"test_plugin.plugin.TestPlugin\"\n      requirements_file: \"requirements.txt\"\n      # essentially the plugin folder hosting the plugin relative to the project root\n      script_path: \"plugins\"\n```\n\n\n## Project Status\n\nCPEX is under active development as part of the [ContextForge](https://github.com/contextforge-org) ecosystem. The framework is designed to work across AI gateways, agent frameworks, LLM proxies, and tool servers.\n\n## Contributing\n\nContributions are welcome. Open an issue, propose a plugin, or submit a pull request.\n\n## License\n\n[Apache 2.0](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontextforge-org%2Fcpex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcontextforge-org%2Fcpex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontextforge-org%2Fcpex/lists"}