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
12 changes: 12 additions & 0 deletions .cspell-repo-terms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -939,3 +939,15 @@ spanid
parentspanid
agtsignature
agtsignaturealg

# --- Python test suite code identifiers and scenario names ---
adtech
junitxml
addoption
addinivalue
getoption
asdict

# --- Docker / BuildKit toolchain terms ---
moby
buildkit
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,17 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
# Use the daemon's built-in BuildKit (docker driver) instead of the
# default docker-container driver. The container driver boots a
# moby/buildkit image pulled from Docker Hub (registry-1.docker.io),
# which intermittently times out on GitHub-hosted runners:
# Error response from daemon: Get "https://registry-1.docker.io/v2/":
# context deadline exceeded
# This job only runs `docker compose up --build`, which does not need
# the container driver's advanced features (multi-platform, gha cache),
# so the docker driver avoids the Hub pull and the associated flake.
driver: docker

- name: Run documented contributor flow
run: |
Expand Down
153 changes: 153 additions & 0 deletions .github/workflows/e2e-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: E2E Python

on:
push:
branches:
- main
paths-ignore:
- "**/*.md"
- "docs/**"
- "site/**"
- "mkdocs.yml"
pull_request:
paths-ignore:
- "**/*.md"
- "docs/**"
- "site/**"
- "mkdocs.yml"
workflow_dispatch: {}

concurrency:
group: e2e-python-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
top5-ollama:
name: Top 5 Python E2E (Ollama)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
AGT_E2E_ARTIFACT_DIR: artifacts/e2e-python/ollama
AGT_E2E_MODEL: llama3.1
AGT_E2E_MODEL_DIGEST: 46e0c10c039e019119339687c3c1757cc81b9da49709a3b3924863ba87ca666e
AGT_E2E_MODEL_ATTEMPTS: "3"
OLLAMA_VERSION: "0.5.7"
# v0.5.7 ollama-linux-amd64.tgz, from the publisher's sha256sum.txt:
# https://github.com/ollama/ollama/releases/tag/v0.5.7
OLLAMA_V0_5_7_LINUX_AMD64_SHA256: 8d4e054bc512d53115074c19de5a7915df78180f89c014abbae4497e4a813a3c
OLLAMA_MODELS: /home/runner/.ollama/models
PYTHONUTF8: "1"
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
cache: pip

- name: Install Rust toolchain
# The ACS Python SDK (agent-control-specification) is a PyO3
# extension over the Rust core; building it from the in-tree
# source at policy-engine/sdk/python needs cargo + a C toolchain
# (present on ubuntu-latest by default).
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

- name: Install Python E2E dependencies
run: |
set -euo pipefail
python -m pip install --upgrade pip==24.3.1
python -m pip install \
-e agent-governance-python/agent-governance-toolkit-core \
./policy-engine/sdk/python \
pytest==9.0.3 \
pytest-timeout==2.4.0

- name: Cache Ollama model
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.OLLAMA_MODELS }}
key: ollama-${{ runner.os }}-${{ env.OLLAMA_VERSION }}-${{ env.AGT_E2E_MODEL_DIGEST }}

- name: Install and start Ollama
run: |
set -euo pipefail
archive="$RUNNER_TEMP/ollama-linux-amd64.tgz"
install_dir="$RUNNER_TEMP/ollama"
curl --fail --show-error --location --retry 3 \
--output "$archive" \
"https://github.com/ollama/ollama/releases/download/v${OLLAMA_VERSION}/ollama-linux-amd64.tgz"
echo "${OLLAMA_V0_5_7_LINUX_AMD64_SHA256} ${archive}" | sha256sum --check --strict
mkdir -p "$install_dir"
tar --extract --gzip --file "$archive" --directory "$install_dir"
export PATH="$install_dir/bin:$PATH"
echo "$install_dir/bin" >> "$GITHUB_PATH"

