Skip to main content
Glama
shima78

AgentShield

by shima78
README.md
# AgentShield

**A policy-driven decision engine for AI agents and AI-native applications.**

AgentShield decides whether an action an agent wants to take should be
**allowed**, sent for **review**, or **denied** — deterministically, based on
rules you define. **AgentShield decides; the agent/application executes.**
It does not need to sit between an agent and a tool, an MCP server, or
anything else — it only needs to be consulted before the action happens:

```text
AI Agent
   |
   | evaluate action
   v
AgentShield
   |
   | ALLOW / REVIEW / DENY
   v
AI Agent
   |
   | execute if permitted
   v
Tool / MCP / API / Action
```

```python
decision = shield.evaluate(request)
```

> **Scope note:** in this phase, AgentShield is a *decision engine*, not an
> enforcement mechanism. It cannot, by itself, technically stop a malicious
> or misbehaving agent from ignoring its answer and executing the action
> anyway — that requires actually sitting in the execution path (as the MCP
> Gateway/server below optionally does) or another enforcement integration.
> Making that guarantee robust across execution paths is future work.

## Why it exists

AI agents increasingly want to take actions with real-world side effects:
merging pull requests, deleting resources, reading secrets, moving money.
Provider SDKs and tool protocols like MCP give you the *mechanism* to take
these actions, but not a consistent, auditable way to decide *whether a
given action should be allowed*. AgentShield is that decision layer —
independent of any specific LLM provider, agent framework, or tool
protocol.

## Two ways to use it

* **Python SDK / Library (primary)** — embed the Core decision engine
  directly in your own agent/application code and consult it before
  executing an action. See [`examples/sdk_example.py`](examples/sdk_example.py)
  and "Minimal example" below. No MCP dependency required.
* **MCP adapter (optional integration)** — `agentshield.mcp` is one
  integration built on top of the same Core: it places AgentShield in the
  execution path between an MCP client and a downstream MCP server, so it
  can also *enforce* (not just advise on) ALLOW/REVIEW/DENY for that path.
  See "MCP adapter" below. This is not what AgentShield *is* — it's one way
  to use it.

## Architecture

```text
                    AgentShield
                         │
                  ┌──────┴──────┐
                  │     Core     │   ← the decision engine
                  │              │
                  │ Policy       │
                  │ Decision     │
                  │ Risk         │
                  │ Audit        │
                  │ Providers    │
                  └──────┬───────┘
                         │
              ┌──────────┴──────────┐
              ▼                     ▼
         Python SDK            MCP adapter      ← agentshield.mcp (optional)
              │                     │
         Developers          AI Agents / MCP
```

The **Core** (`agentshield.decision`, `.policy`, `.engine`, `.risk`,
`.audit`) is a small, dependency-light Python library. It is deterministic:
it never calls an external service, never calls an LLM, and never executes
the action it decides on — it only returns a `Decision`. Given a policy and
a request, it always returns the same decision. The Core has **no
dependency on MCP** (see `tests/test_core_independence.py`) and remains
fully usable on its own.

```text
Core
  ^
  |
MCP adapter (agentshield.mcp)
```

The dependency direction is one-way: the Core has no idea MCP, or anything
else that might call it, exists.

### MCP is one adapter among others

`agentshield.mcp` (documented in full below) is an **optional** integration
that happens to place AgentShield in the execution path for MCP traffic
specifically, so it can enforce rather than just advise for that path:

```text
AI Agent / MCP Client
        |
        | MCP tool call
        v
+----------------------+
|   AgentShield        |
|    MCP adapter       |
+----------+-----------+
           |
           v
    DecisionEngine
           |
    +------+------+
    |             |
  ALLOW        REVIEW / DENY
    |             |
    v             +-----> approval / block
Downstream MCP
   Server
```

AgentShield works with existing MCP servers without requiring those servers
to be modified, and preserves their tool definitions and arguments as-is —
it is a policy layer, not a schema transformation layer. But this is one
possible caller of the Core, not a requirement: the same `DecisionEngine`
works identically for an HTTP API call, a shell command, a workflow step,
or anything else expressed as a `DecisionRequest`.

## Minimal example (Core only, no MCP)

