Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,15 @@ jobs:
# every publishable member without uploading. Catches publishability
# regressions (missing version reqs, packaging-excluded files, broken
# inter-crate deps) on every push/PR, long before a release tag. Mirrors
# `make publish-dry` and the release workflow's dry-run. The two
# `publish = false` FFI crates are excluded (not part of the registry set).
# `make publish-dry` and the release workflow's dry-run. The
# `publish = false` crates are excluded (not part of the registry set):
# the two FFI crates and the tutorial companion crate.
- name: cargo package (no upload)
run: |
# Drop any pre-seeded registry index/cache so cargo fetches fresh and
# --locked resolves against a clean index (see note above).
rm -rf ~/.cargo/registry/index ~/.cargo/registry/cache
cargo package --workspace --locked --exclude cpex-ffi --exclude cpex-demo-ffi
cargo package --workspace --locked --exclude cpex-ffi --exclude cpex-demo-ffi --exclude cpex-tutorial

docs:
name: Docs Build
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/docs-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ jobs:
hugo-version: "latest"
extended: true

# Build with a root-relative baseURL so the link checker can resolve
# internal links against the built files rather than the live site.
- name: Build docs
working-directory: docs
run: hugo --minify
run: hugo --minify --baseURL "/"

- name: Check links
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2
with:
args: --no-progress --root-dir "${{ github.workspace }}/docs/public" "docs/public/**/*.html"
fail: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18 changes: 17 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ jobs:
cpex-plugin-identity-jwt
cpex-plugin-delegator-oauth
cpex-plugin-delegator-biscuit
cpex-plugin-elicitation-ciba
cpex-pdp-cedar-direct
cpex-pdp-cel
cpex-session-valkey
Expand All @@ -118,7 +119,22 @@ jobs:
)
for c in "${crates[@]}"; do
echo "::group::publish $c"
cargo publish -p "$c" --locked
# Idempotent: a prior run can publish a leading prefix of this
# list and then fail further down (e.g. a downstream crate gained
# a new dependency that was not yet in the list). crates.io rejects
# re-publishing an existing version, so treat "already uploaded" as
# success and continue — this lets a re-run of the same tag finish
# the remaining crates instead of aborting on the first done one.
if out="$(cargo publish -p "$c" --locked 2>&1)"; then
printf '%s\n' "$out"
else
printf '%s\n' "$out"
if printf '%s' "$out" | grep -qiE 'already (exists|uploaded)|is already uploaded'; then
echo "::notice::$c already published at this version — skipping"
else
exit 1
fi
fi
echo "::endgroup::"
sleep 15
done
40 changes: 40 additions & 0 deletions .github/workflows/tutorial.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Tutorial checks — runs the runnable tutorial modules end to end.
#
# Split out from ci.yml because the IdP-backed modules need a live Keycloak
# (a Docker service), which the main gate does not. The no-IdP modules and
# the policy unit tests already run in ci.yml via `make examples-build` and
# `cargo test --workspace`; this workflow adds the identity-backed path:
# boot the tutorial Keycloak with docker compose, run every module's
# `--check`, and tear it down.
name: Tutorial

on:
push:
branches: [main]
paths:
- "examples/tutorial/**"
- "crates/**"
- "builtins/**"
- ".github/workflows/tutorial.yaml"
pull_request:
paths:
- "examples/tutorial/**"
- ".github/workflows/tutorial.yaml"

jobs:
tutorial-check:
name: Tutorial modules (with Keycloak)
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Install Rust 1.96.0
uses: dtolnay/rust-toolchain@1.96.0
- uses: Swatinem/rust-cache@v2
# `make tutorial-check` brings the Keycloak compose stack up, runs the
# no-IdP and IdP-backed module checks, then tears the stack down.
- name: make tutorial-check
run: make tutorial-check
13 changes: 13 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Link-checker ignore patterns (lychee). One regex per line, matched against URLs.

# Example / placeholder hosts used in tutorial and config docs; not live endpoints.
localhost
127\.0\.0\.1
.*\.example
.*\.test
example\.com

