Skip to content
Open
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
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md — Capability Host Protocol

CHP is a protocol and Python SDK for making agent, tool, and system execution **observable, replayable, and governable**. Every function wrapped as a `@capability` gets automatic evidence emission, correlation propagation, replay by session ID, and optional policy enforcement — with zero mandatory infrastructure. The reference host is `LocalCapabilityHost` in `packages/python/chp_core/`.

## Three invariants you must never violate

1. **Evidence is append-only.** Never modify or delete rows in an evidence SQLite store. The SHA256 hash chain breaks if any row changes. `store.py` is insert-only by design.
2. **Preserve caller correlation IDs.** If an `InvocationEnvelope` arrives with a `correlation_id`, the host must forward it verbatim into every evidence event and the result. Never generate a new ID over a supplied one.
3. **Spec, schemas, and types must stay in sync.** Any change to `spec/chp-v0.1.md`, `schemas/*.json`, or `packages/python/chp_core/types.py` must be validated with `chp work check-alignment --repo-root .` (runs 41 cross-artifact checks). CI will catch drift, but run it locally first.

## Key commands

```bash
# Fast test suite (~6s)
cd packages/python && python -m pytest tests/ -m "not slow" -q --no-cov

# Full test suite
cd packages/python && python -m pytest tests/ -q --no-cov

# Protocol conformance (9 checks)
python conformance/runner.py

# Spec/schema/type alignment (41 checks)
PYTHONPATH=packages/python chp work check-alignment --repo-root .

# Wire evidence capture for every Claude Code session
PYTHONPATH=packages/python chp hooks install
```

## Navigation

| Where to look | What you'll find |
|---|---|
| `spec/chp-v0.1.md` | Normative protocol — start here for definitions and MUST/SHOULD requirements |
| `schemas/` | JSON Schema for every protocol object (29 files) |
| `packages/python/chp_core/host.py` | `LocalCapabilityHost` — registration, invocation, evidence emission |
| `packages/python/chp_core/store.py` | `SQLiteEvidenceStore` — append-only, SHA256-chained |
| `packages/python/chp_core/types.py` | Python dataclasses for all protocol objects |
| `conformance/runner.py` | 9 conformance checks against a live host |
| `docs/adopter-quickstart.md` | 10-minute path to first evidence event |
| `examples/` | 14 runnable demos |

## Common pitfalls

- **Three docs are legacy** (`docs/onboarding.md`, `docs/agent-prompt.md`, `docs/capability-lookup-prompt.md`) — they describe pre-v0.1 Zenoh-mesh patterns. Do not update or reference them. They redirect to the current docs.
- **`chp-dev` is the private monorepo.** `chp-core` (this repo) is the public mirror, synced via `scripts/sync-to-public.sh`. If you're in `chp-core`, do not manually sync — the pipeline handles it.
- **`jsonschema` is a transitive dep.** Input schema validation in `host.py` uses a lazy import — do not add it to the top-level imports or it becomes a hard dependency for all users.
- **`host.invoke()` cannot run inside an async loop** — use `await host.ainvoke()` instead. The sync wrapper raises `RuntimeError` if an event loop is running.
15 changes: 13 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ Run Python tests:

```bash
cd packages/python
python -m unittest discover -s tests
python -m pytest tests/
```

Run conformance:
Run conformance (26 checks):

```bash
python conformance/runner.py
# or: chp conformance run
```

Verify spec/implementation alignment (41 checks — run before any commit touching `spec/`, `schemas/`, or `types.py`):

```bash
chp work check-alignment --repo-root .
```

Run demos:
Expand All @@ -33,6 +40,10 @@ python examples/agent-operations-demo/demo.py
python examples/mcp-bridge-demo/bridge.py
```

## For AI Agents

AI agents working in this repo should read `AGENTS.md` first — it describes the three invariants they must never violate and has the key commands in compact form.

## Pull Requests

Good first PRs:
Expand Down
55 changes: 14 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ One command wires automatic evidence capture for every Claude Code session. See
- Replay by correlation ID
- Minimal conformance requirements

## Quickstart
## From Source

Install the Python reference host from this checkout:

Expand All @@ -44,46 +44,22 @@ Run the agent/tool observability demo:
python examples/agent-operations-demo/demo.py
```

Run a served capability host endpoint demo:

```bash
chp demo endpoint
```

Run conformance:
Run conformance (29 checks):

```bash
python conformance/runner.py
```

Record development work as CHP evidence:

```bash
chp work run \
--intent "Verify CHP tests." \
--correlation-id chp-dev-001 \
--test-run unit \
-- python -m unittest discover -s packages/python/tests
chp work summary chp-dev-001
```

