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
60 changes: 60 additions & 0 deletions .github/workflows/lint-gateway.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Lint Gateway Enforcement

on:
push:
branches:
- main
- "feat/**"
pull_request:
branches:
- main

jobs:
lint-gateway:
name: Ensure no raw client imports outside gateway
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check for raw GitHub imports outside gateway
run: |
echo "Checking for raw Github imports outside src/scyvera/gateway.py (excluding tests/)..."
if grep -r "from github import Github" --include="*.py" src/ --exclude="gateway.py" 2>/dev/null; then
echo "FAIL: raw Github import 'from github import Github' found outside gateway"
exit 1
fi
if grep -r "import github" --include="*.py" src/ --exclude="gateway.py" 2>/dev/null; then
echo "FAIL: raw 'import github' found outside gateway"
exit 1
fi
echo "PASS: no raw Github imports outside gateway"

- name: Check for raw Qdrant imports outside gateway
run: |
echo "Checking for raw QdrantClient imports outside src/scyvera/gateway.py (excluding tests/)..."
if grep -r "from qdrant_client import" --include="*.py" src/ --exclude="gateway.py" 2>/dev/null; then
echo "FAIL: raw QdrantClient import 'from qdrant_client import' found outside gateway"
exit 1
fi
if grep -r "import qdrant_client" --include="*.py" src/ --exclude="gateway.py" 2>/dev/null; then
echo "FAIL: raw 'import qdrant_client' found outside gateway"
exit 1
fi
echo "PASS: no raw QdrantClient imports outside gateway"

- name: Check for direct Github instantiation outside gateway
run: |
echo "Checking for direct Github( instantiation outside gateway..."
if grep -r "Github(" --include="*.py" src/ --exclude="gateway.py" 2>/dev/null; then
echo "FAIL: direct Github( instantiation found outside gateway"
exit 1
fi
echo "PASS: no direct Github instantiation outside gateway"

- name: Verify tests are excluded (informational)
run: |
echo "Tests may import clients for mocking — excluded from enforcement"
grep -r "from github import Github" --include="*.py" tests/ 2>/dev/null && echo "tests/ contains mocked Github imports (allowed)" || echo "no test Github imports"
grep -r "from qdrant_client import" --include="*.py" tests/ 2>/dev/null && echo "tests/ contains mocked Qdrant imports (allowed)" || echo "no test Qdrant imports"
2 changes: 1 addition & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build pytest PyYAML jsonschema
python -m pip install build pytest PyYAML jsonschema PyGithub qdrant-client

- name: Verify Version matches Tag
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build pytest PyYAML jsonschema
python -m pip install build pytest PyYAML jsonschema PyGithub qdrant-client

- name: Build wheel and source distribution
run: python -m build
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [1.1.3] — 2026-09-05

### Added
- **Gateway Enforcement Layer (`BaseGateway`, `GitHubGateway`, `QdrantGateway`)**:
- Single enforcement boundary for external API operations and credential management.
- Per-instance `@enforcer.gate` wrapping with default-deny semantics.
- Optional dependency handling for `PyGithub` and `qdrant-client` to keep core package dependency-light.
- **Gateway Exception Wrapping**:
- Introduced `GatewayError` to encapsulate third-party API client exceptions.
- Added GitHub Actions workflow for gateway linting (`.github/workflows/lint-gateway.yml`).

### Changed
- `ContractEnforcer.gate()` now supports `"read"` as an action type alias for permission gating.

---

## [1.1.2] — 2026-08-30

### Changed
Expand Down
77 changes: 66 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,42 @@ A framework-independent and domain-independent specification layer describing sy

---

## 🛡️ Validation vs. Runtime Enforcement
## 🛡️ Governance & Enforcement Layers

Scyvera provides two complementary governance layers:
Scyvera provides three complementary governance layers:

| Layer | Component | Responsibility | Trust Level |
|---|---|---|---|
| **Tier 1 & 2: Static Verification** | `validate_contract()`, `lint_contract()` | Validates that a `contract.yaml` is structurally and semantically well-formed against specification schemas. | Declaration conformance |
| **Runtime Enforcement** | `ContractEnforcer` | Gates real-world Python calls against declared permissions and side effects using **Default-Deny** rules. Halts execution on approval points. | Execution boundary enforcement |
| **Gateway Enforcement** | `BaseGateway`, `GitHubGateway`, `QdrantGateway` | Single enforcement boundary: holds credentials privately and gates every third-party API call, structurally preventing gate bypass. | Credential & API isolation |

---

## ⚡ Quickstart

### 1. Installation

Install locally or in your project virtualenv:
Install the core package from PyPI:

```bash
pip install scyvera
# Or for local development:
pip install -e .
```

Or install with optional gateway dependencies:

```bash
# With PyGithub for GitHubGateway
pip install "scyvera[github]"

# With qdrant-client for QdrantGateway
pip install "scyvera[qdrant]"

# With all gateway adapters
pip install "scyvera[all]"

# For local development:
pip install -e ".[dev]"
```

