Skip to content
Merged
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --extra dev
- name: Ruff check
run: uv run ruff check .
- name: Ruff format check
run: uv run ruff format --check .

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --extra dev
- name: Run tests
run: uv run pytest tests/ -v --tb=short
- name: Run live agent integration test
if: env.OPENROUTER_API_KEY != ''
continue-on-error: true
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: uv run pytest tests/test_agent_integration.py -v --tb=short -k "LiveOpenRouter"

test-with-z3:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v4
- name: Install dependencies
run: uv sync --extra dev --extra analysis
- name: Run analysis tests
run: uv run pytest tests/test_progent/test_analysis.py -v --tb=short
107 changes: 107 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Progent is a **Programmable Privilege Control framework for LLM Agents**. It enforces fine-grained security policies on AI agent tool calls using JSON Schema validation. Policies are declarative (JSON) and framework-agnostic.

## Common Commands

### Setup
```bash
uv sync # Install all dependencies
uv sync --extra analysis # Include Z3 policy analysis
uv sync --extra all # Include all optional deps
```

### Testing
```bash
uv run pytest tests/ # Run all tests
uv run pytest tests/test_core.py # Run a single test file
uv run pytest tests/test_core.py::test_function_name # Run a single test
```

### Linting
```bash
uv run ruff check . # Lint
uv run ruff format . # Format
```

### Running the Example Agent
```bash
cd implementations/examples/coding_agent
uv run run_agent.py # Requires OPENROUTER_API_KEY in .env
uv run run_agent.py --framework langchain --policies policies.json
```

## Architecture

### Three-Layer Structure

1. **`progent/`** — Core SDK library (pip-installable)
- `core.py` — Global policy/tool registry + `check_tool_call()` enforcement
- `policy.py` — Policy loading from JSON files or dicts
- `validation.py` — Argument validation (JSON Schema, regex, callables)
- `wrapper.py` — `@secure` decorator and LangChain tool wrapping
- `exceptions.py` — `ProgentBlockedError`, `PolicyValidationError`, `PolicyLoadError`, `PolicyConfigError`
- `analysis.py` — Z3-based conflict detection (optional, requires `z3-solver`)
- `generation.py` — LLM-based policy generation (optional, not yet tested)
- `adapters/` — `langchain.py`, `mcp.py`

2. **`implementations/`** — Example agent implementations using the SDK
- `core/tool_definitions.py` — Single source of truth for tool schemas
- `core/secured_executor.py` — Tool wrapping with logging + policy
- `core/progent_enforcer.py` — Wrapper around progent library
- `frameworks/base_agent.py` — Abstract base; subclassed by `langchain_agent.py`, `adk_agent.py`, `raw_sdk_agent.py`
- `tools/` — Reusable tool implementations (file, command, communication)

3. **`secagent/`** — Reference implementation from the Progent paper authors

### Policy Format

Policies map tool names to rule lists: `{tool_name: [(priority, effect, conditions, fallback), ...]}`. Effect `0` = allow, `1` = deny. Conditions are JSON Schema constraints on arguments. Default behavior is deny (no matching rule = blocked).

### Policy Evaluation Flow

```
Tool Call → check_tool_call(name, kwargs)
→ Iterate rules by priority
→ Match allow rule? → Execute
→ Match deny rule? → Block (fallback: 0=error, 1=terminate, 2=ask user)
→ No match? → Block (default deny)
```

### Core API

```python
from progent import secure, load_policies, check_tool_call

load_policies("policies.json") # Load policy file
check_tool_call("tool", {args}) # Manual check (raises ProgentBlockedError)

@secure # Decorator
def my_tool(arg: str) -> str: ...

tools = secure([tool1, tool2]) # Wrap list of tools/functions
```

### Global State

`progent/core.py` uses module-level globals (`_available_tools`, `_security_policy`, `_user_query`) with getter/setter functions. Tests must call `reset_security_policy()` in fixtures to avoid state leakage.

## Key Conventions