```python
from agentshield import DecisionEngine, DecisionRequest, Outcome, Policy

policy = Policy.from_yaml("examples/policy.yaml")
shield = DecisionEngine(policy)

decision = shield.evaluate(
    DecisionRequest(
        action="merge_pull_request",
        actor="agent",
        server="github",
        arguments={"repo": "acme/app", "pull_request": 42},
        context={"environment": "production"},
    )
)

if decision.outcome == Outcome.DENY:
    ...  # the agent must not execute the action
elif decision.outcome == Outcome.REVIEW:
    ...  # the agent must route this through its own approval flow first
else:
    ...  # the agent may proceed to actually perform the action, however
         # it chooses to (MCP, a direct API call, a CLI, ...)

print(decision.outcome)   # Outcome.REVIEW
print(decision.allowed)   # False
print(decision.reason)    # "Production merges require human approval."
print(decision.rule)      # "production-merge"
```

See [`examples/sdk_example.py`](examples/sdk_example.py) for a runnable
version of this.

> **Breaking rename:** what was `AuthorizationEngine`/`AuthorizationRequest`
> is now `DecisionEngine`/`DecisionRequest`, reflecting that this is a
> general-purpose decision engine, not an MCP-specific authorization layer.
> `DecisionRequest`'s tool/action field was renamed from `tool` to `action`.
> The old class names remain importable as deprecated aliases
> (`AuthorizationEngine is DecisionEngine`, etc.) so `from agentshield import
> AuthorizationEngine` still works, but any code constructing the request
> with `tool=...` must change to `action=...`. `PolicyRule.tool` (the policy
> YAML field) is **unchanged** — existing policy files keep working as-is.

## Using AgentShield from an Agent

The pattern above is the whole integration surface: a Python agent (with
or without a framework) calls `shield.evaluate(request)` directly, as a
decision service/library, **before** doing anything else. AgentShield is
not an execution proxy — it never sits between the agent and whatever it
would eventually call (a tool, an MCP server, a deploy script). It only
needs to be asked first:

```python
decision = shield.evaluate(request)

if decision.outcome == Outcome.DENY:
    stop()
elif decision.outcome == Outcome.REVIEW:
    review()
else:
    execute()
```

See [`examples/agent_demo.py`](examples/agent_demo.py) for a complete,
runnable version of this, as three small scenarios against one policy.
**Policy knows the rules. Jev evaluates the situation:**

1. **Deterministic DENY** — `delete_database` in production. Policy alone
   settles this; Jev is never even consulted, and could not override it
   if it were.
2. **Policy ALLOW, Jev recommends REVIEW** — a staging deploy, which
   policy permits outright — but the situation (a large database
   migration, late on a Friday) is one a semantic evaluator can flag as
   worth a second look, even though nothing technically forbids it.
3. **Policy ALLOW, Jev agrees** — the same staging deploy rule, but a
   small, routine, Tuesday-morning change: nothing here should raise a
   flag, and (with real Jev configured) nothing does.

```bash
python examples/agent_demo.py                       # deterministic policy only
pip install -e ".[jev]"
TYPESAFE_API_KEY=... python examples/agent_demo.py   # + real Jev semantic evaluation
```

Without `TYPESAFE_API_KEY` set, every scenario prints `With Jev: not
evaluated (...)` and falls back to the deterministic-only result — the
demo never fabricates a semantic verdict. This is a decision, not an
enforcement mechanism or a security guarantee: the simulated
`🚀 ... executed.` line only ever prints when the returned `Decision`
actually permits it, but AgentShield itself never executes anything —
the agent remains responsible for that.

## Using AgentShield with an AI Agent

**The Agent decides what it wants to do. AgentShield evaluates that
proposed action before execution.** AgentShield does not replace the
Agent's planning or decision-making — it provides a decision boundary
between an Agent's proposed action and its execution.

```text
User
  |
  v
AI Agent
  |
  | proposes action
  v
AgentShield
  |-- Policy
  +-- Jev
  |
  v
ALLOW / REVIEW / DENY
  |
  v
AI Agent
  |
  v
Tool / API / Action
```

* The Agent chooses the action — from a natural-language request, using
  an LLM (or the deterministic stand-in below), independently, with no
  hard-coded mapping from request to action.
* Policy enforces deterministic constraints.
* Jev evaluates semantic suitability (only when policy alone would ALLOW).
* The Agent executes only after ALLOW.

**AgentShield is not an execution proxy.** It does not need to sit between
the Agent and a tool — nothing here talks MCP, and MCP is not required for
this integration at all (it remains one *optional* adapter among others,
see above).

### Provider-agnostic: the Agent never touches an SDK