mkdir -p "$OLLAMA_MODELS"
ollama serve >/tmp/ollama.log 2>&1 &
is_up() { curl -fsS "http://127.0.0.1:11434/api/version" >/dev/null 2>&1; }
up=0
for _ in $(seq 1 30); do
if is_up; then up=1; break; fi
sleep 2
done
if [ "$up" -ne 1 ]; then
cat /tmp/ollama.log 2>/dev/null || true
exit 1
fi
ollama pull "$AGT_E2E_MODEL"

mkdir -p "$AGT_E2E_ARTIFACT_DIR"
python - <<'PY'
import json
import os
import urllib.request
from pathlib import Path

base_url = "http://127.0.0.1:11434"
with urllib.request.urlopen(f"{base_url}/api/version", timeout=10) as response:
version = json.load(response)["version"]
with urllib.request.urlopen(f"{base_url}/api/tags", timeout=10) as response:
models = json.load(response).get("models", [])

requested_model = os.environ["AGT_E2E_MODEL"]
accepted_names = {requested_model, f"{requested_model}:latest"}
selected = next((model for model in models if model.get("name") in accepted_names), None)
if selected is None:
raise SystemExit(f"Pulled model {requested_model!r} was not listed by Ollama")

actual_digest = selected.get("digest", "")
expected_digest = os.environ["AGT_E2E_MODEL_DIGEST"]
if actual_digest != expected_digest:
raise SystemExit(
f"Model digest mismatch for {requested_model}: "
f"expected {expected_digest}, got {actual_digest}"
)

metadata = {
"model": selected.get("name"),
"model_digest": actual_digest,
"ollama_version": version,
}
output = Path(os.environ["AGT_E2E_ARTIFACT_DIR"]) / "runtime-metadata.json"
output.write_text(json.dumps(metadata, indent=2, sort_keys=True), encoding="utf-8")
print(json.dumps(metadata, sort_keys=True))
PY

- name: Run Top 5 with Ollama
run: |
set -euo pipefail
mkdir -p "$AGT_E2E_ARTIFACT_DIR"
python -m pytest tests/e2e_python \
--junitxml="$AGT_E2E_ARTIFACT_DIR/junit.xml" \
-q

- name: Upload Ollama E2E artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-python-ollama-artifacts
path: artifacts/e2e-python/ollama
if-no-files-found: warn
3 changes: 3 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ useDefault = true

# AgentMesh integration test suites (fake tokens, JWTs in test data)
'''agent-governance-python/agentmesh-integrations/.*/tests/.*''',

# Python governance E2E suite (fake PII/secrets to exercise redaction)
'''tests/e2e_python/.*''',
]
regexes = [
'''ghp_secret123''',
Expand Down
4 changes: 3 additions & 1 deletion scripts/check_v4_ratchet.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@
"scripts/v4_ratchet_baseline.json",
}
)
ALLOWED_V4_PREFIXES: tuple[str, ...] = ("agent-governance-python/agt-v4-migrate/",)
ALLOWED_V4_PREFIXES: tuple[str, ...] = (
"agent-governance-python/agt-v4-migrate/",
)

