context-compiler-example-integrations 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- context_compiler_example_integrations/README.md +71 -0
- context_compiler_example_integrations/__init__.py +1 -0
- context_compiler_example_integrations/examples/__init__.py +12 -0
- context_compiler_example_integrations/examples/_shared/__init__.py +1 -0
- context_compiler_example_integrations/examples/_shared/litellm_request.py +61 -0
- context_compiler_example_integrations/examples/_shared/provider_mode.py +91 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/README.md +77 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/example.py +169 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/README.md +85 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/app.py +205 -0
- context_compiler_example_integrations/examples/execution_authorization/README.md +31 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md +77 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py +168 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/README.md +97 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/app.py +312 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/model_approval.py +98 -0
- context_compiler_example_integrations/examples/gateway_middleware/README.md +49 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md +40 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/__init__.py +1 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/example.py +220 -0
- context_compiler_example_integrations/examples/prompt_construction/README.md +27 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/README.md +197 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/basic.py +392 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/confirmation_helper.py +91 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/with_directive_drafter.py +524 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/README.md +92 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/__init__.py +1 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/example.py +205 -0
- context_compiler_example_integrations/examples/retrieval_filtering/README.md +65 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/README.md +120 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/example.py +320 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/README.md +123 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/example.py +300 -0
- context_compiler_example_integrations/examples/schema_selection/README.md +41 -0
- context_compiler_example_integrations/examples/schema_selection/litellm_response_format/response_format.py +218 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/README.md +60 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/example.py +171 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/README.md +95 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/__init__.py +1 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/example.py +180 -0
- context_compiler_example_integrations/examples/tool_gating/README.md +55 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/README.md +34 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/example.py +195 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/README.md +93 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/example.py +250 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/live_model.py +369 -0
- context_compiler_example_integrations/reference_integrations/__init__.py +12 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/README.md +251 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py +146 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml +23 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook.py +163 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +289 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/README.md +233 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py +750 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +1045 -0
- context_compiler_example_integrations-0.1.0.dist-info/METADATA +110 -0
- context_compiler_example_integrations-0.1.0.dist-info/RECORD +65 -0
- context_compiler_example_integrations-0.1.0.dist-info/WHEEL +4 -0
- context_compiler_example_integrations-0.1.0.dist-info/licenses/LICENSE +173 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Host-side tool gating using authoritative Context Compiler state."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal, TypedDict, cast
|
|
5
|
+
|
|
6
|
+
from context_compiler import (
|
|
7
|
+
POLICY_PROHIBIT,
|
|
8
|
+
POLICY_USE,
|
|
9
|
+
State,
|
|
10
|
+
create_engine,
|
|
11
|
+
get_decision_state,
|
|
12
|
+
get_policy_items,
|
|
13
|
+
is_clarify,
|
|
14
|
+
)
|
|
15
|
+
from context_compiler.engine import Engine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CalendarToolCall(TypedDict):
|
|
19
|
+
tool_name: str
|
|
20
|
+
calendar_id: str
|
|
21
|
+
event_title: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolRegistrySnapshot(TypedDict):
|
|
25
|
+
available_tools: list[str]
|
|
26
|
+
hidden_tools: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CalendarToolExecutionResult(TypedDict):
|
|
30
|
+
authorization_state: Literal["allowed", "blocked"]
|
|
31
|
+
tool_visible: bool
|
|
32
|
+
executed: bool
|
|
33
|
+
blocked_reason: str | None
|
|
34
|
+
tool_result: str | None
|
|
35
|
+
registry_snapshot: ToolRegistrySnapshot
|
|
36
|
+
execution_log: list[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CalendarToolTurnResult(TypedDict):
|
|
40
|
+
decision_kind: Literal["clarify", "update", "passthrough"]
|
|
41
|
+
prompt_to_user: str | None
|
|
42
|
+
execution_result: CalendarToolExecutionResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decision_kind_name(
|
|
46
|
+
decision: object,
|
|
47
|
+
) -> Literal["clarify", "update", "passthrough"]:
|
|
48
|
+
if not isinstance(decision, dict):
|
|
49
|
+
raise ValueError("unexpected decision shape")
|
|
50
|
+
|
|
51
|
+
kind = decision.get("kind")
|
|
52
|
+
kind_name = getattr(kind, "value", None)
|
|
53
|
+
if kind_name not in {"clarify", "update", "passthrough"}:
|
|
54
|
+
raise ValueError(f"unexpected decision kind: {kind_name}")
|
|
55
|
+
|
|
56
|
+
return cast(Literal["clarify", "update", "passthrough"], kind_name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class CalendarAdminHost:
|
|
61
|
+
"""Host-owned tool registry and execution layer."""
|
|
62
|
+
|
|
63
|
+
execution_log: list[str]
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
self.execution_log = []
|
|
67
|
+
self._always_available_tools = ["calendar_view_events"]
|
|
68
|
+
self._calendar_admin_tools = ["calendar_admin_create_event"]
|
|
69
|
+
|
|
70
|
+
def visible_tools(self, state: State) -> ToolRegistrySnapshot:
|
|
71
|
+
available_tools = self._always_available_tools.copy()
|
|
72
|
+
hidden_tools = self._calendar_admin_tools.copy()
|
|
73
|
+
|
|
74
|
+
if calendar_admin_tools_are_allowed(state):
|
|
75
|
+
available_tools.extend(self._calendar_admin_tools)
|
|
76
|
+
hidden_tools = []
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"available_tools": available_tools,
|
|
80
|
+
"hidden_tools": hidden_tools,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def execute_calendar_admin_tool(self, tool_call: CalendarToolCall) -> str:
|
|
84
|
+
self.execution_log.append(
|
|
85
|
+
f"{tool_call['tool_name']}:{tool_call['calendar_id']}:{tool_call['event_title']}"
|
|
86
|
+
)
|
|
87
|
+
return (
|
|
88
|
+
f"created event '{tool_call['event_title']}' "
|
|
89
|
+
f"on calendar '{tool_call['calendar_id']}'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def calendar_admin_tools_are_allowed(state: State) -> bool:
|
|
94
|
+
"""Allow calendar admin tools only from authoritative compiler state."""
|
|
95
|
+
|
|
96
|
+
use_items = set(get_policy_items(state, POLICY_USE))
|
|
97
|
+
prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT))
|
|
98
|
+
|
|
99
|
+
if "calendar_admin" in prohibit_items:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return "calendar_admin" in use_items
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def execute_calendar_admin_tool_if_allowed(
|
|
106
|
+
tool_call: CalendarToolCall,
|
|
107
|
+
*,
|
|
108
|
+
state: State,
|
|
109
|
+
host: CalendarAdminHost,
|
|
110
|
+
) -> CalendarToolExecutionResult:
|
|
111
|
+
"""Hide or execute the admin tool based only on authoritative state."""
|
|
112
|
+
|
|
113
|
+
registry_snapshot = host.visible_tools(state)
|
|
114
|
+
tool_visible = tool_call["tool_name"] in registry_snapshot["available_tools"]
|
|
115
|
+
|
|
116
|
+
if not tool_visible:
|
|
117
|
+
return {
|
|
118
|
+
"authorization_state": "blocked",
|
|
119
|
+
"tool_visible": False,
|
|
120
|
+
"executed": False,
|
|
121
|
+
"blocked_reason": "calendar_admin state not authorized",
|
|
122
|
+
"tool_result": None,
|
|
123
|
+
"registry_snapshot": registry_snapshot,
|
|
124
|
+
"execution_log": host.execution_log.copy(),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
tool_result = host.execute_calendar_admin_tool(tool_call)
|
|
128
|
+
return {
|
|
129
|
+
"authorization_state": "allowed",
|
|
130
|
+
"tool_visible": True,
|
|
131
|
+
"executed": True,
|
|
132
|
+
"blocked_reason": None,
|
|
133
|
+
"tool_result": tool_result,
|
|
134
|
+
"registry_snapshot": registry_snapshot,
|
|
135
|
+
"execution_log": host.execution_log.copy(),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def handle_calendar_admin_turn(
|
|
140
|
+
engine: Engine,
|
|
141
|
+
*,
|
|
142
|
+
compiler_input: str,
|
|
143
|
+
tool_call: CalendarToolCall,
|
|
144
|
+
host: CalendarAdminHost,
|
|
145
|
+
) -> CalendarToolTurnResult:
|
|
146
|
+
"""Block tool exposure on clarify and otherwise enforce current state."""
|
|
147
|
+
|
|
148
|
+
decision = engine.step(compiler_input)
|
|
149
|
+
|
|
150
|
+
if is_clarify(decision):
|
|
151
|
+
return {
|
|
152
|
+
"decision_kind": "clarify",
|
|
153
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
154
|
+
"execution_result": {
|
|
155
|
+
"authorization_state": "blocked",
|
|
156
|
+
"tool_visible": False,
|
|
157
|
+
"executed": False,
|
|
158
|
+
"blocked_reason": "clarification required before exposing calendar admin tools",
|
|
159
|
+
"tool_result": None,
|
|
160
|
+
"registry_snapshot": host.visible_tools(engine.state),
|
|
161
|
+
"execution_log": host.execution_log.copy(),
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
authoritative_state = get_decision_state(decision)
|
|
166
|
+
if authoritative_state is None:
|
|
167
|
+
authoritative_state = engine.state
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
"decision_kind": _decision_kind_name(decision),
|
|
171
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
172
|
+
"execution_result": execute_calendar_admin_tool_if_allowed(
|
|
173
|
+
tool_call,
|
|
174
|
+
state=authoritative_state,
|
|
175
|
+
host=host,
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def run_demo() -> CalendarToolExecutionResult:
|
|
181
|
+
"""Run a deterministic demonstration with explicit authorization state."""
|
|
182
|
+
|
|
183
|
+
engine = create_engine()
|
|
184
|
+
engine.step("use calendar_admin")
|
|
185
|
+
host = CalendarAdminHost()
|
|
186
|
+
|
|
187
|
+
return execute_calendar_admin_tool_if_allowed(
|
|
188
|
+
{
|
|
189
|
+
"tool_name": "calendar_admin_create_event",
|
|
190
|
+
"calendar_id": "ops-admin",
|
|
191
|
+
"event_title": "Quarterly access review",
|
|
192
|
+
},
|
|
193
|
+
state=engine.state,
|
|
194
|
+
host=host,
|
|
195
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# `mcp_calendar_admin`
|
|
2
|
+
|
|
3
|
+
Authoritative state changes whether the host exposes and executes the calendar
|
|
4
|
+
admin MCP tool. This example shows tool gating with MCP as the integration
|
|
5
|
+
surface.
|
|
6
|
+
|
|
7
|
+
Context Compiler owns the authoritative policy state that decides whether the
|
|
8
|
+
host exposes the calendar admin MCP tool.
|
|
9
|
+
|
|
10
|
+
The host exposes and allows `calendar_admin_create_event` only when state
|
|
11
|
+
contains:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
use calendar_admin
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The host omits that tool from the exposed MCP tool set when state is absent or
|
|
18
|
+
when state contains:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
prohibit calendar_admin
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If a caller still invokes the hidden tool directly, the host blocks execution.
|
|
25
|
+
|
|
26
|
+
Adversarial request text does not expose the tool or mutate policy state.
|
|
27
|
+
|
|
28
|
+
The provider-free tests are the canonical proof for this example.
|
|
29
|
+
|
|
30
|
+
This example also includes an opt-in live-model comparison that shows the same
|
|
31
|
+
admin-calendar intent reaches different outcomes because authoritative Context
|
|
32
|
+
Compiler state changes the model-visible tool surface.
|
|
33
|
+
|
|
34
|
+
## Live-model comparison
|
|
35
|
+
|
|
36
|
+
The live-model path keeps the same host-owned MCP tool registry and execution
|
|
37
|
+
rules.
|
|
38
|
+
|
|
39
|
+
The host exposes only the MCP tools allowed by authoritative state, then asks a
|
|
40
|
+
real tool-calling model to complete the same admin action.
|
|
41
|
+
|
|
42
|
+
The model can only use the tools the host exposes.
|
|
43
|
+
|
|
44
|
+
This is not prompt reinjection:
|
|
45
|
+
|
|
46
|
+
- the host does not derive authority from model output
|
|
47
|
+
- the model does not create or mutate authoritative state
|
|
48
|
+
- Context Compiler state determines whether the protected admin tool is visible
|
|
49
|
+
and executable
|
|
50
|
+
|
|
51
|
+
### Same request, different state
|
|
52
|
+
|
|
53
|
+
User intent:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Create an admin calendar event named Quarterly access review on calendar ops-admin.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Outcome matrix:
|
|
60
|
+
|
|
61
|
+
- absent state: protected tool is not exposed, and no protected side effect
|
|
62
|
+
occurs
|
|
63
|
+
- `use calendar_admin`: protected tool is exposed; if the model selects it, the
|
|
64
|
+
host executes it and writes one side effect
|
|
65
|
+
- contradiction with `prohibit calendar_admin`: Context Compiler returns
|
|
66
|
+
clarify/conflict and blocks protected execution before tool execution
|
|
67
|
+
|
|
68
|
+
### Validation
|
|
69
|
+
|
|
70
|
+
Canonical provider-free tests:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_tool_gating_example.py
|
|
74
|
+
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_live_model_helper.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Opt-in live-model validation:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export RUN_MCP_CALENDAR_ADMIN_LIVE_MODEL=1
|
|
81
|
+
export MODEL=openai/gpt-4o-mini
|
|
82
|
+
export OPENAI_API_KEY=...
|
|
83
|
+
uv run --no-sync pytest python/tests/test_mcp_calendar_admin_live_model.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This live-model path uses the shared provider contract documented in
|
|
87
|
+
[PROVIDER_CONTRACT.md](../../../../PROVIDER_CONTRACT.md).
|
|
88
|
+
|
|
89
|
+
Example-specific note for local Ollama:
|
|
90
|
+
|
|
91
|
+
- this example accepts either `PROVIDER=ollama` with a bare `MODEL` such as
|
|
92
|
+
`qwen2.5:1.5b-instruct`
|
|
93
|
+
- or an explicit LiteLLM-style `MODEL=ollama/qwen2.5:1.5b-instruct`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP calendar admin tool-gating example."""
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""MCP-surface tool gating using authoritative Context Compiler state."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal, NotRequired, TypedDict, cast
|
|
5
|
+
|
|
6
|
+
from context_compiler import (
|
|
7
|
+
POLICY_PROHIBIT,
|
|
8
|
+
POLICY_USE,
|
|
9
|
+
State,
|
|
10
|
+
create_engine,
|
|
11
|
+
get_decision_state,
|
|
12
|
+
get_policy_items,
|
|
13
|
+
is_clarify,
|
|
14
|
+
)
|
|
15
|
+
from context_compiler.engine import Engine
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class McpToolDefinition(TypedDict):
|
|
19
|
+
name: str
|
|
20
|
+
title: str
|
|
21
|
+
description: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class McpToolCall(TypedDict):
|
|
25
|
+
tool_name: str
|
|
26
|
+
arguments: dict[str, str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExposedMcpTools(TypedDict):
|
|
30
|
+
tools: list[McpToolDefinition]
|
|
31
|
+
hidden_tool_names: list[str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class McpToolExecutionResult(TypedDict):
|
|
35
|
+
authorization_state: Literal["allowed", "blocked"]
|
|
36
|
+
tool_visible: bool
|
|
37
|
+
executed: bool
|
|
38
|
+
blocked_reason: str | None
|
|
39
|
+
tool_result: str | None
|
|
40
|
+
exposed_tools: ExposedMcpTools
|
|
41
|
+
execution_log: list[str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class McpToolTurnResult(TypedDict):
|
|
45
|
+
decision_kind: Literal["clarify", "update", "passthrough"]
|
|
46
|
+
prompt_to_user: str | None
|
|
47
|
+
execution_result: McpToolExecutionResult
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class McpDecisionResult(TypedDict):
|
|
51
|
+
decision_kind: Literal["clarify", "update", "passthrough"]
|
|
52
|
+
prompt_to_user: str | None
|
|
53
|
+
exposed_tools: ExposedMcpTools
|
|
54
|
+
execution_result: NotRequired[McpToolExecutionResult]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _decision_kind_name(
|
|
58
|
+
decision: object,
|
|
59
|
+
) -> Literal["clarify", "update", "passthrough"]:
|
|
60
|
+
if not isinstance(decision, dict):
|
|
61
|
+
raise ValueError("unexpected decision shape")
|
|
62
|
+
|
|
63
|
+
kind = decision.get("kind")
|
|
64
|
+
kind_name = getattr(kind, "value", None)
|
|
65
|
+
if kind_name not in {"clarify", "update", "passthrough"}:
|
|
66
|
+
raise ValueError(f"unexpected decision kind: {kind_name}")
|
|
67
|
+
|
|
68
|
+
return cast(Literal["clarify", "update", "passthrough"], kind_name)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class CalendarAdminMcpHost:
|
|
73
|
+
"""Host-owned MCP registry and execution layer."""
|
|
74
|
+
|
|
75
|
+
execution_log: list[str] = field(default_factory=list)
|
|
76
|
+
_always_available_tools: list[McpToolDefinition] = field(
|
|
77
|
+
default_factory=lambda: [
|
|
78
|
+
{
|
|
79
|
+
"name": "calendar_view_events",
|
|
80
|
+
"title": "View calendar events",
|
|
81
|
+
"description": "List visible events from a calendar.",
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
_calendar_admin_tools: list[McpToolDefinition] = field(
|
|
86
|
+
default_factory=lambda: [
|
|
87
|
+
{
|
|
88
|
+
"name": "calendar_admin_create_event",
|
|
89
|
+
"title": "Create calendar event",
|
|
90
|
+
"description": "Create an administrative event on a calendar.",
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def exposed_mcp_tools(self, state: State) -> ExposedMcpTools:
|
|
96
|
+
tools = self._always_available_tools.copy()
|
|
97
|
+
hidden_tool_names = [tool["name"] for tool in self._calendar_admin_tools]
|
|
98
|
+
|
|
99
|
+
if calendar_admin_mcp_tools_are_allowed(state):
|
|
100
|
+
tools.extend(self._calendar_admin_tools)
|
|
101
|
+
hidden_tool_names = []
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
"tools": tools,
|
|
105
|
+
"hidden_tool_names": hidden_tool_names,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
def execute_mcp_tool(self, tool_call: McpToolCall) -> str:
|
|
109
|
+
calendar_id = tool_call["arguments"]["calendar_id"]
|
|
110
|
+
event_title = tool_call["arguments"]["event_title"]
|
|
111
|
+
self.execution_log.append(
|
|
112
|
+
f"{tool_call['tool_name']}:{calendar_id}:{event_title}"
|
|
113
|
+
)
|
|
114
|
+
return f"created event '{event_title}' on calendar '{calendar_id}'"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def calendar_admin_mcp_tools_are_allowed(state: State) -> bool:
|
|
118
|
+
"""Allow admin MCP tools only from authoritative compiler state."""
|
|
119
|
+
|
|
120
|
+
use_items = set(get_policy_items(state, POLICY_USE))
|
|
121
|
+
prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT))
|
|
122
|
+
|
|
123
|
+
if "calendar_admin" in prohibit_items:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
return "calendar_admin" in use_items
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def execute_mcp_tool_if_allowed(
|
|
130
|
+
tool_call: McpToolCall,
|
|
131
|
+
*,
|
|
132
|
+
state: State,
|
|
133
|
+
host: CalendarAdminMcpHost,
|
|
134
|
+
) -> McpToolExecutionResult:
|
|
135
|
+
"""Expose and execute MCP tools only when authoritative state allows them."""
|
|
136
|
+
|
|
137
|
+
exposed_tools = host.exposed_mcp_tools(state)
|
|
138
|
+
visible_tool_names = [tool["name"] for tool in exposed_tools["tools"]]
|
|
139
|
+
tool_visible = tool_call["tool_name"] in visible_tool_names
|
|
140
|
+
|
|
141
|
+
if not tool_visible:
|
|
142
|
+
return {
|
|
143
|
+
"authorization_state": "blocked",
|
|
144
|
+
"tool_visible": False,
|
|
145
|
+
"executed": False,
|
|
146
|
+
"blocked_reason": "calendar_admin state not authorized",
|
|
147
|
+
"tool_result": None,
|
|
148
|
+
"exposed_tools": exposed_tools,
|
|
149
|
+
"execution_log": host.execution_log.copy(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
tool_result = host.execute_mcp_tool(tool_call)
|
|
153
|
+
return {
|
|
154
|
+
"authorization_state": "allowed",
|
|
155
|
+
"tool_visible": True,
|
|
156
|
+
"executed": True,
|
|
157
|
+
"blocked_reason": None,
|
|
158
|
+
"tool_result": tool_result,
|
|
159
|
+
"exposed_tools": exposed_tools,
|
|
160
|
+
"execution_log": host.execution_log.copy(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def handle_mcp_tool_turn(
|
|
165
|
+
engine: Engine,
|
|
166
|
+
*,
|
|
167
|
+
compiler_input: str,
|
|
168
|
+
tool_call: McpToolCall,
|
|
169
|
+
host: CalendarAdminMcpHost,
|
|
170
|
+
) -> McpToolTurnResult:
|
|
171
|
+
"""Block MCP tool exposure on clarify and otherwise enforce current state."""
|
|
172
|
+
|
|
173
|
+
decision = engine.step(compiler_input)
|
|
174
|
+
|
|
175
|
+
if is_clarify(decision):
|
|
176
|
+
return {
|
|
177
|
+
"decision_kind": "clarify",
|
|
178
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
179
|
+
"execution_result": {
|
|
180
|
+
"authorization_state": "blocked",
|
|
181
|
+
"tool_visible": False,
|
|
182
|
+
"executed": False,
|
|
183
|
+
"blocked_reason": "clarification required before exposing calendar admin MCP tools",
|
|
184
|
+
"tool_result": None,
|
|
185
|
+
"exposed_tools": host.exposed_mcp_tools(engine.state),
|
|
186
|
+
"execution_log": host.execution_log.copy(),
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
authoritative_state = get_decision_state(decision)
|
|
191
|
+
if authoritative_state is None:
|
|
192
|
+
authoritative_state = engine.state
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"decision_kind": _decision_kind_name(decision),
|
|
196
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
197
|
+
"execution_result": execute_mcp_tool_if_allowed(
|
|
198
|
+
tool_call,
|
|
199
|
+
state=authoritative_state,
|
|
200
|
+
host=host,
|
|
201
|
+
),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def describe_exposed_mcp_tools(
|
|
206
|
+
engine: Engine,
|
|
207
|
+
*,
|
|
208
|
+
compiler_input: str,
|
|
209
|
+
host: CalendarAdminMcpHost,
|
|
210
|
+
) -> McpDecisionResult:
|
|
211
|
+
"""Return the currently exposed MCP tools after applying compiler input."""
|
|
212
|
+
|
|
213
|
+
decision = engine.step(compiler_input)
|
|
214
|
+
|
|
215
|
+
if is_clarify(decision):
|
|
216
|
+
return {
|
|
217
|
+
"decision_kind": "clarify",
|
|
218
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
219
|
+
"exposed_tools": host.exposed_mcp_tools(engine.state),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
authoritative_state = get_decision_state(decision)
|
|
223
|
+
if authoritative_state is None:
|
|
224
|
+
authoritative_state = engine.state
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
"decision_kind": _decision_kind_name(decision),
|
|
228
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
229
|
+
"exposed_tools": host.exposed_mcp_tools(authoritative_state),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def run_demo() -> McpToolExecutionResult:
|
|
234
|
+
"""Run a deterministic MCP demonstration with explicit authorization state."""
|
|
235
|
+
|
|
236
|
+
engine = create_engine()
|
|
237
|
+
engine.step("use calendar_admin")
|
|
238
|
+
host = CalendarAdminMcpHost()
|
|
239
|
+
|
|
240
|
+
return execute_mcp_tool_if_allowed(
|
|
241
|
+
{
|
|
242
|
+
"tool_name": "calendar_admin_create_event",
|
|
243
|
+
"arguments": {
|
|
244
|
+
"calendar_id": "ops-admin",
|
|
245
|
+
"event_title": "Quarterly access review",
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
state=engine.state,
|
|
249
|
+
host=host,
|
|
250
|
+
)
|