The LLM step is behind a small, swappable interface, not hard-coded to
OpenAI — the dependency direction is:

```text
LLM Provider
    |
    | proposes
    v
  Agent
    |
    | asks for a decision
    v
AgentShield
    |
    | ALLOW / REVIEW / DENY
    v
  Agent
    |
    | executes only after ALLOW
    v
  Action
```

`agentshield.agent` defines the provider-neutral vocabulary — `ProposedAction`
(`action`/`arguments`/`context`, adapted directly onto the existing
`DecisionRequest` with `build_decision_request()`, no parallel abstraction)
and the `AgentProvider` interface (`propose_action(user_request) ->
ProposedAction`). It has no dependency on any specific provider SDK.
Three implementations ship: `agentshield.providers.anthropic.AnthropicProvider`
(Claude Messages API, native structured output), `agentshield.providers.openai.OpenAIProvider`
(chat completions, JSON mode), and `agentshield.providers.gemini.GeminiProvider`
(Gemini structured JSON output). Each alone creates its own client, reads
its own API key (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY`),
and converts the response; the agent loop never imports
`anthropic`/`openai`/`google.genai`, never sees an API key, and never sees
a provider response object. **The provider does not know about
AgentShield. AgentShield does not know about Anthropic, OpenAI, or
Gemini. The agent connects the two.**

If a configured provider's call fails, it raises `ProviderError` — the
agent does not silently fall back to a different provider; it fails
safely and does not execute.

[`examples/real_agent_demo.py`](examples/real_agent_demo.py) is a complete,
runnable version of this, built around a small `Agent` class that connects
a provider's proposal to AgentShield's decision to execution:

```python
class Agent:
    def handle(self, user_request):
        proposal = self.provider.propose_action(user_request)   # Agent decides
        request = build_decision_request(proposal)
        decision = self.shield.evaluate(request)                 # AgentShield evaluates
        if decision.outcome == Outcome.ALLOW:
            self._execute(proposal)                               # only after ALLOW
```

It gives the Agent three plain natural-language requests (e.g. *"Delete
the production database."*) — nothing in the demo states what action each
one means; the Agent (via a real LLM provider, or `DeterministicDemoProvider`
below when no key is set) independently derives the structured action,
arguments, and context each time, and AgentShield's policy/Jev then
determine what happens. Execution goes through a tiny local tool registry
(`deploy`, `delete_database`, `search_repository`) that only prints what
it would have done — no MCP, no real side effects. `DeterministicDemoProvider` —
implementing the exact same `AgentProvider` interface a real provider
does — stands in for the LLM step when no key is set, clearly labeled and
never pretending to be a real LLM call; it derives the action from facts
actually present in the request text, the same way a real provider would,
just without general language understanding. When multiple keys are set,
Anthropic is tried first, then OpenAI, then Gemini.

```bash
pip install -e ".[agent-demo,jev]"
export ANTHROPIC_API_KEY=...   # tried first; never committed, never printed
export OPENAI_API_KEY=...      # tried next; never committed, never printed
export GEMINI_API_KEY=...      # tried last; never committed, never printed
export TYPESAFE_API_KEY=...    # for semantic evaluation; never committed, never printed
python examples/real_agent_demo.py
```

All are optional and independent: without a provider key, the
deterministic provider is used instead; without `TYPESAFE_API_KEY`,
semantic evaluation is skipped (also clearly labeled) and the
deterministic-only decision is used.

## Example policy

```yaml
rules:
  - name: block-secret-access
    tool: "*.read_secret"
    outcome: deny
    risk: critical
    reason: "Access to secrets is blocked."

  - name: production-merge
    server: github
    tool: merge_pull_request
    context:
      environment: production
    outcome: review
    risk: high
    reason: "Production merges require human approval."
```

Rules support `actor`, `server`, `tool`, and `context` as optional matching
constraints (all must be unset or match for a rule to apply). `name`,
`outcome`, and `risk` are required on every rule; `reason` is optional. Tool
names support `*`/`?` wildcards (e.g. `"*.delete_*"`); `actor` and `server`
currently require exact matches.

See [`examples/policy.yaml`](examples/policy.yaml) for a fuller example, and
`tests/test_engine.py` for the precedence rules worked out in detail.

## ALLOW / REVIEW / DENY semantics

| Outcome | `allowed` | Meaning |
|---|---|---|
| `ALLOW`  | `True`  | The action may proceed. |
| `REVIEW` | `False` | The action must not proceed automatically; it needs human (or other) approval. |
| `DENY`   | `False` | The action must not proceed, period. |

If no rule in the policy matches a request, the Core defaults to `ALLOW`
with `risk = LOW` and `confidence = 1.0`. This default is intentionally easy
to change in a future phase (e.g. a "default deny" mode) but is not
configurable yet.

### Rule precedence

When multiple rules match a request, the Core picks exactly one, using this
deterministic order (implemented in `engine.py`):

1. **Exact tool match** beats **wildcard tool match** beats **no tool
   constraint**.
2. Within the same tier, the rule with **more context constraints** wins.
3. Within a further tie, the rule with **more specific `actor`/`server`
   constraints** wins.
4. Within a full tie, the **earlier rule** in the policy file wins.

This ordering depends only on each rule's own fields and its position in the
policy — never on dictionary iteration order — so the same policy and
request always produce the same decision.

### Deterministic DENY is authoritative

**A deterministic `DENY` must never be overridden by another layer.** An
optional semantic evaluator (see "Semantic evaluation" below) may add
context or additional review — but it cannot flip a policy-level `DENY`
into an `ALLOW`, and `DecisionEngine` never even consults it once policy
has already said DENY. This holds both in the Core (the engine's
precedence rules operate purely over policy rules, and nothing exposes a
way to override a returned `Decision`) and in the MCP Gateway (a DENY is
never sent to an `ApprovalProvider` and never reaches the downstream
server). Any layer that wants to add a "second opinion" must be additive
(e.g. escalating `ALLOW` to `REVIEW`), never permissive.

## Semantic evaluation (optional, via Jev)

Deterministic policy answers "is this technically permitted?" `DecisionEngine`
can optionally also ask a **semantic** question: "given the action, its
arguments, the current context, and the applicable policy, is this
actually a *sensible* decision?" — a technically-permitted action can
still be a bad idea (a database migration proposed for Friday evening in
production, say).

```text
Action + Context + Applicable Policy
                ↓
              Jev
                ↓
      semantic assessment
                ↓
          AgentShield
```

`agentshield.jev.JevSemanticEvaluator` implements this using
[Jev](https://docs.typesafe.ai), TypeSafe's "System One" model, via the
official `typesafe-sdk` package (optional dependency: `pip install -e ".[jev]"`,
`export TYPESAFE_API_KEY=...`). It asks a single structured `Choice`
question (`good` / `review` / `bad`) with only the *relevant* slice of
policy for that one request as context — never the whole policy file.

```python
from agentshield import DecisionEngine, DecisionRequest, Policy
from agentshield.jev import JevSemanticEvaluator

shield = DecisionEngine(Policy.from_dict({"rules": []}), semantic_evaluator=JevSemanticEvaluator())

decision = shield.evaluate(
    DecisionRequest(
        action="deploy",
        actor="release-agent",
        context={
            "environment": "production",
            "time": "friday_evening",
            "database_migration": True,
        },
    )
)
```

**Deterministic policy remains authoritative, and the semantic evaluator
is only ever consulted for an `ALLOW`:**

```text
DENY   -> final; Jev is never consulted
REVIEW -> final; Jev is never consulted (policy already asked for human
          attention -- there is nothing more for semantic judgment to add)
ALLOW  -> Jev is consulted, and may escalate to REVIEW -- never to DENY
```

`Decision.confidence` is set from Jev's own confidence score when a
semantic evaluation ran (all other decisions keep `confidence = 1.0`, as
before).

This is entirely optional: `agentshield.jev` is never imported by the Core
or by `DecisionEngine` itself (see `tests/test_core_independence.py`), and
Jev is not an enforcement mechanism or a security guarantee — it is one
more input into a decision the agent/application is still responsible for
acting on. See [`examples/jev_example.py`](examples/jev_example.py).

## MCP adapter

An **optional integration**, not the definition of AgentShield: `agentshield.mcp`
puts a policy-enforcement proxy between an MCP client (an agent) and a
downstream MCP server launched locally over stdio, using the official
[MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk).

```python
import asyncio
from agentshield.mcp import GatewayConfig, MCPGateway

async def main():
    config = GatewayConfig.from_yaml("examples/gateway.yaml")
    gateway = MCPGateway.from_config(config)

    async with gateway:
        tools = await gateway.list_tools()          # unmodified downstream tool defs
        result = await gateway.call_tool("echo", {"message": "hi"})
        print(result.decision.outcome, result.executed)

asyncio.run(main())
```

Install the optional MCP dependency first: `pip install -e ".[mcp]"`.

### Configuration

```yaml
server:
  name: demo

downstream:
  command: python
  args:
    - examples/mcp_server.py

context:
  environment: production

policy:
  path: examples/mcp_policy.yaml

actor: agent
```

`downstream` describes how to launch the downstream MCP server as a local
subprocess. `context` is static context merged into every
`DecisionRequest` built by this gateway (so a rule like `context:
{environment: production}` works through the gateway exactly as it does in
the Core). `actor` defaults to `"agent"`.

### Request mapping

Every intercepted `tools/call` is mapped onto the Core's generic
`DecisionRequest` like this — this is the adapter's one job, translating
MCP's vocabulary into the Core's generic vocabulary:

| MCP call | `DecisionRequest` field |
|---|---|
| configured `actor` (default `"agent"`) | `actor` |
| configured `server.name` | `server` |
| MCP tool name | `action` |
| MCP tool arguments, unmodified | `arguments` |
| configured static `context` | `context` |

### ALLOW / REVIEW / DENY at the gateway

* **ALLOW** — the call is forwarded to the downstream server unmodified;
  its result is returned to the caller exactly as the downstream server
  produced it — **including** a downstream-reported error
  (`CallToolResult.is_error=True`, e.g. "file not found"). That is a normal
  MCP result, not a gateway failure, so it is never turned into a Python
  exception; only a call that could not be carried out at all (a broken
  connection, a malformed protocol response) raises `DownstreamToolError`.
* **DENY** — the downstream server is **never called**. The gateway returns
  a `GatewayCallResult` with `executed=False` and the decision (rule, risk,
  reason) attached — no arguments or internal details are leaked back.
* **REVIEW** — the call is **not** executed automatically. It is resolved
  through an `ApprovalProvider`:

  ```python
  from agentshield.mcp import ApprovalProvider, ApprovalResult

  class MyApprovalProvider(ApprovalProvider):
      async def request_approval(self, request, decision) -> ApprovalResult:
          ...  # ask Slack / a web UI / a human, then:
          return ApprovalResult(approved=True, approver="alice")
  ```

  Two minimal implementations ship out of the box: `CallbackApprovalProvider`
  (wraps a sync or async callable — the main building block for tests and
  programmatic integrations) and `ConsoleApprovalProvider` (prompts a human
  at the terminal; demo use only). If a REVIEW decision is reached and no
  `ApprovalProvider` is configured, the gateway fails closed and raises
  `ApprovalProviderRequiredError` — it never guesses.

### Audit

The gateway reuses the Core's `AuditLog` — every intercepted call, of every
outcome, produces one `AuditEvent`. Two optional, backward-compatible fields
were added to `AuditEvent` for the gateway's use: `approval_required` and
`approval_outcome` (`"approved"` / `"rejected"` / `None`). Core-only usage is
unaffected; these default to `False` / `None`.

### Error handling

`agentshield.mcp` defines its own exception hierarchy (`GatewayError` and
subclasses) for gateway/transport failures, distinct from Core errors like
`PolicyError`: `DownstreamConnectionError`, `DownstreamToolError`,
`AuthorizationEvaluationError` (the engine itself failed — fails closed,
never forwards), `ApprovalProviderRequiredError`, `ApprovalProviderError`,
`GatewayConfigError`. Downstream errors are never swallowed.

## Running AgentShield as a real MCP server

`agentshield.mcp.server` is a thin adapter that exposes an `MCPGateway` as a
real, upstream-facing MCP server over stdio, using the official MCP SDK's
`Server`/`stdio_server`. This is the case where the MCP adapter actually
sits in the execution path and can enforce, not just advise. It contains
**no decision logic of its own** — every `tools/call` is routed straight
through the same `MCPGateway.call_tool()` used by the library form above:

```text
MCP Client / AI Agent
         |
         | MCP / stdio
         v
+----------------------+
|     AgentShield      |
|      MCP Server      |   agentshield.mcp.server — protocol only
+----------+-----------+
           |
           v
      MCPGateway          agentshield.mcp.gateway — MCP adapter
           |
           v
  DecisionEngine           agentshield Core — MCP-agnostic
           |
    +------+------+------+
    |             |      |
  ALLOW         REVIEW  DENY
    |             |      |
    |          approval STOP
    |             |
    +-------------+
           |
           v
  DownstreamMCPProxy -> Downstream MCP Server
```

Start it:

```bash
pip install -e ".[mcp]"
python -m agentshield.mcp.server --config examples/gateway.yaml
```

MCP protocol traffic uses stdout; all diagnostics go to stderr via the
standard `logging` module, so stdout stays clean for the protocol. The
downstream connection is established once at startup and kept alive for the
life of the upstream session; it is always closed on shutdown, including on
error, so no subprocess is leaked.

**No `ApprovalProvider` is wired up by this CLI launcher.** stdin/stdout in
this process are owned by the MCP protocol stream, so the interactive
`ConsoleApprovalProvider` cannot be used here — a REVIEW decision therefore
fails closed (the client gets a clear "no approval provider configured"
result, and nothing is forwarded downstream). To approve REVIEW calls,
embed `AgentShieldMCPServer` directly and pass a `CallbackApprovalProvider`
backed by Slack, a web UI, a queue, etc. — see
[`tests/test_mcp_server.py`](tests/test_mcp_server.py) for a worked example.

### Using it from Claude Desktop / Cursor / other MCP clients

Add it to the client's MCP server configuration, e.g. Claude Desktop's
`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "agentshield": {
      "command": "python",
      "args": [
        "-m", "agentshield.mcp.server",
        "--config", "/absolute/path/to/examples/gateway.yaml"
      ]
    }
  }
}
```

The client then sees exactly the downstream server's tools (names,
descriptions, input schemas, unmodified) and every call it makes is
authorized by policy before AgentShield forwards it.

### ALLOW / REVIEW / DENY through the real server

Against [`examples/mcp_policy.yaml`](examples/mcp_policy.yaml):

```text
echo(...)          -> ALLOW  -> forwarded; downstream result returned as-is
create_file(...)   -> REVIEW -> blocked (no approval provider configured)
delete_file(...)   -> DENY   -> blocked; downstream never called
```

## Local demos

```bash
# Core only, no MCP: the primary usage pattern.
python examples/sdk_example.py