### 2. Command-Line Interface (CLI)
Expand Down Expand Up @@ -126,11 +141,40 @@ enforcer.verify_integrity() # Raises ContractTamperError if contract.yaml was m

---

## 🚪 Gateway Enforcement Architecture (v1.1.3)

In automated agent workflows, security boundaries fail if agent nodes can bypass enforcers by importing and calling API clients directly. Scyvera solves this with **Gateways** (`BaseGateway`, `GitHubGateway`, `QdrantGateway`):

- **Single Enforcement Boundary**: API credentials and raw SDK clients (e.g. `PyGithub`, `qdrant-client`) are encapsulated privately within the gateway. No outside node holds credentials or raw clients.
- **Structural Gating**: Every public method is decorated per-instance with `@enforcer.gate(...)`. Any undeclared or unapproved action halts execution immediately via `ContractViolationError` or `ApprovalPendingError`.
- **Exception Normalization**: Client exceptions are caught and wrapped into `GatewayError` to ensure deterministic fault isolation.

```python
from scyvera import ContractEnforcer, GitHubGateway, GatewayError, ContractViolationError

# 1. Load the contract enforcer
enforcer = ContractEnforcer.load("contract.yaml")

# 2. Instantiate gateway with injected enforcer and private credentials
gateway = GitHubGateway(enforcer, token="ghp_...")

# 3. Gated read operation (succeeds if declared in contract)
issue = gateway.read_issue("owner/repo", issue_number=42)

# 4. Gated mutating operation (enforces Default-Deny & approvals)
try:
gateway.close_issue("owner/repo", issue_number=42)
except ContractViolationError as e:
print(f"Blocked by Scyvera: {e}")
```

---

## ⚠️ What Scyvera Does Not Guarantee

Scyvera provides structural verification and runtime gating, but security is an end-to-end discipline. Integrators must understand the following technical boundaries:
Scyvera provides structural verification, runtime gating, and gateway isolation, but security is an end-to-end discipline. Integrators must understand the following technical boundaries:

1. **Gate Bypass (Threat T4)**: Scyvera enforces boundaries at the `@enforcer.gate(...)` decorator. In Python, the runtime cannot physically prevent code from directly calling an un-decorated internal function. Integrators should use `ContractEnforcer.assert_gated(fn)` within their integration test suites to verify that all external-facing tool call sites are wrapped.
1. **Gate Bypass (Threat T4)**: Scyvera enforces boundaries at the `@enforcer.gate(...)` decorator. To eliminate the risk of ungated bypass, applications should use Scyvera **Gateways** (`BaseGateway`, `GitHubGateway`, `QdrantGateway`), where credentials and raw SDK clients are kept strictly private inside the gateway. If invoking ungated custom functions directly, integrators should use `ContractEnforcer.assert_gated(fn)` in test suites.
2. **Internal Third-Party Behavior / String Spoofing (Threat T6)**: Scyvera verifies that a declared intent (e.g. `github:issues:write`) matches an allowed permission in the contract. It does not perform dynamic bytecode analysis or network-packet inspection to guarantee that a decorated library function does not execute unauthorized background calls. The integrity of third-party dependencies remains the responsibility of dependency scanning and peer review.
3. **Pre-Load File Tampering (Threat T5)**: The SHA-256 integrity check in `verify_integrity()` protects against file replacement AFTER load. It does not protect against a tampered `contract.yaml` being present BEFORE `ContractEnforcer.load()` is called. The deployment environment is responsible for protecting the contract file prior to load.

Expand Down Expand Up @@ -228,14 +272,25 @@ agent-contracts/
│ ├── __init__.py
│ ├── builder.py # Programmatic Contract Builder API
│ ├── validator.py # Multi-Version Validator Engine
│ ├── cli.py # CLI Application (validate, init)
│ ├── enforcer.py # Runtime Contract Enforcer
│ ├── gateway.py # Gateway Enforcement Layer (GitHub, Qdrant)
│ ├── exceptions.py # Exception Hierarchy
│ ├── linter.py # Semantic Contract Linter
│ ├── cli.py # CLI Application (validate, init, lint)
│ └── schemas/ # Bundled Package Schemas
├── tests/
│ ├── fixtures/ # Test Fixture Files
│ ├── test_validator.py # v1 Validator Unit Tests
│ ├── test_validator_v1_1.py # v1.1 Validator Unit Tests
│ ├── test_builder.py # Programmatic Builder Unit Tests
│ └── test_cli.py # CLI Unit Tests
│ ├── test_cli.py # CLI Unit Tests
│ ├── test_duplicate_issue_detector_workflow.py # Workflow Embedding Tests
│ ├── test_enforcer.py # Runtime Enforcer Tests (T1-T10)
│ ├── test_filename_enforcement.py # Filename Rule Tests (.yaml only)
│ ├── test_gateway.py # Gateway Enforcement Tests
│ ├── test_lifecycle.py # Lifecycle Validation Tests
│ ├── test_linter.py # Semantic Linter Tests
│ ├── test_schemas_sync.py # Schema Synchronization Tests
│ ├── test_validator.py # v1 Validator Unit Tests
│ └── test_validator_v1_1.py # v1.1 Validator Unit Tests
└── implementations/ # Multi-Framework Reference Implementations
├── n8n/
└── langgraph/
Expand Down
10 changes: 10 additions & 0 deletions implementations/langgraph/duplicate-issue-detector/contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ permissions:
- github: issues:read+write # comment only — never close, label, or edit
- openai: embeddings:read
- qdrant: read+write on one collection
- github.read: read
- qdrant.search: search