- Python >=3.11, target 3.12 (`.python-version`)
- Ruff for linting/formatting (line-length 100, ignores E501)
- pytest with `asyncio_mode = "auto"`
- Build system: hatchling
- Environment variables via `.env` (gitignored): `OPENROUTER_API_KEY`, `PROGENT_POLICY_MODEL`

## Known Weak Spots (from TODO.md)

- MCP adapter (`progent/adapters/mcp.py`) has no real-world testing
- `progent/generation.py` is untested and not integrated
- `implementations/core/progent_enforcer.py` duplicates some logic from `progent/core.py`
- Z3 regex-to-Z3 conversion may fail on edge cases
- No CI/CD pipeline
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@ Secure coding agent with policy enforcement via the `progent` library.
### UV approach(RECOMMENDED)

```bash
# 1. Activates the environment and installs all packages
# 1. Install core dependencies
uv sync

# 1b. Install optional framework extras as needed
uv sync --extra adk # Google ADK (Gemini)
uv sync --extra claude-sdk # Claude Agent SDK
uv sync --extra analysis # Z3 policy analysis
uv sync --extra all # Everything

# 2. Add OPENROUTER_API_KEY to your .env

# 3. Run the agent
Expand Down Expand Up @@ -67,7 +73,8 @@ implementations/ # AGENT IMPLEMENTATIONS
│ ├── base_agent.py # Shared agent logic
│ ├── langchain_agent.py # LangChain adapter
│ ├── adk_agent.py # Google ADK adapter
│ └── raw_sdk_agent.py # OpenAI SDK adapter
│ ├── raw_sdk_agent.py # OpenAI SDK adapter
│ └── claude_sdk_agent.py # Claude Agent SDK adapter
├── tools/ # Tool implementations (stable)
│ ├── file_tools.py
│ ├── command_tools.py
Expand Down Expand Up @@ -115,17 +122,20 @@ python run_agent.py # Default (LangChain + sandbox)
python run_agent.py --workspace ./my_project # Custom workspace
python run_agent.py --framework adk # Use Google ADK
python run_agent.py --framework raw_sdk # Use raw OpenAI SDK
python run_agent.py --framework claude_sdk # Use Claude Agent SDK
python run_agent.py --model anthropic/claude-3.5-sonnet # Different model
python run_agent.py --policies custom.json # Custom policies file
python run_agent.py --api-base https://api.openai.com/v1 # Use OpenAI directly
```

| Flag | Description |
|------|-------------|
| `-w, --workspace` | Working directory for the agent |
| `-f, --framework` | `langchain`, `adk`, or `raw_sdk` |
| `-f, --framework` | `langchain`, `adk`, `raw_sdk`, or `claude_sdk` |
| `-m, --model` | LLM model (OpenRouter format) |
| `-p, --policies` | Path to policies JSON file |
| `-c, --config` | Path to config YAML file |
| `--api-base` | API base URL override |


## REPL Commands
Expand All @@ -144,7 +154,7 @@ All config files are in `implementations/examples/coding_agent/`:
**`config.yaml`** - LLM settings, system prompt, logging:
```yaml
llm:
model: meta-llama/llama-3.1-70b-instruct
model: deepseek/deepseek-v3.2
api_base: https://openrouter.ai/api/v1
```

Expand Down
34 changes: 34 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Security Policy

We take security and privacy matters very seriously. We appreciate that our users place a high degree of confidence and trust in this project and we strive to meet those expectations.

## Vulnerability Reporting

If you believe you've discovered a potential vulnerability in this project's security, please contact us at echarris@smcm.edu.

When reporting a potential vulnerability, please include as much of the following information as possible:

* A description of the vulnerability
* The impacted software or service and its version
* Proof-of-concept code and/or detailed steps to reproduce

## Secure Communications

If you're a security researcher and you believe that you have found a security issue within this project, email the details of your findings to echarris@smcm.edu. Use PGP to protect the message by using the public PGP key below.

## PGP Public Key