# A tiny "agent" that consults AgentShield before acting, across three
# scenarios (deterministic DENY, policy-ALLOW-but-Jev-says-REVIEW,
# policy-ALLOW-and-Jev-agrees) -- see "Using AgentShield from an Agent" above.
python examples/agent_demo.py

# Same, with real Jev semantic evaluation added on top.
pip install -e ".[jev]"
TYPESAFE_API_KEY=... python examples/agent_demo.py

# Real agent autonomy: the Agent itself derives the action from plain
# natural-language requests (via a real LLM, or a deterministic stand-in
# without one) -- nothing here hard-codes which action each request means.
pip install -e ".[agent-demo,jev]"
python examples/real_agent_demo.py
ANTHROPIC_API_KEY=... TYPESAFE_API_KEY=... python examples/real_agent_demo.py
```

The two demos below need the optional MCP extra:
[`examples/mcp_server.py`](examples/mcp_server.py) is a tiny fake MCP server
with three harmless tools (`echo`, `create_file`, `delete_file`), confined to
a local sandbox directory. [`examples/mcp_policy.yaml`](examples/mcp_policy.yaml)
allows `echo`, requires review for `create_file`, and denies `delete_file`.
All demos are entirely local — no network access, no API keys, no external
services.

```bash
pip install -e ".[mcp]"