side_effects:
- Posts one comment on the triggering issue, if similarity >= threshold
- Writes one vector to the Qdrant collection (always, regardless of duplicate result)
- github.comment
- github.read
- github.close
- github.label
- github.merge
- qdrant.write
- qdrant.delete
- qdrant.search

approval_points: [] # intentionally empty — this workflow only ever comments,
# never closes or merges, so no approval gate is required
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import httpx
from github import Github, GithubException
from openai import OpenAI

from scyvera import ContractEnforcer
from scyvera.exceptions import GatewayError
from scyvera.gateway import GitHubGateway

from .state import State

log = logging.getLogger(__name__)
Expand All @@ -17,13 +21,27 @@
_SEARCH_LIMIT = 5


def _get_enforcer() -> ContractEnforcer:
"""Load the workflow contract enforcer.

The contract is resolved relative to this file so the gateway
always enforces the contract that ships with the workflow.
"""
contract_path = Path(__file__).resolve().parent.parent / "contract.yaml"
return ContractEnforcer.load(contract_path)


def detect(state: State) -> dict[str, Any]:
"""Fetch issue title + body from GitHub. Read-only, no side effects."""
log.info("DETECT #%d from %s/%s", state["issue_number"], state["repo_owner"], state["repo_name"])
try:
repo = Github(state["github_token"]).get_repo(f"{state['repo_owner']}/{state['repo_name']}")
issue = repo.get_issue(number=state["issue_number"])
except GithubException as exc:
enforcer = _get_enforcer()
gateway = GitHubGateway(enforcer, token=state["github_token"])
issue = gateway.read_issue(
f"{state['repo_owner']}/{state['repo_name']}",
state["issue_number"],
)
except (GatewayError, Exception) as exc:
log.error("DETECT error: %s", exc)
raise

Expand Down Expand Up @@ -107,9 +125,14 @@ def act(state: State) -> dict[str, Any]:
"This is an automated suggestion — a maintainer will confirm."
)
try:
repo = Github(state["github_token"]).get_repo(f"{state['repo_owner']}/{state['repo_name']}")
repo.get_issue(number=state["issue_number"]).create_comment(body)
except GithubException as exc:
enforcer = _get_enforcer()
gateway = GitHubGateway(enforcer, token=state["github_token"])
gateway.post_comment(
f"{state['repo_owner']}/{state['repo_name']}",
state["issue_number"],
body,
)
except (GatewayError, Exception) as exc:
log.error("ACT error: %s", exc)
raise
log.info("ACT comment posted")
Expand Down
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "scyvera"
version = "1.1.2"
version = "1.1.3"
description = "Framework-independent contracts and validation tooling for AI agents and automated workflows."
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -46,9 +46,21 @@ dependencies = [
]

[project.optional-dependencies]
github = [
"PyGithub>=2.0.0",
]
qdrant = [
"qdrant-client>=1.7.0",
]
all = [
"PyGithub>=2.0.0",
"qdrant-client>=1.7.0",
]
dev = [
"pytest>=8.0",
"build>=1.0",
"PyGithub>=2.0.0",
"qdrant-client>=1.7.0",
]

[project.scripts]
Expand Down
8 changes: 7 additions & 1 deletion src/scyvera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
ContractValidationError,
ContractVersionError,
ContractViolationError,
GatewayError,
)
from .gateway import BaseGateway, GitHubGateway, QdrantGateway
from .linter import LintResult, LintWarning, lint_contract
from .validator import (
LIFECYCLE_DEFAULTS,
Expand All @@ -21,13 +23,14 @@
validate_contract,
)

__version__ = "1.1.2"
__version__ = "1.1.3"

__all__ = [
"__version__",
"AgentIdentity",
"ApprovalPendingError",
"AuditEntry",
"BaseGateway",
"Contract",
"ContractEnforcer",
"ContractFileNameError",
Expand All @@ -36,9 +39,12 @@
"ContractVersion",
"ContractVersionError",
"ContractViolationError",
"GatewayError",
"GitHubGateway",
"LIFECYCLE_DEFAULTS",
"LintResult",
"LintWarning",
"QdrantGateway",
"SCHEMA_V1_PATH",
"SCHEMA_V1_1_PATH",
"SystemIdentity",
Expand Down
Loading
Loading