EXCLUDE_DIR_NAMES: frozenset[str] = frozenset(
{
Expand Down
125 changes: 125 additions & 0 deletions tests/e2e_python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Python Governance E2E Tests

These tests exercise five governance scenarios through production ACS
(Agent Control Specification), prompt-injection, redaction, and sandbox
APIs. External systems are represented by in-memory adtech, healthcare, and
intake resources. Each policy-driven scenario keeps its customer-authored ACS
manifest next to it (e.g. `scenarios/policy_deny/test_policy_deny.py` +
`scenarios/policy_deny/acs.manifest.yaml`). The manifest declares the
`pre_tool_call` intervention point and a host-owned custom policy, and the
native ACS runtime (`AgentControl.from_native`) evaluates it the way a
customer would ship it. Four scenarios run against a real local Ollama model;
prompt injection is intentionally blocked before a model request is made.

| Package | Production boundary | Expected result |
| --- | --- | --- |
| `policy_deny` | Adtech tool call evaluated by the native ACS runtime at `pre_tool_call` | Deny the live budget mutation; do not call the resource |
| `policy_allow` | Healthcare tool call evaluated by the native ACS runtime at `pre_tool_call` | Allow one non-diagnostic visit-note update |
| `pii_redaction` | `MuteAgent` before model and tool boundaries | Allow sanitized intake; keep raw values out of inputs, calls, and artifacts |
| `prompt_injection` | `PromptInjectionDetector` on retrieved content | Deny the poisoned document before Ollama is called |
| `filesystem_escape` | Model-generated code passed to `ExecutionSandbox` | Raise `SANDBOX_VALIDATION_FAILED`; do not create the outside file |

## Setup

Install Ollama if `ollama` is not already on your `PATH`:

```bash
curl -fsSL https://ollama.com/install.sh | sh
```

Start its server in one terminal:

```bash
ollama serve
```

In another terminal, pull the configured model:

```bash
ollama pull llama3.1
```

CI installs the Ollama version pinned in `.github/workflows/e2e-python.yml`,
verifies the release archive checksum, and verifies the pulled model digest.

The two policy scenarios evaluate through the native ACS Python SDK. The SDK
is a PyO3 extension over the Rust core; building it from the in-tree source
needs a Rust toolchain and a C compiler:

```bash
# Rust toolchain (for building the ACS SDK from source)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source "$HOME/.cargo/env"
```

The custom policy runs in-process, so no external policy binary is required.

## Run

From the repository root:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install \
-e agent-governance-python/agent-governance-toolkit-core \
./policy-engine/sdk/python \
pytest pytest-timeout
AGT_E2E_MODEL=llama3.1 \
AGT_E2E_MODEL_ATTEMPTS=3 \
.venv/bin/python -m pytest tests/e2e_python -q
```

Model output is variable. For scenarios that need a specific action, the test
tries up to `AGT_E2E_MODEL_ATTEMPTS` times and fails with `not_exercised` when the
model never produces the required tool call or code. That failure logs the
expected action and a redacted summary of the unexpected response. Once the
model participates, an unexpected policy result, tool execution, or resource
side effect fails the scenario.

The prompt-injection scenario intentionally blocks poisoned retrieval content
before it reaches the model adapter. The PII scenario calls the model only after
redaction and verifies that raw values are absent from model input, tool
arguments, resource calls, and artifacts.

Runtime defaults are defined by `support/ollama.py`:

| Setting | Default | Purpose |
| --- | --- | --- |
| `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | Ollama API endpoint |
| `AGT_E2E_MODEL` | `llama3.1` | Model used by model-backed scenarios |
| `AGT_E2E_MODEL_ATTEMPTS` | `3` | Maximum attempts to obtain the required model action |

## Logging

By default, the tests emit live INFO logs for each model request and response.
PII-like values are redacted before log records are written. Configure logging
from the pytest command line:

```bash
.venv/bin/python -m pytest tests/e2e_python \
--agt-e2e-log-model-io=summary \
--agt-e2e-log-format=pretty \
--agt-e2e-log-text-limit=1000 \
--agt-e2e-log-live=on \
--agt-e2e-log-level=INFO -q
```

Set `--agt-e2e-log-model-io=off` to suppress model request/response logs, or
`--agt-e2e-log-model-io=full` to disable truncation while keeping redaction. Set
`--agt-e2e-log-format=compact` for single-line JSON logs, or
`--agt-e2e-log-live=off` to keep the pytest output quiet.

## Artifacts

Set `AGT_E2E_ARTIFACT_DIR` to preserve JSON results:

```bash
AGT_E2E_ARTIFACT_DIR=artifacts/e2e-python/ollama \
.venv/bin/python -m pytest tests/e2e_python -q
```

The suite writes `adtech.json`, `filesystem-escape.json`, `healthcare.json`,
`pii-redaction.json`, and `prompt-injection.json`. CI also writes `junit.xml`
and `runtime-metadata.json`, then uploads the directory as the
`e2e-python-ollama-artifacts` artifact.
Loading
Loading