# MCP library form: embeds MCPGateway directly in this process.
python examples/run_demo.py

# MCP real server form: a real MCP client connects to
# `python -m agentshield.mcp.server` as a subprocess, exactly like Claude
# Desktop or Cursor would.
python examples/run_demo_server.py
```

Both MCP demos run `MCP Client -> AgentShield -> Fake MCP Server` end to end
and demonstrate ALLOW, REVIEW, and DENY plus (for `run_demo.py`) the
resulting audit trail.

## Real MCP Integration

The MCP demos above are proven against a small fake local MCP server.
[`examples/github/`](examples/github/) proves the exact same, unmodified
MCP adapter against a **real** MCP ecosystem server:
[github/github-mcp-server](https://github.com/github/github-mcp-server),
GitHub's own official MCP server.

```text
Claude
  ↓
AgentShield
  ↓
GitHub MCP
  ↓
GitHub
```

Why this matters: AgentShield can sit in front of an **existing, unmodified
MCP server** — this isn't limited to a purpose-built demo server. The same
`MCPGateway`/`AgentShieldMCPServer` that talk to `examples/mcp_server.py`
talk to real GitHub MCP with zero code changes; only the config and policy
differ. Nothing GitHub-specific was added to the Core, `MCPGateway`, or
`AgentShieldMCPServer` — GitHub-specific detail lives only in
[`examples/github/`](examples/github/) and
[`tests/test_github_integration.py`](tests/test_github_integration.py). The
same gateway works unchanged in front of ComfyUI MCP, AWS MCP, or any other
MCP server.

**Security model:** AgentShield is positioned between the agent and the
tool layer. It does not replace MCP, and it does not modify the downstream
MCP server — it evaluates the action *before* execution:

```text
Agent
  ↓