# The repo's own source links (github.com/contextforge-org/cpex/tree|blob/...) only
# resolve once merged to the default branch, so checking them on a feature branch
# produces false failures. The repository structure itself validates these paths.
github\.com/contextforge-org/cpex
10 changes: 10 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ Each source file should carry an Apache-2.0 SPDX header. For Rust:
// Authors: Your Name
```

## Documentation conventions

The docs site is Hugo (in [`docs/`](docs)). A few conventions keep it consistent:

- **Route syntax:** write the flat list form the runtime parses, `routes:` with `- tool: <name>` entries. The map-keyed form is `apl-core`'s standalone/test surface only; see [Configuration](https://contextforge-org.github.io/cpex/docs/configuration/).
- **Terminology:** "Quick Start" (two words) in prose; `builtin` (one word) for the bundled extensions; "allow/deny" for outcomes. Expand `APL` (Authorization Policy Language), `A2A` (agent-to-agent), `IdP` (identity provider), `PDP`, `CMF`, and `CIBA` on first use in a page.
- **Images:** reference them relative (`images/foo.png`), not with a hardcoded base path (`/cpex/images/...`). The site render hook resolves them against `baseURL`, so previews and alternate deployments work. Put the file under `docs/static/images/`.
- **Runnable examples:** prefer linking the tutorial crate (`examples/tutorial`) over maintaining a second partial example; its modules and tests are CI-run.
- **Voice:** direct and concise; avoid em dashes.

## crates.io publishing

The library crates publish to crates.io from the `release.yaml` workflow on a
Expand Down
38 changes: 38 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"builtins/pdps/cel",
"builtins/session/valkey",
"examples/go-demo/ffi",
"examples/tutorial",
# PyO3 bindings — excluded from default-members so plain `cargo build`
# stays libpython-independent (KD3). Use `cargo build -p cpex-python`
# or `maturin develop` to build this crate explicitly.
Expand Down Expand Up @@ -61,6 +62,7 @@ default-members = [
"builtins/pdps/cedar-direct",
"builtins/pdps/cel",
"examples/go-demo/ffi",
"examples/tutorial",
]

[workspace.package]
Expand Down
44 changes: 43 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -281,13 +281,55 @@ examples-build: rust-examples-build go-examples-build
@echo "✅ All examples built"

.PHONY: examples-run
examples-run: examples-build
examples-run: examples-build tutorial-check-local
@$(CARGO) run --example plugin_demo -p cpex-core --quiet >/dev/null
@$(CARGO) run --example cmf_capabilities_demo -p cpex-core --quiet >/dev/null
@cd $(GO_EXAMPLES_DIR) && $(GO) run . >/dev/null
@cd $(GO_EXAMPLES_DIR) && $(GO) run ./cmd/cmf-demo >/dev/null
@echo "✅ All examples ran successfully"

# =============================================================================
# Tutorial (examples/tutorial)
# =============================================================================
#
# The tutorial ships one runnable binary per module, each with a `--check`
# mode that asserts its scripted scenario. `tutorial-check-local` runs the
# modules that need no infrastructure; `tutorial-check` additionally brings
# up the tutorial Keycloak (docker compose) and runs the IdP-backed modules,
# tearing the stack down afterward. CI runs `tutorial-check`.

TUTORIAL_IDP_COMPOSE = examples/tutorial/idp/docker-compose.yml
TUTORIAL_NOIDP_MODULES = m01_hello m03_shaping m04_effects m09_custom_plugin m10_testing
TUTORIAL_IDP_MODULES = m02_identity m05_pdp m06_delegation m07_tainting m08_elicitation capstone

.PHONY: tutorial-check-local
tutorial-check-local:
@for m in $(TUTORIAL_NOIDP_MODULES); do \
echo "→ tutorial $$m --check"; \
$(CARGO) run -q -p cpex-tutorial --example $$m -- --check >/dev/null || exit 1; \
done
@echo "✅ Tutorial (no-IdP) checks passed"

.PHONY: tutorial-check
tutorial-check: tutorial-check-local
@echo "→ starting tutorial IdP"
@docker compose -f $(TUTORIAL_IDP_COMPOSE) up -d
@echo "→ waiting for Keycloak realm to be ready"
@$(CARGO) run -q -p cpex-tutorial --example wait_for_idp || { \
docker compose -f $(TUTORIAL_IDP_COMPOSE) down; exit 1; }
@for m in $(TUTORIAL_IDP_MODULES); do \
echo "→ tutorial $$m --check"; \
$(CARGO) run -q -p cpex-tutorial --example $$m -- --check || { \
docker compose -f $(TUTORIAL_IDP_COMPOSE) down; exit 1; }; \
done
@docker compose -f $(TUTORIAL_IDP_COMPOSE) down
@echo "✅ Tutorial checks passed (incl. IdP-backed modules)"

.PHONY: tutorial-recordings
tutorial-recordings:
@examples/tutorial/recordings/record.sh
@echo "Upload each cast to asciinema.org, then embed per examples/tutorial/recordings/README.md"

# =============================================================================
# CI gate
# =============================================================================
Expand Down
5 changes: 5 additions & 0 deletions crates/apl-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ keywords.workspace = true
categories.workspace = true
rust-version.workspace = true

[package.metadata.cargo-machete]
# serde is used through its derive macros (Serialize/Deserialize), which
# cargo-machete's source scan does not reliably detect. It is a real dependency.
ignored = ["serde"]

[lib]
# Plain rlib; APL is consumed by other workspace crates (apl-cmf, apl-cpex)
# and does not need cdylib for FFI.
Expand Down
2 changes: 1 addition & 1 deletion docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CPEX is a deterministic reference monitor between an agent and every capability

CPEX composes authorization, delegation, redaction, information-flow tracking, and auditing into a single policy-defined pipeline. Each capability an agent can invoke defines its own enforcement pipeline; APL is the configuration that defines it, executed in two phases: before the operation and after its result.

![CPEX mediates every operation an untrusted LLM triggers, evaluating APL policy against identity, delegation, taint, and audit state the model cannot forge](/cpex/images/cpex_overview.png)
![CPEX mediates every operation an untrusted LLM triggers, evaluating APL policy against identity, delegation, taint, and audit state the model cannot forge](images/cpex_overview.png)

Existing authorization systems (RBAC, ABAC, Cedar, OPA, AuthZEN) answer whether a request should be allowed. CPEX answers a broader question: what security pipeline should execute for this agent operation. It invokes those engines for the decision and enforces the result.

Expand Down
1 change: 0 additions & 1 deletion docs/content/docs/0.1.x/package-integrity.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ See [`cpex.tools.integrity`](../api-reference/#cpextoolsintegrity) for detailed

- [CLI Reference](../cli/) - Command-line usage
- [Configuration](../configuration/) - General configuration options
- [Security Best Practices](../security/) - Comprehensive security guide

## Changelog

Expand Down
8 changes: 4 additions & 4 deletions docs/content/docs/0.1.x/vision.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Hooks are standardized interception points placed at every boundary where an age

This architecture deploys identically across the stack, inside LLM proxies, agent frameworks, and gateways. Each layer runs its own plugins. Prompt injection detection at the proxy. Tool authorization at the gateway. Data loss prevention at the agent.

![CPEX hooks deployed across the agent stack](/cpex/images/distributed_hooks_control_plane.png)
![CPEX hooks deployed across the agent stack](images/distributed_hooks_control_plane.png)

---

Expand All @@ -31,7 +31,7 @@ Enforcement is a three-layer problem.
| **CMF** (Common Message Format) | What you evaluate. A protocol-agnostic context envelope carrying identity, security labels, delegation chains, and content. |
| **APL** (Authorization Policy Language) | How you define policy. Declarative, attribute-based rules with explicit effects. |

![Hooks, CMF, and APL form a unified enforcement stack](/cpex/images/overview_vision.png)
![Hooks, CMF, and APL form a unified enforcement stack](images/overview_vision.png)

Hooks make enforcement **possible**. Policy makes it **usable**. Context makes it **correct**.

Expand All @@ -41,15 +41,15 @@ Hooks make enforcement **possible**. Policy makes it **usable**. Context makes i

Different policy types require different enforcement points. CPEX provides hooks at every layer, from soft stylistic policies enforced at the prompt level to hard compliance requirements enforced at infrastructure boundaries.

![Policy spectrum: each policy type maps to a different enforcement point](/cpex/images/policy_spectrum.png)
![Policy spectrum: each policy type maps to a different enforcement point](images/policy_spectrum.png)

---

## How It Works

An application or framework invokes a hook at a critical operation boundary. The plugin manager dispatches registered plugins (sequentially, concurrently, or fire-and-forget) and returns a result. Plugins can **allow** execution to continue, **block** it with a violation, or **modify** the payload using copy-on-write isolation.

![Plugin execution model: agent → middleware → hook → manager → plugins](/cpex/images/integration_execution_model.png)
![Plugin execution model: agent → middleware → hook → manager → plugins](images/integration_execution_model.png)

The plugin manager handles registration, ordering, timeouts, error isolation, and payload chaining. You get a deterministic enforcement pipeline with no surprises.

Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ bookFlatSection: true

CPEX is a policy enforcement runtime for AI agents: a deterministic reference monitor that mediates every operation an untrusted LLM triggers. Each capability an agent can invoke defines its own enforcement pipeline (authorization, delegation, redaction, information-flow control, audit), configured in APL and run deterministically at the boundary.

Start with the [Vision]({{< relref "/docs/vision" >}}) for the reference-monitor model, the [Quick Start]({{< relref "/docs/quickstart" >}}) to stand up an enforcement point, or [APL]({{< relref "/docs/apl" >}}) to write policy.
Start with the [Vision]({{< relref "/docs/vision" >}}) for the reference-monitor model, the [Threat Model]({{< relref "/docs/threat-model" >}}) for what CPEX defends against at each placement, or the [Use Cases]({{< relref "/docs/use-cases" >}}) for the controls running end-to-end behind a real gateway. Then the [Quick Start]({{< relref "/docs/quickstart" >}}) stands up an enforcement point, and [APL]({{< relref "/docs/apl" >}}) is where you write policy. Prefer to learn by doing? The [Tutorial]({{< relref "/docs/tutorial" >}}) builds a working enforcement point one capability at a time, with runnable code you can edit and re-run.

Using the Python 0.1.x line? Its docs are preserved under [0.1.x (Legacy)]({{< relref "/docs/0.1.x" >}}).
7 changes: 2 additions & 5 deletions docs/content/docs/apl/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ weight: 30

APL is the declarative configuration that defines a CPEX enforcement pipeline. Each capability an agent can invoke (a tool, resource, prompt, or A2A method) defines its own pipeline through a **route** that sequences the controls protecting it, evaluated at the boundary. You describe the conditions and the effects; you do not write enforcement logic in application code.

![An APL config: plugins and global settings, then per-entity routes with a pre-invocation flow (require, PDP, delegate, run) and post-invocation result handling (taint, redact), plus session tainting across entities](/cpex/images/apl_overview.png)
![An APL config: plugins and global settings, then per-entity routes with a pre-invocation flow (require, PDP, delegate, run) and post-invocation result handling (taint, redact), plus session tainting across entities](images/apl_overview.png)

This page covers the configuration: routes, phases, predicates, rules, and field pipelines. The rest of this section goes deeper on each kind of policy:

Expand All @@ -22,10 +22,7 @@ This page covers the configuration: routes, phases, predicates, rules, and field

Policy is organized by **route**: an operation CPEX mediates, identified by the tool, A2A method, or other interface it governs. Each route runs through four phases, in order:

```mermaid
flowchart LR
ARGS["args<br>validate / transform input"] --> POL["authorization.pre_invocation<br>authorize"] --> RES["result<br>transform output"] --> POST["authorization.post_invocation<br>audit / final checks"]
```
![The four route phases in order: args validates and transforms input, authorization.pre_invocation authorizes, result redacts and masks output, and authorization.post_invocation runs audit and final checks; the first deny in any phase halts that phase and every later one](images/apl_phases.png)

- **args**: validate and transform request inputs before the operation runs.
- **authorization.pre_invocation**: authorize the operation. Predicates, PDP calls, delegation, tainting.
Expand Down
9 changes: 1 addition & 8 deletions docs/content/docs/apl/delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@ authorization:

The order matters. The `require` gate runs first, so a credential is only minted for a caller who passed authorization. After the exchange, a post-check verifies the credential actually carries the scope requested, and denies the operation if the IdP returned less.

```mermaid
flowchart LR
IN["caller's verified token<br>(audience: agent)"] --> DEL["delegate(workday-oauth)"]
DEL -->|"RFC 8693 token exchange"| IDP["IdP token endpoint"]
IDP --> OUT["downstream token<br>(audience: workday-api<br>scope: read_compensation)"]
OUT --> BE["backend"]
CHK["delegation.granted.permissions<br>verified before forward"] -.-> OUT
```
![The delegation flow: the caller's verified token enters delegate(workday-oauth), which performs an RFC 8693 exchange at the IdP token endpoint; the resulting downstream token is audience- and scope-limited, delegation.granted.permissions is verified before forward, and only the minted token reaches the backend](images/apl_delegation_flow.png)

## The delegator plugin

Expand Down
Loading
Loading