```
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaFImvhYJKwYBBAHaRw8BAQdAy5ilTsTRyQz3/qRhJpPr7oc/eSDcjTKzuMEB
NGnrLQK0GEV2YW4gPGVjaGFycmlzQHNtY20uZWR1PoiZBBMWCgBBFiEEkOh+eT9x
Xg6xD2CNemBP6MufhQIFAmhSJr4CGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQemBP6MufhQKbDQD/bpfEf+tmAPCVZdICdPc0jGyy0rgNBo/x
hbTnqK4c9iIA/2GY46RmJl0fErOmgnWowwI867HRLBN2kCs6RxoiIFwKuDgEaFIm
vhIKKwYBBAGXVQEFAQEHQOwcEmJWxLzgZEA7md8wMFg3Ldi3iqlFRDulcOOO+Gl5
AwEIB4h+BBgWCgAmFiEEkOh+eT9xXg6xD2CNemBP6MufhQIFAmhSJr4CGwwFCQWj
moAACgkQemBP6MufhQK0FAD/Sfzha5J8aIH2YWvlKRnvB5xwo09dBpA3y2xIA92a
NIoA/jd0S2zIH/Mqpwe45WRc7FMsalT4QoKir0EW0yjigVID
=hq5w
-----END PGP PUBLIC KEY BLOCK-----
```
4 changes: 2 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

## Missing Tests

- [ ] Integration tests for full agent loop (user prompt → tool call → policy check → result)
- [x] Integration tests for full agent loop (user prompt → tool call → policy check → result)
- [ ] Tests for `progent/generation.py`
- [ ] Tests for `progent/adapters/mcp.py` with real MCP server
- [ ] Tests for `progent/adapters/langchain.py`
Expand All @@ -37,5 +37,5 @@

- [ ] `implementations/core/progent_enforcer.py` duplicates some logic from `progent/core.py`
- [ ] Logging is scattered - could be centralized
- [ ] No CI/CD pipeline for running tests
- [x] No CI/CD pipeline for running tests (`.github/workflows/ci.yml`)

8 changes: 4 additions & 4 deletions implementations/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Core infrastructure for the Progent coding agent."""

from .logging_utils import AgentLogger, get_logger, init_logger
from .tool_registry import ToolRegistry, tool, get_registry
from .progent_enforcer import (
load_policies,
init_progent,
ProgentEnforcedRegistry,
enforce_policy,
init_progent,
load_policies,
wrap_tool_with_enforcement,
ProgentEnforcedRegistry,
)
from .tool_registry import ToolRegistry, get_registry, tool

__all__ = [
"AgentLogger",
Expand Down
12 changes: 9 additions & 3 deletions implementations/core/logging_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ def tool_call(self, tool_name: str, arguments: dict[str, Any]) -> None:
json_str = json.dumps({"name": tool_name, "arguments": arguments}, indent=None)
self.logger.info(f"LLM_TOOL_CALL: {json_str}")

def progent_decision(self, tool_name: str, arguments: dict[str, Any], allowed: bool, reason: str = "") -> None:
def progent_decision(
self, tool_name: str, arguments: dict[str, Any], allowed: bool, reason: str = ""
) -> None:
"""Log Progent policy decision."""
status = "ALLOWED" if allowed else "BLOCKED"
args_str = ", ".join(f'{k}="{v}"' if isinstance(v, str) else f"{k}={v}" for k, v in arguments.items())
args_str = ", ".join(
f'{k}="{v}"' if isinstance(v, str) else f"{k}={v}" for k, v in arguments.items()
)
msg = f"PROGENT: {status} - {tool_name}({args_str})"
if reason and not allowed:
msg += f" | Reason: {reason}"
Expand Down Expand Up @@ -101,7 +105,9 @@ def get_logger() -> AgentLogger:
return _logger


def init_logger(log_dir: str = "./logs", level: str = "INFO", session_name: Optional[str] = None) -> AgentLogger:
def init_logger(
log_dir: str = "./logs", level: str = "INFO", session_name: Optional[str] = None
) -> AgentLogger:
"""Initialize the global logger with custom settings."""
global _logger
_logger = AgentLogger(log_dir=log_dir, level=level, session_name=session_name)
Expand Down
Loading