AgentShield
  ↓
Policy decision
  ↓
Tool execution
```

So `DENY` = the tool is never executed, whether that tool is a local fake
server or the real GitHub API.

See **[`examples/github/README.md`](examples/github/README.md)** for full,
step-by-step setup: prerequisites, the Python 3.11+ requirement,
installation, GitHub MCP setup (Docker), the required
`GITHUB_PERSONAL_ACCESS_TOKEN` environment variable (never committed —
referenced from config as `${GITHUB_PERSONAL_ACCESS_TOKEN}` and resolved
only at connect time), the example policy and gateway config, an MCP client
configuration example, and the local-test vs. integration-test commands.

Quick summary:

```bash
pip install -e ".[mcp]"
export GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here   # never committed
python -m agentshield.mcp.server --config examples/github/gateway.yaml
```

The normal test suite stays completely offline and credential-free:

```bash
pytest                      # integration tests skip themselves automatically
pytest -m "not integration" # same, explicit
```

Real integration tests (require Docker + a token):

```bash
pytest -m integration
```

## What's implemented

* **Core — decision engine**: `DecisionEngine`/`DecisionRequest`, typed
  decision/policy models, deterministic matching and precedence, a
  default-allow fallback, and an in-memory audit log. This is the primary,
  MCP-agnostic public API (`shield.evaluate(request)`).
* **MCP adapter (library)**: `MCPGateway`, a policy-enforcement proxy for a
  downstream MCP server reached over stdio, using the official MCP SDK;
  tool discovery; ALLOW/REVIEW/DENY enforcement; a pluggable approval
  abstraction; audit logging.
* **MCP adapter (real server)**: `agentshield.mcp.server` exposes
  `MCPGateway` as an actual upstream-facing MCP server over stdio (a thin
  protocol adapter with no decision logic of its own), launchable via
  `python -m agentshield.mcp.server --config ...` and usable directly from
  Claude Desktop, Cursor, or any other MCP-compatible client.
* **Real MCP integration**: the same MCP adapter proven against a real MCP
  ecosystem server (GitHub MCP) rather than only the bundled fake one, with
  an opt-in, credential-gated integration test suite.
* **Optional semantic evaluation**: `DecisionEngine(..., semantic_evaluator=...)`
  and `agentshield.jev.JevSemanticEvaluator`, a single `good`/`review`/`bad`
  judgment on top of deterministic policy, backed by the real Jev API. The
  Core has no dependency on it either.

Deliberately **not** implemented yet: automatic agent interception, a
general CLI, an HTTP authorization service or remote AgentShield service, a
database, a web dashboard, authentication infrastructure, multiple semantic
questions/evaluators/models, a semantic-orchestration or confidence-threshold
framework, or configurable approval backends (Slack, webhook, web UI, secret
management). A full enforcement/proxy redesign beyond the existing MCP
adapter is future work — see the scope note at the top of this README.

## Roadmap

```text
Core decision engine (current)
→ MCP adapter — library + real server (this repo)
→ Real MCP integration — GitHub (this repo)
→ Optional semantic evaluation — Jev (this repo)
→ Python SDK packaging
→ stronger enforcement integrations
→ TypeScript SDK
→ further integrations
```

## Installing and running tests

Requires **Python 3.11+** (`requires-python = ">=3.11"`).

```bash
pip install -e ".[dev]"       # includes the optional mcp and jev extras
pytest                        # offline, credential-free
pytest -m integration         # optional: real GitHub MCP + real Jev API tests
```

## Status

AgentShield is an early-stage, open-source project. It does not make any
production security guarantees; treat it as policy infrastructure you
integrate and test against your own threat model.