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
51 changes: 25 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Turn any LangChain agent into a trust-verified agent in 2 lines:
```python
from langchain_capiscio import CapiscioGuard

# Zero-config — reads CAPISCIO_API_KEY from env, connects to registry
secured = CapiscioGuard() | my_chain
# Reads CAPISCIO_API_KEY from env, connects to registry
secured = CapiscioGuard.connect() | my_chain
result = secured.invoke({"input": "Summarize this ticket"})
```

`CapiscioGuard` reads your environment, connects to the CapiscIO registry on first use, and verifies caller trust badges before every invocation.
`CapiscioGuard.connect()` reads your environment, connects to the CapiscIO registry, and returns a guard that verifies caller trust badges before every invocation — consistent with `CapiscIO.connect()` and `CapiscioMCPServer.connect()` across the ecosystem.

## Why LangChain Guard?

Expand All @@ -51,9 +51,9 @@ LangChain Guard solves this with:
Control enforcement behavior per guard instance:

```python
guard = CapiscioGuard(mode="block") # Fail closed (production default)
guard = CapiscioGuard(mode="monitor") # Warn but continue
guard = CapiscioGuard(mode="log") # Log only
guard = CapiscioGuard.connect(mode="block") # Fail closed (production default)
guard = CapiscioGuard.connect(mode="monitor") # Warn but continue
guard = CapiscioGuard.connect(mode="log") # Log only
```

## LCEL Pipe Composition
Expand All @@ -68,7 +68,7 @@ from langchain_capiscio import CapiscioGuard
from langgraph.prebuilt import create_react_agent

agent = create_react_agent(llm, tools)
secured = CapiscioGuard(mode="log") | agent
secured = CapiscioGuard.connect(mode="log") | agent
result = secured.invoke({"input": "What's 42 * 17?"})
```

Expand All @@ -94,7 +94,7 @@ Events emitted: `task_started`, `task_completed`, `task_failed`, `tool_call`, `t
from langchain_capiscio import CapiscioGuard, capiscio_guard

# Option 1: Runnable as graph node
graph.add_node("verify", CapiscioGuard())
graph.add_node("verify", CapiscioGuard.connect())

# Option 2: Decorator
@capiscio_guard(mode="block")
Expand All @@ -106,7 +106,7 @@ def call_agent(state: dict) -> dict:

### Zero-config (recommended)

Set environment variables and create a guard with no arguments:
Set environment variables and connect with no arguments:

```bash
export CAPISCIO_API_KEY="cap_..."
Expand All @@ -116,41 +116,39 @@ export CAPISCIO_DEV_MODE="true" # optional
```

```python
guard = CapiscioGuard() # reads env vars, connects on first invoke()
guard = CapiscioGuard.connect() # reads env vars, connects eagerly
```

### Explicit configuration

```python
guard = CapiscioGuard(
mode="block",
guard = CapiscioGuard.connect(
api_key="cap_...",
mode="block",
name="my-agent",
server_url="https://dev.registry.capisc.io",
)
```

### `connect_kwargs`
### Extra connect kwargs

Pass extra keyword arguments through to `CapiscIO.connect()`:
Pass additional keyword arguments through to `CapiscIO.connect()`:

```python
guard = CapiscioGuard(
guard = CapiscioGuard.connect(
mode="log",
connect_kwargs={
"dev_mode": True,
"keys_dir": "capiscio_keys/",
"agent_card": my_card_dict,
},
dev_mode=True,
keys_dir="capiscio_keys/",
agent_card=my_card_dict,
)
```

## Using Environment Variables

`CapiscioGuard.from_env()` mirrors the `CapiscIO.from_env()` / `MCPServerIdentity.from_env()` pattern used across CapiscIO packages:
`CapiscioGuard.connect()` reads environment variables automatically. `from_env()` is kept as a convenience alias:

```python
guard = CapiscioGuard.from_env(mode="log")
guard = CapiscioGuard.connect(mode="log")
```