Validate the served-host demo as evidence:

```bash
chp work validate-demo endpoint --correlation-id chp-demo-validation
chp work replay chp-demo-validation
```

Check v0.1 protocol alignment:
Run the test suite:

```bash
chp work check-alignment --correlation-id chp-alignment
python -m pytest packages/python/tests/
```

Check launch messaging:
Check spec/implementation alignment (41 checks — required before commits to `spec/` or `types.py`):

```bash
chp work check-messaging --correlation-id chp-messaging
chp work check-alignment --repo-root .
```

## Minimal Capability
Expand Down Expand Up @@ -116,23 +92,20 @@ The host emits `execution_started` and `execution_completed` evidence for the in

## Repository Map

- `spec/chp-v0.1.md`: minimal CHP v0.1 specification
- `schemas/`: JSON Schemas for protocol objects
- `packages/python/chp_core/`: reference local host
- `spec/chp-v0.1.md`: normative CHP v0.1 specification
- `schemas/`: JSON Schemas for all protocol objects
- `packages/python/chp_core/`: Python reference host (`LocalCapabilityHost`, `SQLiteEvidenceStore`)
- `conformance/`: 29-check conformance runner
- `AGENTS.md`: orientation for AI agents working in this repo
- `docs/llms.txt`: compact protocol reference for LLM context windows
- `docs/adopter-quickstart.md`: 10-minute path to first evidence event
- `docs/roadmap.md`: shipped history and upcoming milestones
- `examples/capability-host-endpoint-demo/`: HTTP-served host demo
- `examples/agent-operations-demo/`: agent/tool observability demo
- `examples/codex-self-observation-demo/`: Codex dogfooding demo
- `examples/mcp-bridge-demo/`: experimental MCP-style bridge prototype
- `conformance/`: conformance runner
- `docs/comparisons/chp-vs-mcp.md`: precise MCP comparison
- `docs/comparisons/chp-and-opentelemetry.md`: OpenTelemetry alignment note
- `docs/comparisons/landscape.md`: adjacent framework comparison
- `docs/design/codex-self-observation.md`: Codex dogfooding pattern
- `docs/design/public-v0.1-internal-legacy-boundary.md`: public/internal boundary
- `docs/design/evidence-integrity-v0.2.md`: future evidence integrity proposal
- `docs/security/threat-model-v0.1.md`: v0.1 threat model
- `docs/release-checklist-v0.1.md`: release-readiness checklist
- `docs/packaging-v0.1.md`: packaging and versioning plan

## CHP vs MCP

Expand Down
150 changes: 141 additions & 9 deletions conformance/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
LocalCapabilityHost,
SQLiteEvidenceStore,
)
from sample_failing_hosts import BrokenNoEvidenceHost # noqa: E402
from sample_failing_hosts import ( # noqa: E402
BrokenNoEvidenceHost,
BrokenNoHashChainHost,
BrokenNonStandardCodesHost,
)


Check = Callable[[Any], Awaitable[None]]
Expand Down Expand Up @@ -1089,6 +1093,124 @@ async def check_persistence(_host: Any) -> None:
mgr2.close_conn()


async def check_standard_denial_codes(host: Any) -> None:
"""Invoking a missing capability produces the standard denial code 'capability_not_found'."""
corr_id = "conf-denial-codes-001"
result = await invoke_host(host, "nonexistent.capability.xyz", {}, correlation={"correlation_id": corr_id})
assert not result_value(result, "success"), "expected failure for missing capability"
assert result_value(result, "outcome") == "denied", (
f"expected 'denied', got {result_value(result, 'outcome')!r}"
)
denial = result_value(result, "denial")
if isinstance(denial, dict):
code = denial.get("code")
else:
code = getattr(denial, "code", None)
assert code == "capability_not_found", (
f"expected standard code 'capability_not_found', got {code!r}"
)


async def check_input_schema_validation(_host: Any) -> None:
"""A capability with input_schema rejects non-conforming payloads before execution."""
import tempfile, os
from chp_core import CapabilityDescriptor, LocalCapabilityHost, SQLiteEvidenceStore

with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f:
store_path = f.name
try:
store = SQLiteEvidenceStore(store_path)
host = LocalCapabilityHost("conf-schema", store=store)

async def handler(_ctx, _payload):
return {"ok": True}

host.register(
CapabilityDescriptor(
id="conf.typed",
version="1.0.0",
description="Typed capability.",
input_schema={
"type": "object",
"properties": {"n": {"type": "integer"}},
"required": ["n"],
"additionalProperties": False,
},
),
handler,
)

