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
1 change: 1 addition & 0 deletions python/examples/_shared/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Shared helpers for Python examples."""
61 changes: 61 additions & 0 deletions python/examples/_shared/litellm_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Tiny shared helper for LiteLLM live-validation request kwargs."""

from __future__ import annotations

from typing import Literal, TypedDict


class ProviderConfigLike(TypedDict):
mode: Literal["openai", "ollama", "openai_compatible"]
source: str
base_url: str
model: str
api_key: str | None


class LiteLLMProviderKwargs(TypedDict, total=False):
model: str
api_base: str
api_key: str
temperature: float
drop_params: bool


def build_litellm_provider_kwargs(
config: ProviderConfigLike,
) -> LiteLLMProviderKwargs:
"""Return LiteLLM-safe provider/model kwargs for live validations."""

model = _normalize_litellm_model_id(config)
base_url = _config_value(config, "base_url")
if base_url is None:
raise RuntimeError("Provider config missing base_url.")

kwargs: LiteLLMProviderKwargs = {
"model": model,
"api_base": base_url,
"drop_params": True,
}
api_key = _config_value(config, "api_key")
if api_key:
kwargs["api_key"] = api_key
kwargs["temperature"] = 0
return kwargs


def _normalize_litellm_model_id(config: ProviderConfigLike) -> str:
mode = _config_value(config, "mode")
model = _config_value(config, "model")
if model is None:
raise RuntimeError("Provider config missing model.")
if mode == "ollama" and "/" not in model:
return f"ollama/{model}"
return model


def _config_value(config: object, key: str) -> str | None:
if isinstance(config, dict):
value = config.get(key)
else:
value = getattr(config, key, None)
return value if isinstance(value, str) or value is None else str(value)
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from importlib import import_module
from typing import cast

from python.examples._shared.litellm_request import build_litellm_provider_kwargs

try:
from host_support import print_startup_config, resolve_provider_config
except ImportError:
Expand Down Expand Up @@ -80,13 +82,9 @@ def get_model_approval_claim(expense_summary: str) -> ModelApproval:
f"{expense_summary}"
)
kwargs: dict[str, object] = {
"model": config.model,
**build_litellm_provider_kwargs(config),
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"api_base": config.base_url,
}
if config.api_key:
kwargs["api_key"] = config.api_key

response = completion(**kwargs)
message = _extract_response_content(response)
Expand Down
4 changes: 4 additions & 0 deletions python/examples/tool_gating/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ prohibit calendar_admin

the MCP tool is absent from the exposed tool set and blocked if invoked
directly.

This example also includes an opt-in live-model comparison showing that a real
tool-calling model can only act through the MCP tool surface the host exposes
from authoritative Context Compiler state.
68 changes: 68 additions & 0 deletions python/examples/tool_gating/mcp_calendar_admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,71 @@ prohibit calendar_admin
If a caller still invokes the hidden tool directly, the host blocks execution.

Adversarial request text does not expose the tool or mutate policy state.

The provider-free tests are the canonical proof for this example.

This example also includes an opt-in live-model comparison that shows the same
admin-calendar intent reaches different outcomes because authoritative Context
Compiler state changes the model-visible tool surface.

## Live-model comparison

The live-model path keeps the same host-owned MCP tool registry and execution
rules.

The host exposes only the MCP tools allowed by authoritative state, then asks a
real tool-calling model to complete the same admin action.

The model can only use the tools the host exposes.

This is not prompt reinjection:

- the host does not derive authority from model output
- the model does not create or mutate authoritative state
- Context Compiler state determines whether the protected admin tool is visible
and executable

### Same request, different state

User intent:

```text
Create an admin calendar event named Quarterly access review on calendar ops-admin.
```

Outcome matrix:

- absent state: protected tool is not exposed, and no protected side effect
occurs
- `use calendar_admin`: protected tool is exposed; if the model selects it, the
host executes it and writes one side effect
- contradiction with `prohibit calendar_admin`: Context Compiler returns
clarify/conflict and blocks protected execution before tool execution

### Validation

Canonical provider-free tests:

```bash
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_tool_gating_example.py
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_live_model_helper.py
```

Opt-in live-model validation:

```bash
export RUN_MCP_CALENDAR_ADMIN_LIVE_MODEL=1
export MODEL=openai/gpt-4o-mini
export OPENAI_API_KEY=...
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_live_model.py
```

Optional compatible-provider settings:

- `PROVIDER`
- `OPENAI_BASE_URL`

For local Ollama, this example accepts either:

- `PROVIDER=ollama` with a bare `MODEL` such as `qwen2.5:1.5b-instruct`
- or an explicit LiteLLM-style `MODEL=ollama/qwen2.5:1.5b-instruct`
Loading