| Variable | Required | Description | Default |
Expand Down Expand Up @@ -200,8 +198,8 @@ services:
```

```python
# No code changes needed — CapiscioGuard reads env vars automatically
secured = CapiscioGuard(mode="block") | my_agent
# No code changes needed — CapiscioGuard.connect() reads env vars automatically
secured = CapiscioGuard.connect(mode="block") | my_agent
```

> **Warning:** Never bake private keys into container images. Inject them at runtime via environment variables or mounted secrets.
Expand Down Expand Up @@ -241,10 +239,11 @@ set_capiscio_context(CapiscioRequestContext(

### Guard

- `CapiscioGuard(mode, api_key, name, server_url, connect_kwargs, identity, config)` — LCEL-composable trust enforcement Runnable
- `CapiscioGuard.connect(api_key, *, mode, name, server_url, dev_mode, **kwargs)` — Connect to registry and return a ready-to-use guard (recommended)
- `CapiscioGuard.from_env(mode, **kwargs)` — Alias for `connect()` (reads env vars)
- `CapiscioGuard(*, identity, config, mode, api_key, name, server_url, connect_kwargs)` — Low-level constructor (keyword-only, lazy init on first invoke)
- `CapiscioGuard.invoke(input, config)` — Verify badge and pass through to downstream
- `CapiscioGuard.ainvoke(input, config)` — Async version
- `CapiscioGuard.from_env(mode, **kwargs)` — Create guard from environment variables

### Callbacks

Expand Down
2 changes: 1 addition & 1 deletion langchain_capiscio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from langchain_capiscio import CapiscioGuard
from langchain_openai import ChatOpenAI

secured = CapiscioGuard() | ChatOpenAI()
secured = CapiscioGuard.connect() | ChatOpenAI()
result = secured.invoke("Summarise quarterly earnings")
"""

Expand Down
97 changes: 89 additions & 8 deletions langchain_capiscio/guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,100 @@ def __init__(
self._wire_badge_renewal(identity)
self._initialized = True

@classmethod
def connect(
cls,
api_key: str | None = None,
*,
mode: str = "block",
name: str | None = None,
server_url: str | None = None,
dev_mode: bool | None = None,
**kwargs: Any,
) -> CapiscioGuard:
"""Connect to CapiscIO and return a ready-to-use guard.

This is the recommended entry point — consistent with
``CapiscIO.connect()`` and ``CapiscioMCPServer.connect()``
across the CapiscIO ecosystem.

Eagerly connects to the registry so errors surface immediately
rather than on first ``invoke()``.

Args:
api_key: Registry API key. Falls back to ``CAPISCIO_API_KEY``
env var when ``None``.
mode: Enforcement mode — ``"block"``, ``"monitor"``, or ``"log"``.
name: Agent name for registration. Falls back to
``CAPISCIO_AGENT_NAME`` env var.
server_url: Registry URL override. Falls back to
``CAPISCIO_SERVER_URL`` env var.
dev_mode: Enable dev mode (relaxed validation). When ``None``,
falls back to ``CAPISCIO_DEV_MODE`` env var. Explicit
``True``/``False`` overrides the env var.
**kwargs: Extra keyword arguments forwarded to
``CapiscIO.connect()`` (e.g. ``keys_dir``, ``agent_card``).

Returns:
A fully-initialized ``CapiscioGuard`` ready for use in LCEL
pipes or as a standalone Runnable.

Raises:
CapiscioConfigError: If no API key is available.

Example::

guard = CapiscioGuard.connect(mode="block")
secured = guard | my_chain
"""
import os

effective_api_key = api_key or os.environ.get("CAPISCIO_API_KEY")
if not effective_api_key:
raise CapiscioConfigError(
"No API key provided. Set CAPISCIO_API_KEY env var"
" or pass api_key= to CapiscioGuard.connect()."
)

from capiscio_sdk import CapiscIO

connect_kwargs: dict[str, Any] = {**kwargs}

effective_name = name or os.environ.get("CAPISCIO_AGENT_NAME")
if effective_name:
connect_kwargs["name"] = effective_name

effective_url = server_url or os.environ.get("CAPISCIO_SERVER_URL")
if effective_url:
connect_kwargs["server_url"] = effective_url

if dev_mode is None:
dev_mode = os.environ.get("CAPISCIO_DEV_MODE", "").lower() in (
"true",
"1",
"yes",
)
if dev_mode:
connect_kwargs["dev_mode"] = True

identity = CapiscIO.connect(effective_api_key, **connect_kwargs)
config = cls._make_config(mode)

return cls(identity=identity, config=config, mode=mode)

@classmethod
def from_env(cls, *, mode: str = "block", **kwargs: Any) -> CapiscioGuard:
"""Create a CapiscioGuard using environment variables for configuration.
"""Create a guard from environment variables.

Reads CAPISCIO_API_KEY, CAPISCIO_SERVER_URL, CAPISCIO_AGENT_NAME,
and CAPISCIO_DEV_MODE from the environment. Mirrors the ``from_env()``
pattern used by ``CapiscIO.from_env()`` and
``MCPServerIdentity.from_env()``.
Convenience alias for ``CapiscioGuard.connect()`` — reads API key,
agent name, server URL, and dev mode from environment variables.
Explicit ``**kwargs`` override env vars when provided.

Any explicit keyword arguments are forwarded to the constructor and
take precedence over environment variables.
.. deprecated::
Prefer ``CapiscioGuard.connect()`` for consistency with the
rest of the CapiscIO ecosystem.
Comment thread
beonde marked this conversation as resolved.
"""
return cls(mode=mode, **kwargs)
return cls.connect(mode=mode, **kwargs)

@staticmethod
def _make_config(mode: str) -> Any:
Expand Down
Loading
Loading