bad = await host.ainvoke(
"conf.typed",
{"n": "not-an-integer"},
correlation={"correlation_id": "conf-schema-bad"},
)
assert not bad.success, "expected denial for invalid payload"
assert bad.outcome == "denied", f"expected denied, got {bad.outcome!r}"
assert bad.denial.code == "input_schema_validation_failed", (
f"expected 'input_schema_validation_failed', got {bad.denial.code!r}"
)

good = await host.ainvoke(
"conf.typed",
{"n": 42},
correlation={"correlation_id": "conf-schema-good"},
)
assert good.success, f"valid payload should succeed, got: {good}"
store.close()
finally:
os.unlink(store_path)


def _assert_hash_chain(events: list[Any]) -> None:
"""Shared assertion: events must carry content_hash and link via prev_hash."""
assert len(events) >= 2, f"expected at least 2 events, got {len(events)}"
for ev in events:
assert "content_hash" in ev, f"missing content_hash in event seq={ev.get('sequence')}"
assert isinstance(ev["content_hash"], str) and len(ev["content_hash"]) == 64, (
f"content_hash must be a 64-char hex string, got {ev['content_hash']!r}"
)
second = events[1]
assert "prev_hash" in second, "second event must have prev_hash linking to first"
assert second["prev_hash"] == events[0]["content_hash"], (
"prev_hash of second event must equal content_hash of first"
)


async def check_evidence_hash_chain(host: Any) -> None:
"""Evidence events carry SHA256 content_hash + prev_hash to form a tamper-detectable chain."""
corr_id = "conf-chain-001"

if hasattr(host, "by_correlation_with_hashes"):
# Host exposes hash-aware replay — test it directly (catches BrokenNoHashChainHost)
await invoke_host(host, "conformance.echo", {"value": "integrity-check"}, correlation={"correlation_id": corr_id})
events = host.by_correlation_with_hashes(corr_id)
_assert_hash_chain(events)
else:
# Fall back: create an isolated reference host and verify the SQLiteEvidenceStore chain
import tempfile, os
from chp_core import CapabilityDescriptor, LocalCapabilityHost, SQLiteEvidenceStore

with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as f:
store_path = f.name
try:
store = SQLiteEvidenceStore(store_path)
ref_host = LocalCapabilityHost("conf-chain", store=store)

async def echo(_ctx, payload):
return {"echo": payload.get("value")}

ref_host.register(CapabilityDescriptor(id="conf.chain.echo", version="1.0.0", description=""), echo)
await ref_host.ainvoke("conf.chain.echo", {"value": "integrity-check"}, correlation={"correlation_id": corr_id})
events = store.by_correlation_with_hashes(corr_id)
_assert_hash_chain(events)
chain_result = store.verify_chain(corr_id)
assert chain_result.valid, f"hash chain should be valid, got: {chain_result}"
store.close()
finally:
os.unlink(store_path)


CHECKS: list[tuple[str, Check]] = [
("capability declaration", check_declaration),
("capability discovery", check_discovery),
Expand Down Expand Up @@ -1116,16 +1238,26 @@ async def check_persistence(_host: Any) -> None:
("compliance capability", check_compliance_capability),
("incident capability", check_incident_capability),
("sqlite persistence", check_persistence),
("standard denial codes", check_standard_denial_codes),
("input schema validation", check_input_schema_validation),
("evidence hash chain", check_evidence_hash_chain),
]


SAMPLE_HOSTS = {
"passing": build_passing_host,
"failing-no-evidence": lambda: BrokenNoEvidenceHost(),
"failing-non-standard-codes": lambda: BrokenNonStandardCodesHost(),
"failing-no-hash-chain": lambda: BrokenNoHashChainHost(),
}


async def run(sample: str) -> list[CheckResult]:
if sample == "passing":
host = await build_passing_host()
elif sample == "failing-no-evidence":
host = BrokenNoEvidenceHost()
else:
raise ValueError(f"unknown sample host: {sample}")
builder = SAMPLE_HOSTS.get(sample)
if builder is None:
raise ValueError(f"unknown sample host: {sample!r}. Choices: {list(SAMPLE_HOSTS)}")
host_or_coro = builder()
host = await host_or_coro if hasattr(host_or_coro, "__await__") else host_or_coro

results = []
for name, check in CHECKS:
Expand All @@ -1141,9 +1273,9 @@ def main() -> int:
parser = argparse.ArgumentParser(description="Run CHP v0.1 conformance checks.")
parser.add_argument(
"--sample",
choices=["passing", "failing-no-evidence"],
choices=list(SAMPLE_HOSTS),
default="passing",
help="Built-in sample host to test.",
help="Built-in sample host to test against.",
)
args = parser.parse_args()

Expand Down
Loading
Loading