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,205 @@
|
|
|
1
|
+
"""Small FastAPI checkpoint-continuation example for travel booking."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from context_compiler import POLICY_USE, State, create_engine, get_policy_items
|
|
8
|
+
from context_compiler.engine import Checkpoint, Engine
|
|
9
|
+
from fastapi import FastAPI, HTTPException
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from typing_extensions import TypedDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BookingRecord(TypedDict):
|
|
15
|
+
booking_id: str
|
|
16
|
+
active_itinerary: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BookingResponse(TypedDict):
|
|
20
|
+
booking_id: str
|
|
21
|
+
active_itinerary: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ChangeTripResponse(TypedDict):
|
|
25
|
+
decision_kind: Literal["clarify"]
|
|
26
|
+
prompt_to_user: str | None
|
|
27
|
+
checkpoint_pending: bool
|
|
28
|
+
booking: BookingResponse
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfirmResponse(TypedDict):
|
|
32
|
+
decision_kind: Literal["clarify", "update", "passthrough"]
|
|
33
|
+
prompt_to_user: str | None
|
|
34
|
+
checkpoint_pending: bool
|
|
35
|
+
host_applied_change: bool
|
|
36
|
+
booking: BookingResponse
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ChangeTripRequest(BaseModel):
|
|
40
|
+
booking_id: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfirmRequest(BaseModel):
|
|
44
|
+
booking_id: str
|
|
45
|
+
user_input: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class CheckpointStore:
|
|
50
|
+
"""Host-owned checkpoint persistence for stateless HTTP requests."""
|
|
51
|
+
|
|
52
|
+
checkpoints_by_booking_id: dict[str, Checkpoint] = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def save(self, booking_id: str, checkpoint: Checkpoint) -> None:
|
|
55
|
+
self.checkpoints_by_booking_id[booking_id] = checkpoint
|
|
56
|
+
|
|
57
|
+
def load(self, booking_id: str) -> Checkpoint:
|
|
58
|
+
checkpoint = self.checkpoints_by_booking_id.get(booking_id)
|
|
59
|
+
if checkpoint is None:
|
|
60
|
+
raise KeyError(booking_id)
|
|
61
|
+
return checkpoint
|
|
62
|
+
|
|
63
|
+
def has(self, booking_id: str) -> bool:
|
|
64
|
+
return booking_id in self.checkpoints_by_booking_id
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class BookingStore:
|
|
69
|
+
"""Host-owned booking persistence for the example."""
|
|
70
|
+
|
|
71
|
+
bookings_by_id: dict[str, BookingRecord] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
def get_or_create(self, booking_id: str) -> BookingRecord:
|
|
74
|
+
booking = self.bookings_by_id.get(booking_id)
|
|
75
|
+
if booking is None:
|
|
76
|
+
booking = {"booking_id": booking_id, "active_itinerary": "boston_trip"}
|
|
77
|
+
self.bookings_by_id[booking_id] = booking
|
|
78
|
+
return booking
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class BookingHost:
|
|
83
|
+
"""Host-owned booking mutation logic."""
|
|
84
|
+
|
|
85
|
+
booking_store: BookingStore
|
|
86
|
+
applied_changes: list[str] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
def apply_selected_itinerary(self, booking_id: str, state: State) -> bool:
|
|
89
|
+
selected_itinerary = select_itinerary_from_state(state)
|
|
90
|
+
if selected_itinerary is None:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
booking = self.booking_store.get_or_create(booking_id)
|
|
94
|
+
booking["active_itinerary"] = selected_itinerary
|
|
95
|
+
self.applied_changes.append(selected_itinerary)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def select_itinerary_from_state(state: State) -> str | None:
|
|
100
|
+
use_items = list(get_policy_items(state, POLICY_USE))
|
|
101
|
+
if not use_items:
|
|
102
|
+
return None
|
|
103
|
+
return use_items[0]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def restore_engine_from_checkpoint(checkpoint: Checkpoint) -> Engine:
|
|
107
|
+
engine = create_engine()
|
|
108
|
+
engine.import_checkpoint(checkpoint)
|
|
109
|
+
return engine
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def restore_engine_from_authoritative_state_only(checkpoint: Checkpoint) -> Engine:
|
|
113
|
+
authoritative_state = checkpoint["authoritative_state"]
|
|
114
|
+
return create_engine(state=authoritative_state)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fresh_engine() -> Engine:
|
|
118
|
+
"""Create a fresh engine per request to demonstrate stateless boundaries."""
|
|
119
|
+
|
|
120
|
+
return create_engine()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_app(
|
|
124
|
+
*,
|
|
125
|
+
checkpoint_store: CheckpointStore | None = None,
|
|
126
|
+
booking_store: BookingStore | None = None,
|
|
127
|
+
engine_factory: Callable[[], Engine] = _fresh_engine,
|
|
128
|
+
) -> FastAPI:
|
|
129
|
+
checkpoint_store = checkpoint_store or CheckpointStore()
|
|
130
|
+
booking_store = booking_store or BookingStore()
|
|
131
|
+
booking_host = BookingHost(booking_store=booking_store)
|
|
132
|
+
|
|
133
|
+
app = FastAPI(title="checkpoint-continuation-fastapi-example")
|
|
134
|
+
app.state.checkpoint_store = checkpoint_store
|
|
135
|
+
app.state.booking_store = booking_store
|
|
136
|
+
app.state.booking_host = booking_host
|
|
137
|
+
app.state.engine_factory = engine_factory
|
|
138
|
+
|
|
139
|
+
@app.post("/change-trip")
|
|
140
|
+
def change_trip(request: ChangeTripRequest) -> ChangeTripResponse:
|
|
141
|
+
booking = booking_store.get_or_create(request.booking_id)
|
|
142
|
+
engine = engine_factory()
|
|
143
|
+
|
|
144
|
+
compiler_input = f"use chicago_trip instead of {booking['active_itinerary']}"
|
|
145
|
+
decision = engine.step(compiler_input)
|
|
146
|
+
checkpoint_store.save(request.booking_id, engine.export_checkpoint())
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"decision_kind": "clarify",
|
|
150
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
151
|
+
"checkpoint_pending": engine.has_pending_clarification(),
|
|
152
|
+
"booking": {
|
|
153
|
+
"booking_id": booking["booking_id"],
|
|
154
|
+
"active_itinerary": booking["active_itinerary"],
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@app.post("/confirm")
|
|
159
|
+
def confirm(request: ConfirmRequest) -> ConfirmResponse:
|
|
160
|
+
booking = booking_store.get_or_create(request.booking_id)
|
|
161
|
+
try:
|
|
162
|
+
checkpoint = checkpoint_store.load(request.booking_id)
|
|
163
|
+
except KeyError as exc:
|
|
164
|
+
raise HTTPException(status_code=404, detail="checkpoint not found") from exc
|
|
165
|
+
|
|
166
|
+
engine = restore_engine_from_checkpoint(checkpoint)
|
|
167
|
+
decision = engine.step(request.user_input)
|
|
168
|
+
checkpoint_store.save(request.booking_id, engine.export_checkpoint())
|
|
169
|
+
|
|
170
|
+
host_applied_change = False
|
|
171
|
+
if decision["kind"].value == "update":
|
|
172
|
+
host_applied_change = booking_host.apply_selected_itinerary(
|
|
173
|
+
request.booking_id, engine.state
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"decision_kind": decision["kind"].value,
|
|
178
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
179
|
+
"checkpoint_pending": engine.has_pending_clarification(),
|
|
180
|
+
"host_applied_change": host_applied_change,
|
|
181
|
+
"booking": {
|
|
182
|
+
"booking_id": booking["booking_id"],
|
|
183
|
+
"active_itinerary": booking["active_itinerary"],
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@app.get("/booking")
|
|
188
|
+
def get_booking(booking_id: str) -> BookingResponse:
|
|
189
|
+
booking = booking_store.get_or_create(booking_id)
|
|
190
|
+
return {
|
|
191
|
+
"booking_id": booking["booking_id"],
|
|
192
|
+
"active_itinerary": booking["active_itinerary"],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return app
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
app = create_app()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
print(
|
|
203
|
+
"Run with: uv run fastapi dev "
|
|
204
|
+
"python/examples/checkpoint_continuation/fastapi/app.py"
|
|
205
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Execution authorization
|
|
2
|
+
|
|
3
|
+
These examples show host-side actions executing only when authoritative Context
|
|
4
|
+
Compiler state explicitly authorizes them.
|
|
5
|
+
|
|
6
|
+
They demonstrate observable runtime behavior changes rather than prompt
|
|
7
|
+
compliance. User wording alone does not authorize the action.
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### `expense_approval`
|
|
12
|
+
|
|
13
|
+
Authorizes a host-owned `submit_expense` function only when state contains:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
use expense_approval
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The host blocks execution when state is absent or when state contains:
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
prohibit expense_approval
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The tests cover authorized execution, absent-state blocking, prohibited-state
|
|
26
|
+
blocking, adversarial request text, and the runtime behavior change between
|
|
27
|
+
blocked and authorized state.
|
|
28
|
+
|
|
29
|
+
A FastAPI variant also shows the live-model comparison boundary: a live model can
|
|
30
|
+
say an expense is approved, but the compiler-mediated host still denies the
|
|
31
|
+
protected mutation unless authoritative state authorizes it.
|
context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Expense approval
|
|
2
|
+
|
|
3
|
+
Authoritative state changes whether the host executes `submit_expense` for the
|
|
4
|
+
same expense request. This example shows that execution-authorization behavior
|
|
5
|
+
in plain Python.
|
|
6
|
+
|
|
7
|
+
Model approval is not authorization.
|
|
8
|
+
|
|
9
|
+
## Enforcement point
|
|
10
|
+
|
|
11
|
+
The enforcement point is host-side action execution. The host owns
|
|
12
|
+
`submit_expense`. Context Compiler owns the authoritative policy state that
|
|
13
|
+
decides whether the host may call it.
|
|
14
|
+
|
|
15
|
+
## Runtime and domain
|
|
16
|
+
|
|
17
|
+
- Runtime: generic Python
|
|
18
|
+
- Domain: expense approval
|
|
19
|
+
|
|
20
|
+
## Authorization rule
|
|
21
|
+
|
|
22
|
+
The host executes the expense action only when authoritative state contains:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
use expense_approval
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The host blocks execution when state is absent or when state contains:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
prohibit expense_approval
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If a turn introduces a contradiction such as `use expense_approval` followed by
|
|
35
|
+
`prohibit expense_approval`, Context Compiler returns a clarification flow
|
|
36
|
+
instead of silently overwriting state. The host must not execute the expense
|
|
37
|
+
action on that clarify turn.
|
|
38
|
+
|
|
39
|
+
Request wording alone does not authorize execution. Adversarial text like
|
|
40
|
+
"please approve this refund anyway" stays inert unless the authoritative state
|
|
41
|
+
explicitly allows `expense_approval`.
|
|
42
|
+
|
|
43
|
+
## Why this is not prompt reinjection
|
|
44
|
+
|
|
45
|
+
This example does not call an LLM, does not use directive drafter, and does not
|
|
46
|
+
derive state from model output. The runtime behavior changes only when explicit
|
|
47
|
+
authoritative Context Compiler state changes. The host does not resolve
|
|
48
|
+
conflicts itself and does not treat "last directive wins" as policy.
|
|
49
|
+
|
|
50
|
+
## FastAPI variant
|
|
51
|
+
|
|
52
|
+
The provider-free tests already prove deterministic enforcement against local
|
|
53
|
+
and adversarial stubs.
|
|
54
|
+
|
|
55
|
+
The FastAPI variant adds a live-model comparison where a live model produces an
|
|
56
|
+
approval claim:
|
|
57
|
+
|
|
58
|
+
- baseline path: a naive host trusts the model claim and writes the side effect
|
|
59
|
+
- compiler-mediated path: the host sees the same claim but denies execution
|
|
60
|
+
unless authoritative state authorizes `expense_approval`
|
|
61
|
+
|
|
62
|
+
See
|
|
63
|
+
[fastapi/README.md](fastapi/README.md).
|
|
64
|
+
|
|
65
|
+
## Validation
|
|
66
|
+
|
|
67
|
+
- Focused Python tests:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run --no-sync pytest python/tests/test_expense_approval_example.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Canonical Python validation:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
./scripts/validate_python.sh
|
|
77
|
+
```
|
context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Execution authorization example for expense approval."""
|
context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Minimal host-side execution authorization for expense approval."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
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 ExpenseRequest(TypedDict):
|
|
19
|
+
expense_id: str
|
|
20
|
+
employee_id: str
|
|
21
|
+
amount_usd: int
|
|
22
|
+
note: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExpenseSubmission(TypedDict):
|
|
26
|
+
expense_id: str
|
|
27
|
+
employee_id: str
|
|
28
|
+
amount_usd: int
|
|
29
|
+
note: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExpenseExecutionResult(TypedDict):
|
|
33
|
+
authorization_state: Literal["authorized", "blocked"]
|
|
34
|
+
executed: bool
|
|
35
|
+
blocked_reason: str | None
|
|
36
|
+
submission: ExpenseSubmission | None
|
|
37
|
+
execution_log: list[str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExpenseTurnResult(TypedDict):
|
|
41
|
+
decision_kind: Literal["clarify", "update", "passthrough"]
|
|
42
|
+
prompt_to_user: str | None
|
|
43
|
+
execution_result: ExpenseExecutionResult
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _decision_kind_name(
|
|
47
|
+
decision: object,
|
|
48
|
+
) -> Literal["clarify", "update", "passthrough"]:
|
|
49
|
+
if not isinstance(decision, dict):
|
|
50
|
+
raise ValueError("unexpected decision shape")
|
|
51
|
+
|
|
52
|
+
kind = decision.get("kind")
|
|
53
|
+
kind_name = getattr(kind, "value", None)
|
|
54
|
+
if kind_name not in {"clarify", "update", "passthrough"}:
|
|
55
|
+
raise ValueError(f"unexpected decision kind: {kind_name}")
|
|
56
|
+
|
|
57
|
+
return cast(Literal["clarify", "update", "passthrough"], kind_name)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ExpenseHost:
|
|
62
|
+
"""Host-owned runtime behavior for expense execution."""
|
|
63
|
+
|
|
64
|
+
execution_log: list[str] = field(default_factory=list)
|
|
65
|
+
|
|
66
|
+
def submit_expense(self, request: ExpenseRequest) -> ExpenseSubmission:
|
|
67
|
+
self.execution_log.append(f"submitted:{request['expense_id']}")
|
|
68
|
+
return {
|
|
69
|
+
"expense_id": request["expense_id"],
|
|
70
|
+
"employee_id": request["employee_id"],
|
|
71
|
+
"amount_usd": request["amount_usd"],
|
|
72
|
+
"note": request["note"],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def expense_execution_is_authorized(state: State) -> bool:
|
|
77
|
+
"""Authorize execution only from explicit authoritative compiler state."""
|
|
78
|
+
|
|
79
|
+
use_items = set(get_policy_items(state, POLICY_USE))
|
|
80
|
+
prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT))
|
|
81
|
+
|
|
82
|
+
if "expense_approval" in prohibit_items:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
return "expense_approval" in use_items
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def execute_expense_if_authorized(
|
|
89
|
+
request: ExpenseRequest,
|
|
90
|
+
*,
|
|
91
|
+
state: State,
|
|
92
|
+
host: ExpenseHost,
|
|
93
|
+
) -> ExpenseExecutionResult:
|
|
94
|
+
"""Run the host-side action only when authoritative state allows it."""
|
|
95
|
+
|
|
96
|
+
if not expense_execution_is_authorized(state):
|
|
97
|
+
return {
|
|
98
|
+
"authorization_state": "blocked",
|
|
99
|
+
"executed": False,
|
|
100
|
+
"blocked_reason": "expense_approval state not authorized",
|
|
101
|
+
"submission": None,
|
|
102
|
+
"execution_log": host.execution_log.copy(),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
submission = host.submit_expense(request)
|
|
106
|
+
return {
|
|
107
|
+
"authorization_state": "authorized",
|
|
108
|
+
"executed": True,
|
|
109
|
+
"blocked_reason": None,
|
|
110
|
+
"submission": submission,
|
|
111
|
+
"execution_log": host.execution_log.copy(),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def handle_expense_turn(
|
|
116
|
+
engine: Engine,
|
|
117
|
+
*,
|
|
118
|
+
compiler_input: str,
|
|
119
|
+
request: ExpenseRequest,
|
|
120
|
+
host: ExpenseHost,
|
|
121
|
+
) -> ExpenseTurnResult:
|
|
122
|
+
"""Block execution on clarify and otherwise enforce current authoritative state."""
|
|
123
|
+
|
|
124
|
+
decision = engine.step(compiler_input)
|
|
125
|
+
|
|
126
|
+
if is_clarify(decision):
|
|
127
|
+
return {
|
|
128
|
+
"decision_kind": "clarify",
|
|
129
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
130
|
+
"execution_result": {
|
|
131
|
+
"authorization_state": "blocked",
|
|
132
|
+
"executed": False,
|
|
133
|
+
"blocked_reason": "clarification required before expense execution",
|
|
134
|
+
"submission": None,
|
|
135
|
+
"execution_log": host.execution_log.copy(),
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
authoritative_state = get_decision_state(decision)
|
|
140
|
+
if authoritative_state is None:
|
|
141
|
+
authoritative_state = engine.state
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"decision_kind": _decision_kind_name(decision),
|
|
145
|
+
"prompt_to_user": decision.get("prompt_to_user"),
|
|
146
|
+
"execution_result": execute_expense_if_authorized(
|
|
147
|
+
request,
|
|
148
|
+
state=authoritative_state,
|
|
149
|
+
host=host,
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def run_demo() -> ExpenseExecutionResult:
|
|
155
|
+
"""Run a deterministic demonstration with explicit authorization state."""
|
|
156
|
+
|
|
157
|
+
engine = create_engine()
|
|
158
|
+
engine.step("use expense_approval")
|
|
159
|
+
|
|
160
|
+
request: ExpenseRequest = {
|
|
161
|
+
"expense_id": "expense-100",
|
|
162
|
+
"employee_id": "employee-123",
|
|
163
|
+
"amount_usd": 245,
|
|
164
|
+
"note": "Taxi from airport to client office.",
|
|
165
|
+
}
|
|
166
|
+
host = ExpenseHost()
|
|
167
|
+
|
|
168
|
+
return execute_expense_if_authorized(request, state=engine.state, host=host)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Model approval is not authorization
|
|
2
|
+
|
|
3
|
+
This FastAPI variant provides provider-free runtime-boundary validation by
|
|
4
|
+
default, plus an opt-in live-model validation path.
|
|
5
|
+
|
|
6
|
+
The provider-free tests already prove deterministic enforcement against local
|
|
7
|
+
and adversarial stubs.
|
|
8
|
+
|
|
9
|
+
The default tests here prove the runtime boundary behavior with a visible
|
|
10
|
+
approval claim present at the request surface.
|
|
11
|
+
|
|
12
|
+
If you also run the opt-in live-model validation with
|
|
13
|
+
`RUN_EXPENSE_APPROVAL_LIVE_MODEL=1`, this variant demonstrates that the same
|
|
14
|
+
boundary still matters when a real model says the expense is approved.
|
|
15
|
+
|
|
16
|
+
## What the user sees
|
|
17
|
+
|
|
18
|
+
The app exposes two mutation paths for the same expense request:
|
|
19
|
+
|
|
20
|
+
- baseline path: a naive host trusts the model approval claim and executes the
|
|
21
|
+
protected mutation
|
|
22
|
+
- compiler-mediated path: the host sees the same kind of model approval claim,
|
|
23
|
+
but only executes when authoritative Context Compiler state permits
|
|
24
|
+
`expense_approval`
|
|
25
|
+
|
|
26
|
+
The request also carries a visible `agent_claim` field to represent
|
|
27
|
+
caller-supplied or model-supplied approval text.
|
|
28
|
+
|
|
29
|
+
The observable proof is a host-owned append-only JSONL file:
|
|
30
|
+
|
|
31
|
+
- baseline writes one record when the model returns an approval claim
|
|
32
|
+
- compiler-mediated returns `403` and writes no record when state does not
|
|
33
|
+
authorize execution
|
|
34
|
+
- compiler-mediated writes one record only when authoritative state includes:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
use expense_approval
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If a request introduces a contradiction such as `prohibit expense_approval`
|
|
41
|
+
against an already authorized state, Context Compiler returns a clarify flow.
|
|
42
|
+
The host returns a conflict response and writes no record.
|
|
43
|
+
|
|
44
|
+
## Same request, different state
|
|
45
|
+
|
|
46
|
+
The compiler-mediated proof uses the same endpoint, the same model approval
|
|
47
|
+
claim, and the same expense action. Only authoritative Context Compiler state
|
|
48
|
+
changes.
|
|
49
|
+
|
|
50
|
+
| Endpoint | Model claim | Authoritative state | Compiler input | Outcome |
|
|
51
|
+
| --- | --- | --- | --- | --- |
|
|
52
|
+
| `/compiler/expenses` | approved | absent | none | `403`, no side effect |
|
|
53
|
+
| `/compiler/expenses` | approved | `use expense_approval` | none | `200`, one side effect |
|
|
54
|
+
| `/compiler/expenses` | approved | `use expense_approval` | `prohibit expense_approval` | `409`, clarify, no new side effect |
|
|
55
|
+
|
|
56
|
+
## Enforcement boundary
|
|
57
|
+
|
|
58
|
+
The model claim is visible in both paths.
|
|
59
|
+
|
|
60
|
+
Caller-supplied approval text is also visible in both paths.
|
|
61
|
+
|
|
62
|
+
The baseline host treats that claim as authority.
|
|
63
|
+
|
|
64
|
+
The compiler-mediated host does not treat the visible approval text as
|
|
65
|
+
authorization.
|
|
66
|
+
|
|
67
|
+
Context Compiler state is the only authorization source for the protected
|
|
68
|
+
mutation after an approval claim is present in the compiler-mediated path.
|
|
69
|
+
|
|
70
|
+
## Validation
|
|
71
|
+
|
|
72
|
+
Focused provider-free tests:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
uv run --no-sync pytest python/tests/test_fastapi_expense_approval_example.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Optional live-model validation:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export RUN_EXPENSE_APPROVAL_LIVE_MODEL=1
|
|
82
|
+
export OPENAI_API_KEY=...
|
|
83
|
+
export MODEL=openai/gpt-4o-mini
|
|
84
|
+
uv run --no-sync pytest python/tests/test_fastapi_expense_approval_live_model.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Unless you run that env-var-gated command, this repo has only validated the
|
|
88
|
+
provider-free runtime-boundary path, not the live-provider path.
|
|
89
|
+
|
|
90
|
+
The live-model path uses the shared provider contract documented in
|
|
91
|
+
[PROVIDER_CONTRACT.md](../../../../../PROVIDER_CONTRACT.md).
|
|
92
|
+
|
|
93
|
+
## Run locally
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv run fastapi dev python/examples/execution_authorization/expense_approval/fastapi/app.py
|
|
97
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastAPI execution-authorization comparison for expense approval."""
|