Spaces:
Sleeping
Sleeping
b2230765034 commited on
Commit ·
af6094d
0
Parent(s):
Initial commit - Secure Reasoning MCP Server
Browse files- README.md +65 -0
- app.py +148 -0
- crypto_engine.py +333 -0
- graph.py +702 -0
- mock_tools.py +221 -0
- prompts.py +407 -0
- requirements.txt +18 -0
- schemas.py +135 -0
- server.py +60 -0
- state.py +113 -0
README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Secure Reasoning MCP Server
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: green
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
tags:
|
| 11 |
+
- mcp-in-action-track-enterprise
|
| 12 |
+
- agent
|
| 13 |
+
- security
|
| 14 |
+
- langgraph
|
| 15 |
+
- merkle-tree
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
# 🛡️ Secure Reasoning MCP Server
|
| 19 |
+
> **"Don't Trust, Verify."** — AI Ajanları için Şeffaf, Denetlenebilir ve Değiştirilemez Muhakeme (Reasoning) Katmanı.
|
| 20 |
+
|
| 21 |
+
[](https://huggingface.co/spaces)
|
| 22 |
+
[](https://gradio.app)
|
| 23 |
+
[](https://langchain-ai.github.io/langgraph/)
|
| 24 |
+
|
| 25 |
+
## 🏆 Hackathon Track
|
| 26 |
+
Bu proje **MCP 1st Birthday Hackathon** kapsamında geliştirilmiştir.
|
| 27 |
+
* **Track:** `Track 2: MCP in Action`
|
| 28 |
+
* **Category Tag:** `mcp-in-action-track-enterprise`
|
| 29 |
+
*(Not: Bu proje kurumsal yapay zeka güvenliği ve denetlenebilirlik (audit) sorunlarına çözüm getirdiği için Enterprise kategorisindedir.)*
|
| 30 |
+
|
| 31 |
+
## 💡 Problem: Kara Kutu Sorunu
|
| 32 |
+
Otonom AI ajanları (Agents) giderek daha karmaşık görevleri yerine getiriyor. Ancak kritik bir sorun var: **Bir ajanın neden o kararı verdiğini veya işlem sırasında manipüle edilip edilmediğini nasıl kanıtlayabilirsiniz?**
|
| 33 |
+
Mevcut sistemlerde loglar silinebilir, değiştirilebilir ve ajanın düşünce süreci (reasoning chain) bir kara kutudur.
|
| 34 |
+
|
| 35 |
+
## 🚀 Çözüm: Kriptografik "Chain-of-Checks"
|
| 36 |
+
Biz, sadece "Chain-of-Thought" (Düşünce Zinciri) değil, **"Chain-of-Checks" (Denetim Zinciri)** sunuyoruz.
|
| 37 |
+
|
| 38 |
+
Sistemimiz iki ana katmandan oluşur:
|
| 39 |
+
1. **The Brain (LangGraph):** Planlayan, güvenlik kontrolü yapan ve uygulayan zeka.
|
| 40 |
+
2. **The Ledger (Crypto Engine):** Her adımı hash'leyen, Merkle Ağacına ekleyen ve WORM (Write-Once-Read-Many) depolamada saklayan kasa.
|
| 41 |
+
|
| 42 |
+
## 🏗️ Mimari (Architecture)
|
| 43 |
+
|
| 44 |
+
Sistemimiz **Gradio 5** arayüzü arkasında çalışan, olay tabanlı (event-driven) bir LangGraph mimarisi kullanır.
|
| 45 |
+
|
| 46 |
+
```mermaid
|
| 47 |
+
graph TD
|
| 48 |
+
User[Kullanıcı Görevi] -->|Gradio UI| Agent
|
| 49 |
+
|
| 50 |
+
subgraph "🛡️ Secure Agent (The Brain)"
|
| 51 |
+
Agent --> Plan[📝 Planner Node]
|
| 52 |
+
Plan --> Safety{🛡️ Safety Check}
|
| 53 |
+
Safety -->|Riskli| Refine[Refiner Node]
|
| 54 |
+
Safety -->|Güvenli| Exec[⚡ Executor Node]
|
| 55 |
+
Exec --> Justify[💭 Justification]
|
| 56 |
+
end
|
| 57 |
+
|
| 58 |
+
subgraph "🔒 Immutable Ledger (The Vault)"
|
| 59 |
+
Exec -.->|Log Data| Hash[#️⃣ SHA-256 Hash]
|
| 60 |
+
Hash --> Merkle[🌳 Merkle Tree Update]
|
| 61 |
+
Merkle --> WORM[💾 WORM Storage]
|
| 62 |
+
WORM -.->|Cryptographic Proof| UI[Gradio Dashboard]
|
| 63 |
+
end
|
| 64 |
+
|
| 65 |
+
Justify -->|Stream| UI
|
app.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import asyncio
|
| 5 |
+
import uuid
|
| 6 |
+
|
| 7 |
+
# Your modules
|
| 8 |
+
from graph import create_reasoning_graph
|
| 9 |
+
from state import create_initial_state
|
| 10 |
+
|
| 11 |
+
# Load environment variables
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
# Initialize the graph
|
| 15 |
+
graph = create_reasoning_graph()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
async def run_agent_stream(user_input):
|
| 19 |
+
if not user_input or not user_input.strip():
|
| 20 |
+
yield "Please enter a task.", "SYSTEM READY"
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
reasoning_log = "Starting Secure Reasoning Pipeline...\n\n"
|
| 24 |
+
crypto_log = "SECURE LEDGER INITIALIZED...\nWaiting for execution...\n"
|
| 25 |
+
|
| 26 |
+
yield reasoning_log, crypto_log
|
| 27 |
+
|
| 28 |
+
task_id = f"task_{uuid.uuid4().hex[:8]}"
|
| 29 |
+
config = {"configurable": {"thread_id": task_id}}
|
| 30 |
+
|
| 31 |
+
initial_state = create_initial_state(
|
| 32 |
+
task=user_input.strip(),
|
| 33 |
+
task_id=task_id,
|
| 34 |
+
user_id="gradio_user"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
async for event in graph.astream(initial_state, config, stream_mode="values"):
|
| 39 |
+
|
| 40 |
+
if "messages" in event and event["messages"]:
|
| 41 |
+
last_msg = event["messages"][-1]
|
| 42 |
+
if hasattr(last_msg, "content") and hasattr(last_msg, "type"):
|
| 43 |
+
if last_msg.type == "ai":
|
| 44 |
+
content = last_msg.content[:500] if len(last_msg.content) > 500 else last_msg.content
|
| 45 |
+
reasoning_log += f"\nAgent: {content}\n"
|
| 46 |
+
|
| 47 |
+
if "plan" in event and event["plan"]:
|
| 48 |
+
plan = event["plan"]
|
| 49 |
+
if hasattr(plan, "steps") and plan.steps:
|
| 50 |
+
plan_text = "\nExecution Plan:\n"
|
| 51 |
+
for step in plan.steps:
|
| 52 |
+
plan_text += f" - Step {step.step_number}: {step.action}\n"
|
| 53 |
+
if plan_text not in reasoning_log:
|
| 54 |
+
reasoning_log += plan_text
|
| 55 |
+
|
| 56 |
+
if "status" in event:
|
| 57 |
+
status = event["status"]
|
| 58 |
+
if status == "executing":
|
| 59 |
+
step_idx = event.get("current_step_index", 0)
|
| 60 |
+
reasoning_log += f"\nExecuting Step {step_idx + 1}...\n"
|
| 61 |
+
elif status == "completed":
|
| 62 |
+
reasoning_log += "\nTask Completed Successfully!\n"
|
| 63 |
+
elif status == "blocked":
|
| 64 |
+
reasoning_log += "\nTask Blocked by Safety Guardrails\n"
|
| 65 |
+
elif status == "failed":
|
| 66 |
+
error = event.get("error", "Unknown error")
|
| 67 |
+
reasoning_log += f"\nTask Failed: {error}\n"
|
| 68 |
+
|
| 69 |
+
if "logs" in event and event["logs"]:
|
| 70 |
+
latest_log = event["logs"][-1]
|
| 71 |
+
|
| 72 |
+
timestamp = getattr(latest_log, "timestamp", "N/A")
|
| 73 |
+
action_hash = getattr(latest_log, "action_hash", "N/A")
|
| 74 |
+
merkle_root = getattr(latest_log, "merkle_root", "N/A")
|
| 75 |
+
worm_path = getattr(latest_log, "worm_path", "memory")
|
| 76 |
+
step_number = getattr(latest_log, "step_number", "?")
|
| 77 |
+
|
| 78 |
+
if hasattr(timestamp, "isoformat"):
|
| 79 |
+
timestamp = timestamp.isoformat()
|
| 80 |
+
|
| 81 |
+
log_entry = f"\n--------------------------------------------------\nSTEP: {step_number}\nTIME: {timestamp}\nHASH: {str(action_hash)[:20]}...\nROOT: {str(merkle_root)[:20]}...\nWORM: {worm_path}\nPROOF VERIFIED\n--------------------------------------------------\n"
|
| 82 |
+
|
| 83 |
+
if str(action_hash)[:20] not in crypto_log:
|
| 84 |
+
crypto_log += log_entry
|
| 85 |
+
|
| 86 |
+
yield reasoning_log, crypto_log
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
reasoning_log += f"\n\nError: {str(e)}\n"
|
| 90 |
+
yield reasoning_log, crypto_log
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
custom_css = '''
|
| 94 |
+
#reasoning-box {
|
| 95 |
+
height: 500px;
|
| 96 |
+
overflow-y: scroll;
|
| 97 |
+
background-color: #f9f9f9;
|
| 98 |
+
border: 1px solid #ddd;
|
| 99 |
+
padding: 15px;
|
| 100 |
+
}
|
| 101 |
+
#crypto-box {
|
| 102 |
+
height: 500px;
|
| 103 |
+
overflow-y: scroll;
|
| 104 |
+
background-color: #1e1e1e;
|
| 105 |
+
color: #00ff00;
|
| 106 |
+
font-family: monospace;
|
| 107 |
+
border: 1px solid #333;
|
| 108 |
+
padding: 15px;
|
| 109 |
+
}
|
| 110 |
+
'''
|
| 111 |
+
|
| 112 |
+
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css, title="Secure Reasoning MCP") as demo:
|
| 113 |
+
|
| 114 |
+
gr.Markdown("# Secure Reasoning MCP Server\n**Verifier:** Gradio 5 + LangGraph + Merkle Trees")
|
| 115 |
+
|
| 116 |
+
with gr.Row():
|
| 117 |
+
with gr.Column(scale=1):
|
| 118 |
+
user_input = gr.Textbox(
|
| 119 |
+
label="Task Description",
|
| 120 |
+
placeholder="E.g.: Write a short report about renewable energy...",
|
| 121 |
+
lines=2
|
| 122 |
+
)
|
| 123 |
+
submit_btn = gr.Button("Start Task", variant="primary")
|
| 124 |
+
|
| 125 |
+
with gr.Row():
|
| 126 |
+
with gr.Column(scale=1):
|
| 127 |
+
gr.Markdown("### Agent Reasoning Flow")
|
| 128 |
+
reasoning_output = gr.Markdown(elem_id="reasoning-box", value="Waiting for task...")
|
| 129 |
+
|
| 130 |
+
with gr.Column(scale=1):
|
| 131 |
+
gr.Markdown("### Immutable Crypto Ledger")
|
| 132 |
+
crypto_output = gr.Textbox(
|
| 133 |
+
elem_id="crypto-box",
|
| 134 |
+
value="SYSTEM READY",
|
| 135 |
+
lines=20,
|
| 136 |
+
max_lines=20,
|
| 137 |
+
show_label=False,
|
| 138 |
+
interactive=False
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
submit_btn.click(
|
| 142 |
+
fn=run_agent_stream,
|
| 143 |
+
inputs=[user_input],
|
| 144 |
+
outputs=[reasoning_output, crypto_output]
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
if __name__ == "__main__":
|
| 148 |
+
demo.launch()
|
crypto_engine.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def hash_tool(data):
|
| 8 |
+
"""
|
| 9 |
+
Hash tool: Convert input to deterministic string and return SHA-256 hex digest.
|
| 10 |
+
"""
|
| 11 |
+
if isinstance(data, dict):
|
| 12 |
+
data_str = json.dumps(data, sort_keys=True)
|
| 13 |
+
else:
|
| 14 |
+
data_str = str(data)
|
| 15 |
+
|
| 16 |
+
return hashlib.sha256(data_str.encode()).hexdigest()
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MerkleTree:
|
| 20 |
+
"""
|
| 21 |
+
Merkle Tree: Maintains a list of leaves and recalculates root on each update.
|
| 22 |
+
"""
|
| 23 |
+
def __init__(self):
|
| 24 |
+
self.leaves = []
|
| 25 |
+
self.root = None
|
| 26 |
+
|
| 27 |
+
def _calculate_root(self, leaves):
|
| 28 |
+
"""
|
| 29 |
+
Calculate Merkle root from a list of leaves.
|
| 30 |
+
If empty, return None. If single leaf, return it. Otherwise, build tree bottom-up.
|
| 31 |
+
"""
|
| 32 |
+
if not leaves:
|
| 33 |
+
return None
|
| 34 |
+
|
| 35 |
+
if len(leaves) == 1:
|
| 36 |
+
return leaves[0]
|
| 37 |
+
|
| 38 |
+
current_level = leaves[:]
|
| 39 |
+
|
| 40 |
+
while len(current_level) > 1:
|
| 41 |
+
next_level = []
|
| 42 |
+
for i in range(0, len(current_level), 2):
|
| 43 |
+
left = current_level[i]
|
| 44 |
+
right = current_level[i + 1] if i + 1 < len(current_level) else left
|
| 45 |
+
combined = hashlib.sha256((left + right).encode()).hexdigest()
|
| 46 |
+
next_level.append(combined)
|
| 47 |
+
current_level = next_level
|
| 48 |
+
|
| 49 |
+
return current_level[0]
|
| 50 |
+
|
| 51 |
+
def update(self, new_hash):
|
| 52 |
+
"""
|
| 53 |
+
Add a new leaf and recalculate the Merkle root.
|
| 54 |
+
Returns the new root.
|
| 55 |
+
"""
|
| 56 |
+
self.leaves.append(new_hash)
|
| 57 |
+
self.root = self._calculate_root(self.leaves)
|
| 58 |
+
return self.root
|
| 59 |
+
|
| 60 |
+
def get_proof(self, index):
|
| 61 |
+
"""
|
| 62 |
+
Generate Merkle proof for a leaf at given index.
|
| 63 |
+
Returns a list of (sibling_hash, position) tuples where position is 'left' or 'right'.
|
| 64 |
+
"""
|
| 65 |
+
if index < 0 or index >= len(self.leaves):
|
| 66 |
+
return None
|
| 67 |
+
|
| 68 |
+
proof = []
|
| 69 |
+
current_index = index
|
| 70 |
+
current_level = self.leaves[:]
|
| 71 |
+
|
| 72 |
+
while len(current_level) > 1:
|
| 73 |
+
# Determine if current_index is odd (right) or even (left)
|
| 74 |
+
is_right = current_index % 2 == 1
|
| 75 |
+
sibling_index = current_index - 1 if is_right else current_index + 1
|
| 76 |
+
|
| 77 |
+
# Get sibling hash if it exists
|
| 78 |
+
if sibling_index < len(current_level):
|
| 79 |
+
sibling_hash = current_level[sibling_index]
|
| 80 |
+
position = "left" if is_right else "right"
|
| 81 |
+
proof.append({"hash": sibling_hash, "position": position})
|
| 82 |
+
|
| 83 |
+
# Move to next level
|
| 84 |
+
current_index = current_index // 2
|
| 85 |
+
next_level = []
|
| 86 |
+
for i in range(0, len(current_level), 2):
|
| 87 |
+
left = current_level[i]
|
| 88 |
+
right = current_level[i + 1] if i + 1 < len(current_level) else left
|
| 89 |
+
combined = hashlib.sha256((left + right).encode()).hexdigest()
|
| 90 |
+
next_level.append(combined)
|
| 91 |
+
current_level = next_level
|
| 92 |
+
|
| 93 |
+
return proof
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def worm_write_tool(step_data, hash_value, merkle_root, filename="worm_log.jsonl"):
|
| 97 |
+
"""
|
| 98 |
+
WORM (Write Once, Read Many) storage: Append a JSON record to JSONL file.
|
| 99 |
+
Returns the record written.
|
| 100 |
+
"""
|
| 101 |
+
# Determine the next ID by counting existing lines
|
| 102 |
+
next_id = 0
|
| 103 |
+
if os.path.exists(filename):
|
| 104 |
+
with open(filename, "r") as f:
|
| 105 |
+
next_id = sum(1 for _ in f)
|
| 106 |
+
|
| 107 |
+
record = {
|
| 108 |
+
"id": next_id,
|
| 109 |
+
"timestamp": time.time(),
|
| 110 |
+
"step": step_data,
|
| 111 |
+
"hash": hash_value,
|
| 112 |
+
"root": merkle_root
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
with open(filename, "a") as f:
|
| 116 |
+
f.write(json.dumps(record) + "\n")
|
| 117 |
+
|
| 118 |
+
return record
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def proof_generate_tool(record_id, filename="worm_log.jsonl"):
|
| 122 |
+
"""
|
| 123 |
+
Generate a Merkle proof for a specific record in the WORM log.
|
| 124 |
+
Rehydrates the Merkle Tree from the log to ensure proof is against current state.
|
| 125 |
+
Returns a JSON proof containing hash, merkle_proof, root, and timestamp.
|
| 126 |
+
"""
|
| 127 |
+
if not os.path.exists(filename):
|
| 128 |
+
print(f"[PROOF] Error: {filename} does not exist.")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
# Read all records from WORM log
|
| 132 |
+
records = []
|
| 133 |
+
hashes = []
|
| 134 |
+
target_record = None
|
| 135 |
+
|
| 136 |
+
with open(filename, "r") as f:
|
| 137 |
+
for line in f:
|
| 138 |
+
record = json.loads(line.strip())
|
| 139 |
+
records.append(record)
|
| 140 |
+
hashes.append(record["hash"])
|
| 141 |
+
if record["id"] == record_id:
|
| 142 |
+
target_record = record
|
| 143 |
+
|
| 144 |
+
if target_record is None:
|
| 145 |
+
print(f"[PROOF] Error: Record with ID {record_id} not found.")
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
# Rehydrate Merkle Tree from hashes
|
| 149 |
+
tree = MerkleTree()
|
| 150 |
+
for h in hashes:
|
| 151 |
+
tree.update(h)
|
| 152 |
+
|
| 153 |
+
# Get proof for the target record index
|
| 154 |
+
proof = tree.get_proof(record_id)
|
| 155 |
+
|
| 156 |
+
proof_result = {
|
| 157 |
+
"record_id": record_id,
|
| 158 |
+
"hash": target_record["hash"],
|
| 159 |
+
"merkle_proof": proof,
|
| 160 |
+
"merkle_root": tree.root,
|
| 161 |
+
"timestamp": target_record["timestamp"],
|
| 162 |
+
"step_details": target_record["step"]
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
return proof_result
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def verify_proof_tool(target_hash, merkle_proof, merkle_root):
|
| 169 |
+
"""
|
| 170 |
+
Verify if a target_hash belongs to the merkle_root using the merkle_proof.
|
| 171 |
+
|
| 172 |
+
Logic:
|
| 173 |
+
- Start with current_hash = target_hash
|
| 174 |
+
- Loop through proof items (sibling hashes with positions)
|
| 175 |
+
- Reconstruct the path up to the root
|
| 176 |
+
- Compare final calculated root with provided merkle_root
|
| 177 |
+
|
| 178 |
+
Returns True if valid, False otherwise.
|
| 179 |
+
"""
|
| 180 |
+
if merkle_proof is None:
|
| 181 |
+
return False
|
| 182 |
+
|
| 183 |
+
current_hash = target_hash
|
| 184 |
+
|
| 185 |
+
# Traverse the proof path
|
| 186 |
+
for proof_item in merkle_proof:
|
| 187 |
+
sibling_hash = proof_item["hash"]
|
| 188 |
+
position = proof_item["position"]
|
| 189 |
+
|
| 190 |
+
# Combine hashes based on position
|
| 191 |
+
if position == "left":
|
| 192 |
+
# Sibling is on the left, so: hash(sibling + current)
|
| 193 |
+
combined_str = sibling_hash + current_hash
|
| 194 |
+
elif position == "right":
|
| 195 |
+
# Sibling is on the right, so: hash(current + sibling)
|
| 196 |
+
combined_str = current_hash + sibling_hash
|
| 197 |
+
else:
|
| 198 |
+
return False
|
| 199 |
+
|
| 200 |
+
# Calculate the next level hash
|
| 201 |
+
current_hash = hashlib.sha256(combined_str.encode()).hexdigest()
|
| 202 |
+
|
| 203 |
+
# Final check: does calculated root match provided root?
|
| 204 |
+
return current_hash == merkle_root
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def secure_agent_action(action_type, details, merkle_tree):
|
| 208 |
+
"""
|
| 209 |
+
Gatekeeper Logic: Cite-Before-Act mechanism.
|
| 210 |
+
- READ: Auto-approve
|
| 211 |
+
- WRITE/MUTATE: Require human approval via CLI
|
| 212 |
+
All actions (approved or denied) are logged to WORM storage.
|
| 213 |
+
"""
|
| 214 |
+
action_type = action_type.upper()
|
| 215 |
+
|
| 216 |
+
if action_type == "READ":
|
| 217 |
+
# Auto-approve READ actions
|
| 218 |
+
print(f"\n[GATEKEEPER] READ action detected: {details}")
|
| 219 |
+
print("[GATEKEEPER] Auto-approving READ action.")
|
| 220 |
+
|
| 221 |
+
step_data = {
|
| 222 |
+
"action_type": action_type,
|
| 223 |
+
"details": details,
|
| 224 |
+
"status": "APPROVED"
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
step_hash = hash_tool(step_data)
|
| 228 |
+
merkle_root = merkle_tree.update(step_hash)
|
| 229 |
+
worm_write_tool(step_data, step_hash, merkle_root)
|
| 230 |
+
|
| 231 |
+
print(f"[GATEKEEPER] Merkle Root: {merkle_root}")
|
| 232 |
+
print(f"[GATEKEEPER] Action logged.\n")
|
| 233 |
+
return True
|
| 234 |
+
|
| 235 |
+
elif action_type in ["WRITE", "MUTATE", "DELETE"]:
|
| 236 |
+
# Require approval for mutation actions
|
| 237 |
+
print(f"\n[GATEKEEPER] ⚠️ WRITE/MUTATE action detected: {details}")
|
| 238 |
+
print("[GATEKEEPER] This action requires human approval.")
|
| 239 |
+
|
| 240 |
+
approval = input("[GATEKEEPER] Approve this action? (y/n): ").strip().lower()
|
| 241 |
+
|
| 242 |
+
if approval == "y":
|
| 243 |
+
print("[GATEKEEPER] ✓ Action APPROVED by user.")
|
| 244 |
+
status = "APPROVED"
|
| 245 |
+
result = True
|
| 246 |
+
else:
|
| 247 |
+
print("[GATEKEEPER] ✗ Action DENIED by user.")
|
| 248 |
+
status = "DENIED"
|
| 249 |
+
result = False
|
| 250 |
+
|
| 251 |
+
# Log the action (approved or denied) to maintain audit trail
|
| 252 |
+
step_data = {
|
| 253 |
+
"action_type": action_type,
|
| 254 |
+
"details": details,
|
| 255 |
+
"status": status
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
step_hash = hash_tool(step_data)
|
| 259 |
+
merkle_root = merkle_tree.update(step_hash)
|
| 260 |
+
worm_write_tool(step_data, step_hash, merkle_root)
|
| 261 |
+
|
| 262 |
+
print(f"[GATEKEEPER] Merkle Root: {merkle_root}")
|
| 263 |
+
print(f"[GATEKEEPER] Audit logged.\n")
|
| 264 |
+
return result
|
| 265 |
+
|
| 266 |
+
else:
|
| 267 |
+
print(f"\n[GATEKEEPER] Unknown action type: {action_type}\n")
|
| 268 |
+
return False
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
if __name__ == "__main__":
|
| 272 |
+
print("=" * 70)
|
| 273 |
+
print("SECURE REASONING MCP SERVER - TEST SCENARIO")
|
| 274 |
+
print("=" * 70)
|
| 275 |
+
|
| 276 |
+
# Initialize Merkle Tree
|
| 277 |
+
mt = MerkleTree()
|
| 278 |
+
|
| 279 |
+
# Test 1: READ action (auto-approved)
|
| 280 |
+
print("\n[TEST 1] Simulating READ action...")
|
| 281 |
+
secure_agent_action("READ", "Query user database for profile info", mt)
|
| 282 |
+
|
| 283 |
+
# Test 2: WRITE action (user approval - simulate "y")
|
| 284 |
+
print("[TEST 2] Simulating WRITE action (approve with 'y')...")
|
| 285 |
+
secure_agent_action("WRITE", "Update user profile with new email address", mt)
|
| 286 |
+
|
| 287 |
+
# Test 3: WRITE action (user denial - simulate "n")
|
| 288 |
+
print("[TEST 3] Simulating WRITE action (deny with 'n')...")
|
| 289 |
+
secure_agent_action("WRITE", "Delete user account permanently", mt)
|
| 290 |
+
|
| 291 |
+
print("=" * 70)
|
| 292 |
+
print("TEST SCENARIO COMPLETE")
|
| 293 |
+
print("=" * 70)
|
| 294 |
+
print("\nWORM Log saved to: worm_log.jsonl")
|
| 295 |
+
print("Review the file to verify all actions are logged with hashes and Merkle roots.\n")
|
| 296 |
+
|
| 297 |
+
# Test 4: Generate proof for record_id=1
|
| 298 |
+
print("=" * 70)
|
| 299 |
+
print("PROOF GENERATION TEST")
|
| 300 |
+
print("=" * 70)
|
| 301 |
+
print("\n[TEST 4] Generating Merkle proof for record_id=1...")
|
| 302 |
+
proof = proof_generate_tool(1)
|
| 303 |
+
if proof:
|
| 304 |
+
print("\n[PROOF] Generated Merkle Proof:")
|
| 305 |
+
print(json.dumps(proof, indent=2))
|
| 306 |
+
else:
|
| 307 |
+
proof = None
|
| 308 |
+
print()
|
| 309 |
+
|
| 310 |
+
# Test 5: Verify the proof (positive case)
|
| 311 |
+
print("=" * 70)
|
| 312 |
+
print("PROOF VERIFICATION TEST")
|
| 313 |
+
print("=" * 70)
|
| 314 |
+
if proof:
|
| 315 |
+
print("\n[TEST 5a] Verifying proof with correct hash and root...")
|
| 316 |
+
is_valid = verify_proof_tool(proof["hash"], proof["merkle_proof"], proof["merkle_root"])
|
| 317 |
+
print(f"[VERIFY] Verification Result (POSITIVE): {is_valid}")
|
| 318 |
+
|
| 319 |
+
# Test 5b: Verify with tampered hash (negative case)
|
| 320 |
+
print("\n[TEST 5b] Verifying proof with tampered hash (should fail)...")
|
| 321 |
+
tampered_hash = proof["hash"][:-2] + "XX" # Change last 2 characters
|
| 322 |
+
is_valid_tampered = verify_proof_tool(tampered_hash, proof["merkle_proof"], proof["merkle_root"])
|
| 323 |
+
print(f"[VERIFY] Verification Result (NEGATIVE - tampered hash): {is_valid_tampered}")
|
| 324 |
+
|
| 325 |
+
# Test 5c: Verify with tampered root (negative case)
|
| 326 |
+
print("\n[TEST 5c] Verifying proof with tampered root (should fail)...")
|
| 327 |
+
tampered_root = proof["merkle_root"][:-2] + "XX" # Change last 2 characters
|
| 328 |
+
is_valid_tampered_root = verify_proof_tool(proof["hash"], proof["merkle_proof"], tampered_root)
|
| 329 |
+
print(f"[VERIFY] Verification Result (NEGATIVE - tampered root): {is_valid_tampered_root}")
|
| 330 |
+
|
| 331 |
+
print("\n" + "=" * 70)
|
| 332 |
+
print("ALL TESTS COMPLETE - SECURE REASONING MCP SERVER OPERATIONAL")
|
| 333 |
+
print("=" * 70 + "\n")
|
graph.py
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangGraph State Machine for Secure Reasoning MCP Server
|
| 3 |
+
Implements the Chain-of-Checks workflow with cryptographic logging.
|
| 4 |
+
"""
|
| 5 |
+
import json
|
| 6 |
+
from typing import Literal
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
from langgraph.graph import StateGraph, END
|
| 10 |
+
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
| 11 |
+
from langchain_anthropic import ChatAnthropic
|
| 12 |
+
|
| 13 |
+
from state import AgentState
|
| 14 |
+
from schemas import (
|
| 15 |
+
ExecutionPlan, StepPlan, SafetyCheckResult, ExecutionResult,
|
| 16 |
+
Justification, CryptoLogEntry, HashRequest, MerkleUpdateRequest,
|
| 17 |
+
WORMWriteRequest
|
| 18 |
+
)
|
| 19 |
+
from prompts import (
|
| 20 |
+
format_planner_prompt, format_safety_prompt, format_executor_prompt,
|
| 21 |
+
format_justification_prompt, format_synthesis_prompt
|
| 22 |
+
)
|
| 23 |
+
from mock_tools import MockCryptoTools
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# ============================================================================
|
| 27 |
+
# GLOBAL CONFIGURATION
|
| 28 |
+
# ============================================================================
|
| 29 |
+
|
| 30 |
+
# Initialize LLM (Claude 3.5 Sonnet)
|
| 31 |
+
llm = ChatAnthropic(
|
| 32 |
+
model="claude-3-5-sonnet-20241022",
|
| 33 |
+
temperature=0.1, # Low temperature for deterministic reasoning
|
| 34 |
+
max_tokens=4096
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Initialize crypto tools (replace with real tools when ready)
|
| 38 |
+
crypto_tools = MockCryptoTools()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ============================================================================
|
| 42 |
+
# NODE 1: PLANNER
|
| 43 |
+
# ============================================================================
|
| 44 |
+
|
| 45 |
+
def planner_node(state: AgentState) -> AgentState:
|
| 46 |
+
"""
|
| 47 |
+
Generate a step-by-step execution plan for the task.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
state: Current agent state with the task
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
Updated state with the execution plan
|
| 54 |
+
"""
|
| 55 |
+
print(f"\n{'='*60}")
|
| 56 |
+
print(f"🧠 PLANNER NODE - Generating execution plan")
|
| 57 |
+
print(f"{'='*60}")
|
| 58 |
+
|
| 59 |
+
# Format the prompt
|
| 60 |
+
prompts = format_planner_prompt(state["task"])
|
| 61 |
+
|
| 62 |
+
# Create messages
|
| 63 |
+
messages = [
|
| 64 |
+
SystemMessage(content=prompts["system"]),
|
| 65 |
+
HumanMessage(content=prompts["user"])
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
# Call LLM
|
| 69 |
+
response = llm.invoke(messages)
|
| 70 |
+
|
| 71 |
+
# Parse JSON response
|
| 72 |
+
try:
|
| 73 |
+
plan_data = json.loads(response.content)
|
| 74 |
+
|
| 75 |
+
# Convert to ExecutionPlan model
|
| 76 |
+
steps = [StepPlan(**step) for step in plan_data["steps"]]
|
| 77 |
+
plan = ExecutionPlan(
|
| 78 |
+
steps=steps,
|
| 79 |
+
total_steps=plan_data["total_steps"]
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
print(f"✅ Generated plan with {plan.total_steps} steps:")
|
| 83 |
+
for step in steps:
|
| 84 |
+
print(f" Step {step.step_number}: {step.action}")
|
| 85 |
+
|
| 86 |
+
# Update state
|
| 87 |
+
state["plan"] = plan
|
| 88 |
+
state["current_step_index"] = 0
|
| 89 |
+
state["status"] = "executing"
|
| 90 |
+
state["messages"].extend([
|
| 91 |
+
HumanMessage(content=prompts["user"]),
|
| 92 |
+
AIMessage(content=response.content)
|
| 93 |
+
])
|
| 94 |
+
|
| 95 |
+
return state
|
| 96 |
+
|
| 97 |
+
except json.JSONDecodeError as e:
|
| 98 |
+
print(f"❌ Failed to parse planner response: {e}")
|
| 99 |
+
state["error"] = f"Planner failed to generate valid JSON: {str(e)}"
|
| 100 |
+
state["status"] = "failed"
|
| 101 |
+
return state
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# ============================================================================
|
| 105 |
+
# NODE 2: SAFETY CHECKER
|
| 106 |
+
# ============================================================================
|
| 107 |
+
|
| 108 |
+
def safety_node(state: AgentState) -> AgentState:
|
| 109 |
+
"""
|
| 110 |
+
Validate that the current step is safe to execute.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
state: Current agent state with the plan
|
| 114 |
+
|
| 115 |
+
Returns:
|
| 116 |
+
Updated state with safety validation result
|
| 117 |
+
"""
|
| 118 |
+
print(f"\n{'='*60}")
|
| 119 |
+
print(f"🛡️ SAFETY NODE - Validating step {state['current_step_index'] + 1}")
|
| 120 |
+
print(f"{'='*60}")
|
| 121 |
+
|
| 122 |
+
# Get current step
|
| 123 |
+
current_step = state["plan"].steps[state["current_step_index"]]
|
| 124 |
+
|
| 125 |
+
# Format previous steps for context
|
| 126 |
+
previous_steps = "None"
|
| 127 |
+
if state["current_step_index"] > 0:
|
| 128 |
+
prev_steps_list = [
|
| 129 |
+
f"Step {i+1}: {state['plan'].steps[i].action}"
|
| 130 |
+
for i in range(state["current_step_index"])
|
| 131 |
+
]
|
| 132 |
+
previous_steps = "\n".join(prev_steps_list)
|
| 133 |
+
|
| 134 |
+
# Format the prompt
|
| 135 |
+
prompts = format_safety_prompt(
|
| 136 |
+
step_description=current_step.action,
|
| 137 |
+
task=state["task"],
|
| 138 |
+
step_number=state["current_step_index"] + 1,
|
| 139 |
+
total_steps=state["plan"].total_steps,
|
| 140 |
+
previous_steps=previous_steps,
|
| 141 |
+
additional_context="This is a secure reasoning system with cryptographic logging."
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
# Create messages
|
| 145 |
+
messages = [
|
| 146 |
+
SystemMessage(content=prompts["system"]),
|
| 147 |
+
HumanMessage(content=prompts["user"])
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
# Call LLM
|
| 151 |
+
response = llm.invoke(messages)
|
| 152 |
+
|
| 153 |
+
# Parse JSON response
|
| 154 |
+
try:
|
| 155 |
+
safety_data = json.loads(response.content)
|
| 156 |
+
safety_result = SafetyCheckResult(**safety_data)
|
| 157 |
+
|
| 158 |
+
print(f"🔍 Safety Check Result:")
|
| 159 |
+
print(f" Is Safe: {safety_result.is_safe}")
|
| 160 |
+
print(f" Risk Level: {safety_result.risk_level}")
|
| 161 |
+
print(f" Reasoning: {safety_result.reasoning[:100]}...")
|
| 162 |
+
|
| 163 |
+
# Update state
|
| 164 |
+
state["safety_status"] = safety_result
|
| 165 |
+
state["messages"].extend([
|
| 166 |
+
HumanMessage(content=prompts["user"]),
|
| 167 |
+
AIMessage(content=response.content)
|
| 168 |
+
])
|
| 169 |
+
|
| 170 |
+
# Mark if blocked
|
| 171 |
+
if not safety_result.is_safe:
|
| 172 |
+
state["safety_blocked"] = True
|
| 173 |
+
print(f"🚫 Step BLOCKED due to safety concerns")
|
| 174 |
+
else:
|
| 175 |
+
print(f"✅ Step approved for execution")
|
| 176 |
+
|
| 177 |
+
return state
|
| 178 |
+
|
| 179 |
+
except json.JSONDecodeError as e:
|
| 180 |
+
print(f"❌ Failed to parse safety response: {e}")
|
| 181 |
+
# Default to blocking if parsing fails (fail-safe)
|
| 182 |
+
state["safety_status"] = SafetyCheckResult(
|
| 183 |
+
is_safe=False,
|
| 184 |
+
risk_level="critical",
|
| 185 |
+
reasoning=f"Safety check failed due to parsing error: {str(e)}",
|
| 186 |
+
blocked_reasons=["parsing_error"]
|
| 187 |
+
)
|
| 188 |
+
state["safety_blocked"] = True
|
| 189 |
+
return state
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ============================================================================
|
| 193 |
+
# NODE 3: EXECUTOR
|
| 194 |
+
# ============================================================================
|
| 195 |
+
|
| 196 |
+
def executor_node(state: AgentState) -> AgentState:
|
| 197 |
+
"""
|
| 198 |
+
Execute the current step (call tools if needed).
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
state: Current agent state with approved step
|
| 202 |
+
|
| 203 |
+
Returns:
|
| 204 |
+
Updated state with execution result
|
| 205 |
+
"""
|
| 206 |
+
print(f"\n{'='*60}")
|
| 207 |
+
print(f"⚡ EXECUTOR NODE - Executing step {state['current_step_index'] + 1}")
|
| 208 |
+
print(f"{'='*60}")
|
| 209 |
+
|
| 210 |
+
# Get current step
|
| 211 |
+
current_step = state["plan"].steps[state["current_step_index"]]
|
| 212 |
+
|
| 213 |
+
# Format previous results for context
|
| 214 |
+
previous_results = "None"
|
| 215 |
+
if state["justifications"]:
|
| 216 |
+
prev_results_list = [
|
| 217 |
+
f"Step {j.step_number}: {j.reasoning[:100]}..."
|
| 218 |
+
for j in state["justifications"][-3:] # Last 3 steps
|
| 219 |
+
]
|
| 220 |
+
previous_results = "\n".join(prev_results_list)
|
| 221 |
+
|
| 222 |
+
# Format the prompt
|
| 223 |
+
prompts = format_executor_prompt(
|
| 224 |
+
step_description=current_step.action,
|
| 225 |
+
task=state["task"],
|
| 226 |
+
expected_outcome=current_step.expected_outcome,
|
| 227 |
+
requires_tools=current_step.requires_tools,
|
| 228 |
+
previous_results=previous_results
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
# Create messages
|
| 232 |
+
messages = [
|
| 233 |
+
SystemMessage(content=prompts["system"]),
|
| 234 |
+
HumanMessage(content=prompts["user"])
|
| 235 |
+
]
|
| 236 |
+
|
| 237 |
+
# Call LLM
|
| 238 |
+
response = llm.invoke(messages)
|
| 239 |
+
|
| 240 |
+
# Parse JSON response
|
| 241 |
+
try:
|
| 242 |
+
executor_data = json.loads(response.content)
|
| 243 |
+
tool_needed = executor_data.get("tool_needed", "internal_reasoning")
|
| 244 |
+
tool_params = executor_data.get("tool_params")
|
| 245 |
+
direct_result = executor_data.get("direct_result")
|
| 246 |
+
|
| 247 |
+
print(f"🔧 Tool Selection: {tool_needed}")
|
| 248 |
+
|
| 249 |
+
# Execute based on tool selection
|
| 250 |
+
if tool_needed == "internal_reasoning":
|
| 251 |
+
result = ExecutionResult(
|
| 252 |
+
success=True,
|
| 253 |
+
output=direct_result or "Analysis completed through reasoning",
|
| 254 |
+
tool_calls=["internal_reasoning"]
|
| 255 |
+
)
|
| 256 |
+
else:
|
| 257 |
+
# Simulate tool execution (in real system, dispatch to actual tools)
|
| 258 |
+
result = ExecutionResult(
|
| 259 |
+
success=True,
|
| 260 |
+
output=f"Simulated result from {tool_needed} with params: {tool_params}",
|
| 261 |
+
tool_calls=[tool_needed]
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
print(f"✅ Execution successful")
|
| 265 |
+
print(f" Output: {str(result.output)[:100]}...")
|
| 266 |
+
|
| 267 |
+
# Update state
|
| 268 |
+
state["execution_result"] = result
|
| 269 |
+
state["messages"].extend([
|
| 270 |
+
HumanMessage(content=prompts["user"]),
|
| 271 |
+
AIMessage(content=response.content)
|
| 272 |
+
])
|
| 273 |
+
|
| 274 |
+
return state
|
| 275 |
+
|
| 276 |
+
except json.JSONDecodeError as e:
|
| 277 |
+
print(f"❌ Execution failed: {e}")
|
| 278 |
+
state["execution_result"] = ExecutionResult(
|
| 279 |
+
success=False,
|
| 280 |
+
output=None,
|
| 281 |
+
error=f"Failed to parse executor response: {str(e)}",
|
| 282 |
+
tool_calls=[]
|
| 283 |
+
)
|
| 284 |
+
return state
|
| 285 |
+
except Exception as e:
|
| 286 |
+
print(f"❌ Execution error: {e}")
|
| 287 |
+
state["execution_result"] = ExecutionResult(
|
| 288 |
+
success=False,
|
| 289 |
+
output=None,
|
| 290 |
+
error=str(e),
|
| 291 |
+
tool_calls=[]
|
| 292 |
+
)
|
| 293 |
+
return state
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ============================================================================
|
| 297 |
+
# NODE 4: LOGGER (Cryptographic Logging)
|
| 298 |
+
# ============================================================================
|
| 299 |
+
|
| 300 |
+
def logger_node(state: AgentState) -> AgentState:
|
| 301 |
+
"""
|
| 302 |
+
Hash the execution result and log it to Merkle Tree + WORM storage.
|
| 303 |
+
|
| 304 |
+
Args:
|
| 305 |
+
state: Current agent state with execution result
|
| 306 |
+
|
| 307 |
+
Returns:
|
| 308 |
+
Updated state with cryptographic log entry
|
| 309 |
+
"""
|
| 310 |
+
print(f"\n{'='*60}")
|
| 311 |
+
print(f"📝 LOGGER NODE - Creating cryptographic proof")
|
| 312 |
+
print(f"{'='*60}")
|
| 313 |
+
|
| 314 |
+
current_step = state["plan"].steps[state["current_step_index"]]
|
| 315 |
+
execution_result = state["execution_result"]
|
| 316 |
+
|
| 317 |
+
try:
|
| 318 |
+
# 1. Prepare the data to log
|
| 319 |
+
log_data = {
|
| 320 |
+
"task_id": state["task_id"],
|
| 321 |
+
"step_number": state["current_step_index"] + 1,
|
| 322 |
+
"action": current_step.action,
|
| 323 |
+
"result": execution_result.output if execution_result.success else execution_result.error,
|
| 324 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 325 |
+
"safety_approved": state["safety_status"].is_safe if state["safety_status"] else False
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
# 2. Hash the action data
|
| 329 |
+
hash_request = HashRequest(
|
| 330 |
+
data=json.dumps(log_data, sort_keys=True),
|
| 331 |
+
algorithm="sha256"
|
| 332 |
+
)
|
| 333 |
+
hash_response = crypto_tools.hash_tool(hash_request)
|
| 334 |
+
action_hash = hash_response.hash
|
| 335 |
+
print(f"🔐 Action Hash: {action_hash[:16]}...")
|
| 336 |
+
|
| 337 |
+
# 3. Update Merkle Tree
|
| 338 |
+
merkle_request = MerkleUpdateRequest(
|
| 339 |
+
leaf_hash=action_hash,
|
| 340 |
+
metadata={"step": state["current_step_index"] + 1}
|
| 341 |
+
)
|
| 342 |
+
merkle_response = crypto_tools.merkle_update_tool(merkle_request)
|
| 343 |
+
merkle_root = merkle_response.merkle_root
|
| 344 |
+
print(f"🌳 Merkle Root: {merkle_root[:16]}...")
|
| 345 |
+
|
| 346 |
+
# 4. Write to WORM storage
|
| 347 |
+
entry_id = f"{state['task_id']}_step_{state['current_step_index'] + 1}"
|
| 348 |
+
worm_request = WORMWriteRequest(
|
| 349 |
+
entry_id=entry_id,
|
| 350 |
+
data=log_data,
|
| 351 |
+
merkle_root=merkle_root
|
| 352 |
+
)
|
| 353 |
+
worm_response = crypto_tools.worm_write_tool(worm_request)
|
| 354 |
+
print(f"💾 WORM Path: {worm_response.storage_path}")
|
| 355 |
+
|
| 356 |
+
# 5. Create log entry
|
| 357 |
+
log_entry = CryptoLogEntry(
|
| 358 |
+
step_number=state["current_step_index"] + 1,
|
| 359 |
+
action_hash=action_hash,
|
| 360 |
+
merkle_root=merkle_root,
|
| 361 |
+
worm_path=worm_response.storage_path
|
| 362 |
+
)
|
| 363 |
+
|
| 364 |
+
# Update state
|
| 365 |
+
state["logs"].append(log_entry)
|
| 366 |
+
print(f"✅ Cryptographic logging complete")
|
| 367 |
+
|
| 368 |
+
return state
|
| 369 |
+
|
| 370 |
+
except Exception as e:
|
| 371 |
+
print(f"❌ Logging failed: {e}")
|
| 372 |
+
state["error"] = f"Cryptographic logging failed: {str(e)}"
|
| 373 |
+
return state
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
# ============================================================================
|
| 377 |
+
# NODE 5: JUSTIFICATION
|
| 378 |
+
# ============================================================================
|
| 379 |
+
|
| 380 |
+
def justification_node(state: AgentState) -> AgentState:
|
| 381 |
+
"""
|
| 382 |
+
Generate an explanation for why the action was taken.
|
| 383 |
+
|
| 384 |
+
Args:
|
| 385 |
+
state: Current agent state with execution result
|
| 386 |
+
|
| 387 |
+
Returns:
|
| 388 |
+
Updated state with justification
|
| 389 |
+
"""
|
| 390 |
+
print(f"\n{'='*60}")
|
| 391 |
+
print(f"💭 JUSTIFICATION NODE - Explaining the action")
|
| 392 |
+
print(f"{'='*60}")
|
| 393 |
+
|
| 394 |
+
current_step = state["plan"].steps[state["current_step_index"]]
|
| 395 |
+
execution_result = state["execution_result"]
|
| 396 |
+
|
| 397 |
+
# Determine tool used
|
| 398 |
+
tool_used = ", ".join(execution_result.tool_calls) if execution_result.tool_calls else "none"
|
| 399 |
+
|
| 400 |
+
# Format the prompt
|
| 401 |
+
prompts = format_justification_prompt(
|
| 402 |
+
step_description=current_step.action,
|
| 403 |
+
tool_used=tool_used,
|
| 404 |
+
execution_result=str(execution_result.output)[:500] if execution_result.success else execution_result.error,
|
| 405 |
+
task=state["task"],
|
| 406 |
+
step_number=state["current_step_index"] + 1,
|
| 407 |
+
total_steps=state["plan"].total_steps,
|
| 408 |
+
expected_outcome=current_step.expected_outcome
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
# Create messages
|
| 412 |
+
messages = [
|
| 413 |
+
SystemMessage(content=prompts["system"]),
|
| 414 |
+
HumanMessage(content=prompts["user"])
|
| 415 |
+
]
|
| 416 |
+
|
| 417 |
+
# Call LLM
|
| 418 |
+
response = llm.invoke(messages)
|
| 419 |
+
|
| 420 |
+
# Parse JSON response
|
| 421 |
+
try:
|
| 422 |
+
justification_data = json.loads(response.content)
|
| 423 |
+
justification = Justification(**justification_data)
|
| 424 |
+
|
| 425 |
+
print(f"📋 Justification generated:")
|
| 426 |
+
print(f" {justification.reasoning[:150]}...")
|
| 427 |
+
|
| 428 |
+
# Update state
|
| 429 |
+
state["justifications"].append(justification)
|
| 430 |
+
state["messages"].extend([
|
| 431 |
+
HumanMessage(content=prompts["user"]),
|
| 432 |
+
AIMessage(content=response.content)
|
| 433 |
+
])
|
| 434 |
+
|
| 435 |
+
return state
|
| 436 |
+
|
| 437 |
+
except json.JSONDecodeError as e:
|
| 438 |
+
print(f"⚠️ Failed to parse justification, using fallback: {e}")
|
| 439 |
+
# Create fallback justification
|
| 440 |
+
fallback = Justification(
|
| 441 |
+
step_number=state["current_step_index"] + 1,
|
| 442 |
+
reasoning=f"Executed {current_step.action} as planned. Result: {execution_result.success}",
|
| 443 |
+
evidence=None,
|
| 444 |
+
alternatives_considered=None
|
| 445 |
+
)
|
| 446 |
+
state["justifications"].append(fallback)
|
| 447 |
+
return state
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
# ============================================================================
|
| 451 |
+
# NODE 6: STEP ITERATOR
|
| 452 |
+
# ============================================================================
|
| 453 |
+
|
| 454 |
+
def step_iterator_node(state: AgentState) -> AgentState:
|
| 455 |
+
"""
|
| 456 |
+
Move to the next step or complete the task.
|
| 457 |
+
|
| 458 |
+
Args:
|
| 459 |
+
state: Current agent state
|
| 460 |
+
|
| 461 |
+
Returns:
|
| 462 |
+
Updated state with incremented step index
|
| 463 |
+
"""
|
| 464 |
+
print(f"\n{'='*60}")
|
| 465 |
+
print(f"➡️ STEP ITERATOR - Moving to next step")
|
| 466 |
+
print(f"{'='*60}")
|
| 467 |
+
|
| 468 |
+
# Increment step index
|
| 469 |
+
state["current_step_index"] += 1
|
| 470 |
+
|
| 471 |
+
# Check if we're done
|
| 472 |
+
if state["current_step_index"] >= state["plan"].total_steps:
|
| 473 |
+
print(f"🎉 All steps completed!")
|
| 474 |
+
state["status"] = "completed"
|
| 475 |
+
else:
|
| 476 |
+
print(f"📍 Moving to step {state['current_step_index'] + 1}/{state['plan'].total_steps}")
|
| 477 |
+
|
| 478 |
+
return state
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
# ============================================================================
|
| 482 |
+
# NODE 7: REFINER (for unsafe steps)
|
| 483 |
+
# ============================================================================
|
| 484 |
+
|
| 485 |
+
def refiner_node(state: AgentState) -> AgentState:
|
| 486 |
+
"""
|
| 487 |
+
Handle unsafe steps by modifying or skipping them.
|
| 488 |
+
|
| 489 |
+
Args:
|
| 490 |
+
state: Current agent state with blocked step
|
| 491 |
+
|
| 492 |
+
Returns:
|
| 493 |
+
Updated state with refinement decision
|
| 494 |
+
"""
|
| 495 |
+
print(f"\n{'='*60}")
|
| 496 |
+
print(f"🔧 REFINER NODE - Handling unsafe step")
|
| 497 |
+
print(f"{'='*60}")
|
| 498 |
+
|
| 499 |
+
current_step = state["plan"].steps[state["current_step_index"]]
|
| 500 |
+
safety_status = state["safety_status"]
|
| 501 |
+
|
| 502 |
+
# Log the blocked action
|
| 503 |
+
print(f"🚫 Step blocked: {current_step.action}")
|
| 504 |
+
print(f" Reason: {safety_status.reasoning}")
|
| 505 |
+
|
| 506 |
+
# Create a null execution result
|
| 507 |
+
state["execution_result"] = ExecutionResult(
|
| 508 |
+
success=False,
|
| 509 |
+
output=None,
|
| 510 |
+
error=f"Step blocked by safety guardrails: {safety_status.reasoning}",
|
| 511 |
+
tool_calls=[]
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
# Create justification for blocking
|
| 515 |
+
justification = Justification(
|
| 516 |
+
step_number=state["current_step_index"] + 1,
|
| 517 |
+
reasoning=f"Step was blocked by safety guardrails. Risk level: {safety_status.risk_level}. Reason: {safety_status.reasoning}",
|
| 518 |
+
evidence=safety_status.blocked_reasons or [],
|
| 519 |
+
alternatives_considered=["Skip this step", "Abort entire task"]
|
| 520 |
+
)
|
| 521 |
+
state["justifications"].append(justification)
|
| 522 |
+
|
| 523 |
+
# Mark status
|
| 524 |
+
state["status"] = "blocked"
|
| 525 |
+
|
| 526 |
+
print(f"⚠️ Task blocked due to safety concerns")
|
| 527 |
+
|
| 528 |
+
return state
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
# ============================================================================
|
| 532 |
+
# CONDITIONAL EDGES
|
| 533 |
+
# ============================================================================
|
| 534 |
+
|
| 535 |
+
def should_execute_or_refine(state: AgentState) -> Literal["execute", "refine"]:
|
| 536 |
+
"""
|
| 537 |
+
Decide whether to execute or refine based on safety check.
|
| 538 |
+
|
| 539 |
+
Args:
|
| 540 |
+
state: Current agent state
|
| 541 |
+
|
| 542 |
+
Returns:
|
| 543 |
+
"execute" if safe, "refine" if unsafe
|
| 544 |
+
"""
|
| 545 |
+
if state["safety_status"] and state["safety_status"].is_safe:
|
| 546 |
+
return "execute"
|
| 547 |
+
else:
|
| 548 |
+
return "refine"
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
def should_continue_or_end(state: AgentState) -> Literal["continue", "end"]:
|
| 552 |
+
"""
|
| 553 |
+
Decide whether to continue to next step or end the workflow.
|
| 554 |
+
|
| 555 |
+
Args:
|
| 556 |
+
state: Current agent state
|
| 557 |
+
|
| 558 |
+
Returns:
|
| 559 |
+
"continue" if more steps remain, "end" if done or blocked
|
| 560 |
+
"""
|
| 561 |
+
# End if blocked
|
| 562 |
+
if state["safety_blocked"] and state["status"] == "blocked":
|
| 563 |
+
return "end"
|
| 564 |
+
|
| 565 |
+
# End if error occurred
|
| 566 |
+
if state["error"]:
|
| 567 |
+
return "end"
|
| 568 |
+
|
| 569 |
+
# End if all steps completed
|
| 570 |
+
if state["current_step_index"] >= state["plan"].total_steps:
|
| 571 |
+
return "end"
|
| 572 |
+
|
| 573 |
+
# Continue to next step
|
| 574 |
+
return "continue"
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
# ============================================================================
|
| 578 |
+
# GRAPH CONSTRUCTION
|
| 579 |
+
# ============================================================================
|
| 580 |
+
|
| 581 |
+
def create_reasoning_graph() -> StateGraph:
|
| 582 |
+
"""
|
| 583 |
+
Construct the full LangGraph state machine.
|
| 584 |
+
|
| 585 |
+
Returns:
|
| 586 |
+
Compiled StateGraph ready for execution
|
| 587 |
+
"""
|
| 588 |
+
# Create the graph
|
| 589 |
+
workflow = StateGraph(AgentState)
|
| 590 |
+
|
| 591 |
+
# Add nodes
|
| 592 |
+
workflow.add_node("planner", planner_node)
|
| 593 |
+
workflow.add_node("safety", safety_node)
|
| 594 |
+
workflow.add_node("executor", executor_node)
|
| 595 |
+
workflow.add_node("logger", logger_node)
|
| 596 |
+
workflow.add_node("justification", justification_node)
|
| 597 |
+
workflow.add_node("iterator", step_iterator_node)
|
| 598 |
+
workflow.add_node("refiner", refiner_node)
|
| 599 |
+
|
| 600 |
+
# Set entry point
|
| 601 |
+
workflow.set_entry_point("planner")
|
| 602 |
+
|
| 603 |
+
# Add edges
|
| 604 |
+
workflow.add_edge("planner", "safety")
|
| 605 |
+
|
| 606 |
+
# Conditional: safe -> execute, unsafe -> refine
|
| 607 |
+
workflow.add_conditional_edges(
|
| 608 |
+
"safety",
|
| 609 |
+
should_execute_or_refine,
|
| 610 |
+
{
|
| 611 |
+
"execute": "executor",
|
| 612 |
+
"refine": "refiner"
|
| 613 |
+
}
|
| 614 |
+
)
|
| 615 |
+
|
| 616 |
+
# After execution: log -> justify -> iterate
|
| 617 |
+
workflow.add_edge("executor", "logger")
|
| 618 |
+
workflow.add_edge("logger", "justification")
|
| 619 |
+
workflow.add_edge("justification", "iterator")
|
| 620 |
+
|
| 621 |
+
# After refining: go to iterator (to mark as done)
|
| 622 |
+
workflow.add_edge("refiner", "iterator")
|
| 623 |
+
|
| 624 |
+
# Conditional: continue to next step or end
|
| 625 |
+
workflow.add_conditional_edges(
|
| 626 |
+
"iterator",
|
| 627 |
+
should_continue_or_end,
|
| 628 |
+
{
|
| 629 |
+
"continue": "safety", # Loop back to safety check for next step
|
| 630 |
+
"end": END
|
| 631 |
+
}
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
# Compile the graph
|
| 635 |
+
return workflow.compile()
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
# ============================================================================
|
| 639 |
+
# CONVENIENCE FUNCTION
|
| 640 |
+
# ============================================================================
|
| 641 |
+
|
| 642 |
+
def run_reasoning_task(task: str, task_id: str, user_id: str = None) -> AgentState:
|
| 643 |
+
"""
|
| 644 |
+
Execute a reasoning task through the full pipeline.
|
| 645 |
+
|
| 646 |
+
Args:
|
| 647 |
+
task: The task to solve
|
| 648 |
+
task_id: Unique identifier for this execution
|
| 649 |
+
user_id: Optional user identifier
|
| 650 |
+
|
| 651 |
+
Returns:
|
| 652 |
+
Final agent state with results and logs
|
| 653 |
+
"""
|
| 654 |
+
from state import create_initial_state
|
| 655 |
+
|
| 656 |
+
# Create initial state
|
| 657 |
+
initial_state = create_initial_state(task, task_id, user_id)
|
| 658 |
+
|
| 659 |
+
# Create and run the graph
|
| 660 |
+
graph = create_reasoning_graph()
|
| 661 |
+
|
| 662 |
+
print(f"\n{'#'*60}")
|
| 663 |
+
print(f"🚀 STARTING REASONING PIPELINE")
|
| 664 |
+
print(f" Task: {task}")
|
| 665 |
+
print(f" Task ID: {task_id}")
|
| 666 |
+
print(f"{'#'*60}")
|
| 667 |
+
|
| 668 |
+
# Execute
|
| 669 |
+
final_state = graph.invoke(initial_state)
|
| 670 |
+
|
| 671 |
+
print(f"\n{'#'*60}")
|
| 672 |
+
print(f"🏁 REASONING PIPELINE COMPLETE")
|
| 673 |
+
print(f" Status: {final_state['status']}")
|
| 674 |
+
print(f" Steps Executed: {len(final_state['justifications'])}/{final_state['plan'].total_steps if final_state['plan'] else 0}")
|
| 675 |
+
print(f" Cryptographic Logs: {len(final_state['logs'])}")
|
| 676 |
+
print(f"{'#'*60}\n")
|
| 677 |
+
|
| 678 |
+
return final_state
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
# ============================================================================
|
| 682 |
+
# EXAMPLE USAGE
|
| 683 |
+
# ============================================================================
|
| 684 |
+
|
| 685 |
+
if __name__ == "__main__":
|
| 686 |
+
# Test the graph
|
| 687 |
+
result = run_reasoning_task(
|
| 688 |
+
task="Analyze the current state of AI safety research and provide 3 key findings",
|
| 689 |
+
task_id="test_001",
|
| 690 |
+
user_id="demo_user"
|
| 691 |
+
)
|
| 692 |
+
|
| 693 |
+
# Print results
|
| 694 |
+
print("\n=== FINAL RESULTS ===")
|
| 695 |
+
print(f"Status: {result['status']}")
|
| 696 |
+
print(f"\nJustifications:")
|
| 697 |
+
for j in result['justifications']:
|
| 698 |
+
print(f" Step {j.step_number}: {j.reasoning[:100]}...")
|
| 699 |
+
|
| 700 |
+
print(f"\nCryptographic Audit Trail:")
|
| 701 |
+
for log in result['logs']:
|
| 702 |
+
print(f" Step {log.step_number}: Hash {log.action_hash[:16]}... -> Root {log.merkle_root[:16]}...")
|
mock_tools.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mock Crypto Tools for Secure Reasoning MCP Server
|
| 3 |
+
Provides mock implementations for development and testing.
|
| 4 |
+
Replace with real implementations when connecting to actual MCP server.
|
| 5 |
+
"""
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Optional, Dict, Any, List
|
| 10 |
+
|
| 11 |
+
from schemas import (
|
| 12 |
+
HashRequest, HashResponse,
|
| 13 |
+
MerkleUpdateRequest, MerkleUpdateResponse,
|
| 14 |
+
WORMWriteRequest, WORMWriteResponse
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MockMerkleTree:
|
| 19 |
+
"""
|
| 20 |
+
In-memory Merkle Tree for mock operations.
|
| 21 |
+
"""
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.leaves: List[str] = []
|
| 24 |
+
self.root: Optional[str] = None
|
| 25 |
+
|
| 26 |
+
def _calculate_root(self, leaves: List[str]) -> Optional[str]:
|
| 27 |
+
"""Calculate Merkle root from a list of leaves."""
|
| 28 |
+
if not leaves:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
if len(leaves) == 1:
|
| 32 |
+
return leaves[0]
|
| 33 |
+
|
| 34 |
+
current_level = leaves[:]
|
| 35 |
+
|
| 36 |
+
while len(current_level) > 1:
|
| 37 |
+
next_level = []
|
| 38 |
+
for i in range(0, len(current_level), 2):
|
| 39 |
+
left = current_level[i]
|
| 40 |
+
right = current_level[i + 1] if i + 1 < len(current_level) else left
|
| 41 |
+
combined = hashlib.sha256((left + right).encode()).hexdigest()
|
| 42 |
+
next_level.append(combined)
|
| 43 |
+
current_level = next_level
|
| 44 |
+
|
| 45 |
+
return current_level[0]
|
| 46 |
+
|
| 47 |
+
def update(self, new_hash: str) -> str:
|
| 48 |
+
"""Add a new leaf and recalculate the Merkle root."""
|
| 49 |
+
self.leaves.append(new_hash)
|
| 50 |
+
self.root = self._calculate_root(self.leaves)
|
| 51 |
+
return self.root
|
| 52 |
+
|
| 53 |
+
def get_proof(self, index: int) -> List[str]:
|
| 54 |
+
"""Generate Merkle proof for a leaf at given index."""
|
| 55 |
+
if index < 0 or index >= len(self.leaves):
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
proof = []
|
| 59 |
+
current_index = index
|
| 60 |
+
current_level = self.leaves[:]
|
| 61 |
+
|
| 62 |
+
while len(current_level) > 1:
|
| 63 |
+
is_right = current_index % 2 == 1
|
| 64 |
+
sibling_index = current_index - 1 if is_right else current_index + 1
|
| 65 |
+
|
| 66 |
+
if sibling_index < len(current_level):
|
| 67 |
+
proof.append(current_level[sibling_index])
|
| 68 |
+
|
| 69 |
+
current_index = current_index // 2
|
| 70 |
+
next_level = []
|
| 71 |
+
for i in range(0, len(current_level), 2):
|
| 72 |
+
left = current_level[i]
|
| 73 |
+
right = current_level[i + 1] if i + 1 < len(current_level) else left
|
| 74 |
+
combined = hashlib.sha256((left + right).encode()).hexdigest()
|
| 75 |
+
next_level.append(combined)
|
| 76 |
+
current_level = next_level
|
| 77 |
+
|
| 78 |
+
return proof
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class MockCryptoTools:
|
| 82 |
+
"""
|
| 83 |
+
Mock implementations of cryptographic tools.
|
| 84 |
+
Provides in-memory versions of hash, Merkle tree, and WORM storage.
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(self):
|
| 88 |
+
self.merkle_tree = MockMerkleTree()
|
| 89 |
+
self.worm_storage: Dict[str, Dict[str, Any]] = {}
|
| 90 |
+
self.storage_counter = 0
|
| 91 |
+
|
| 92 |
+
def hash_tool(self, request: HashRequest) -> HashResponse:
|
| 93 |
+
"""
|
| 94 |
+
Hash data using SHA-256 (or specified algorithm).
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
request: HashRequest with data and algorithm
|
| 98 |
+
|
| 99 |
+
Returns:
|
| 100 |
+
HashResponse with the hash digest
|
| 101 |
+
"""
|
| 102 |
+
data = request.data
|
| 103 |
+
|
| 104 |
+
# Ensure deterministic serialization
|
| 105 |
+
if isinstance(data, dict):
|
| 106 |
+
data_str = json.dumps(data, sort_keys=True)
|
| 107 |
+
else:
|
| 108 |
+
data_str = str(data)
|
| 109 |
+
|
| 110 |
+
# Compute hash (only SHA-256 implemented for mock)
|
| 111 |
+
hash_value = hashlib.sha256(data_str.encode()).hexdigest()
|
| 112 |
+
|
| 113 |
+
return HashResponse(
|
| 114 |
+
hash=hash_value,
|
| 115 |
+
algorithm=request.algorithm,
|
| 116 |
+
timestamp=datetime.utcnow()
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
def merkle_update_tool(self, request: MerkleUpdateRequest) -> MerkleUpdateResponse:
|
| 120 |
+
"""
|
| 121 |
+
Add a hash to the Merkle tree and return updated root.
|
| 122 |
+
|
| 123 |
+
Args:
|
| 124 |
+
request: MerkleUpdateRequest with leaf hash
|
| 125 |
+
|
| 126 |
+
Returns:
|
| 127 |
+
MerkleUpdateResponse with new root and proof
|
| 128 |
+
"""
|
| 129 |
+
# Add leaf and get new root
|
| 130 |
+
new_root = self.merkle_tree.update(request.leaf_hash)
|
| 131 |
+
leaf_index = len(self.merkle_tree.leaves) - 1
|
| 132 |
+
|
| 133 |
+
# Generate proof for the new leaf
|
| 134 |
+
proof = self.merkle_tree.get_proof(leaf_index)
|
| 135 |
+
|
| 136 |
+
return MerkleUpdateResponse(
|
| 137 |
+
merkle_root=new_root,
|
| 138 |
+
leaf_index=leaf_index,
|
| 139 |
+
proof=proof,
|
| 140 |
+
tree_size=len(self.merkle_tree.leaves)
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
def worm_write_tool(self, request: WORMWriteRequest) -> WORMWriteResponse:
|
| 144 |
+
"""
|
| 145 |
+
Write data to WORM (Write Once, Read Many) storage.
|
| 146 |
+
|
| 147 |
+
Args:
|
| 148 |
+
request: WORMWriteRequest with entry ID and data
|
| 149 |
+
|
| 150 |
+
Returns:
|
| 151 |
+
WORMWriteResponse with storage confirmation
|
| 152 |
+
"""
|
| 153 |
+
# Check if entry already exists (WORM = no overwrites)
|
| 154 |
+
if request.entry_id in self.worm_storage:
|
| 155 |
+
return WORMWriteResponse(
|
| 156 |
+
success=False,
|
| 157 |
+
storage_path=f"worm://{request.entry_id}",
|
| 158 |
+
verification_hash="",
|
| 159 |
+
timestamp=datetime.utcnow()
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Store the data
|
| 163 |
+
self.storage_counter += 1
|
| 164 |
+
storage_path = f"worm://mock/{self.storage_counter}/{request.entry_id}"
|
| 165 |
+
|
| 166 |
+
# Compute verification hash
|
| 167 |
+
verification_hash = hashlib.sha256(
|
| 168 |
+
json.dumps(request.data, sort_keys=True).encode()
|
| 169 |
+
).hexdigest()
|
| 170 |
+
|
| 171 |
+
# Store immutably
|
| 172 |
+
self.worm_storage[request.entry_id] = {
|
| 173 |
+
"data": request.data,
|
| 174 |
+
"merkle_root": request.merkle_root,
|
| 175 |
+
"verification_hash": verification_hash,
|
| 176 |
+
"storage_path": storage_path,
|
| 177 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
return WORMWriteResponse(
|
| 181 |
+
success=True,
|
| 182 |
+
storage_path=storage_path,
|
| 183 |
+
verification_hash=verification_hash,
|
| 184 |
+
timestamp=datetime.utcnow()
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
def worm_read_tool(self, entry_id: str) -> Optional[Dict[str, Any]]:
|
| 188 |
+
"""
|
| 189 |
+
Read data from WORM storage (for verification).
|
| 190 |
+
|
| 191 |
+
Args:
|
| 192 |
+
entry_id: The ID of the entry to read
|
| 193 |
+
|
| 194 |
+
Returns:
|
| 195 |
+
The stored data or None if not found
|
| 196 |
+
"""
|
| 197 |
+
return self.worm_storage.get(entry_id)
|
| 198 |
+
|
| 199 |
+
def verify_proof(self, target_hash: str, proof: List[str], root: str) -> bool:
|
| 200 |
+
"""
|
| 201 |
+
Verify a Merkle proof.
|
| 202 |
+
|
| 203 |
+
Args:
|
| 204 |
+
target_hash: The hash to verify
|
| 205 |
+
proof: List of sibling hashes
|
| 206 |
+
root: Expected Merkle root
|
| 207 |
+
|
| 208 |
+
Returns:
|
| 209 |
+
True if proof is valid, False otherwise
|
| 210 |
+
"""
|
| 211 |
+
current_hash = target_hash
|
| 212 |
+
|
| 213 |
+
for sibling_hash in proof:
|
| 214 |
+
# Combine in lexicographic order for consistency
|
| 215 |
+
if current_hash < sibling_hash:
|
| 216 |
+
combined = current_hash + sibling_hash
|
| 217 |
+
else:
|
| 218 |
+
combined = sibling_hash + current_hash
|
| 219 |
+
current_hash = hashlib.sha256(combined.encode()).hexdigest()
|
| 220 |
+
|
| 221 |
+
return current_hash == root
|
prompts.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt Templates for Secure Reasoning MCP Server
|
| 3 |
+
Optimized for Claude 3.5 Sonnet with strict JSON output requirements.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
# ============================================================================
|
| 7 |
+
# PLANNER PROMPT
|
| 8 |
+
# ============================================================================
|
| 9 |
+
|
| 10 |
+
PLANNER_SYSTEM_PROMPT = """You are a strategic planning agent for a secure reasoning system. Your role is to break down complex tasks into clear, executable steps.
|
| 11 |
+
|
| 12 |
+
**Your Responsibilities:**
|
| 13 |
+
1. Analyze the user's task thoroughly
|
| 14 |
+
2. Create a step-by-step execution plan
|
| 15 |
+
3. Identify which steps require external tools
|
| 16 |
+
4. Ensure steps are atomic (one clear action per step)
|
| 17 |
+
5. Order steps logically with dependencies respected
|
| 18 |
+
|
| 19 |
+
**Output Format:**
|
| 20 |
+
You MUST respond with valid JSON only, no preamble or explanation. Use this exact structure:
|
| 21 |
+
|
| 22 |
+
{
|
| 23 |
+
"steps": [
|
| 24 |
+
{
|
| 25 |
+
"step_number": 1,
|
| 26 |
+
"action": "Clear description of what to do",
|
| 27 |
+
"expected_outcome": "What this step should achieve",
|
| 28 |
+
"requires_tools": true
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"total_steps": 3
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
**Guidelines:**
|
| 35 |
+
- Break complex tasks into 3-7 steps (avoid over-fragmenting)
|
| 36 |
+
- Each step should be verifiable and have a clear completion criterion
|
| 37 |
+
- Mark `requires_tools: true` if the step needs data retrieval, computation, or external API calls
|
| 38 |
+
- Mark `requires_tools: false` for pure reasoning, analysis, or synthesis steps
|
| 39 |
+
- Consider safety: avoid steps that could cause harm or violate ethics
|
| 40 |
+
- Be specific: "Search for X" not "Find information"
|
| 41 |
+
|
| 42 |
+
**Examples of Good Steps:**
|
| 43 |
+
✓ "Search web for current S&P 500 price and extract the value"
|
| 44 |
+
✓ "Analyze the retrieved data to identify the trend over last 30 days"
|
| 45 |
+
✓ "Synthesize findings into a 3-sentence summary for the user"
|
| 46 |
+
|
| 47 |
+
**Examples of Bad Steps:**
|
| 48 |
+
✗ "Do research" (too vague)
|
| 49 |
+
✗ "Get data and analyze it" (multiple actions)
|
| 50 |
+
✗ "Figure out the answer" (not actionable)"""
|
| 51 |
+
|
| 52 |
+
PLANNER_USER_TEMPLATE = """Task: {task}
|
| 53 |
+
|
| 54 |
+
Please generate a detailed execution plan for this task. Return ONLY valid JSON, nothing else."""
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ============================================================================
|
| 58 |
+
# SAFETY GUARDRAIL PROMPT
|
| 59 |
+
# ============================================================================
|
| 60 |
+
|
| 61 |
+
SAFETY_SYSTEM_PROMPT = """You are a safety validation system for an AI reasoning agent. Your role is to prevent harmful, unethical, or dangerous actions BEFORE they are executed.
|
| 62 |
+
|
| 63 |
+
**Your Responsibilities:**
|
| 64 |
+
1. Evaluate if a proposed action is safe to execute
|
| 65 |
+
2. Assess potential risks across multiple dimensions
|
| 66 |
+
3. Block actions that could cause harm
|
| 67 |
+
4. Provide clear reasoning for safety decisions
|
| 68 |
+
|
| 69 |
+
**Risk Assessment Dimensions:**
|
| 70 |
+
- **Harm to People**: Physical, psychological, or financial harm
|
| 71 |
+
- **Privacy Violations**: Unauthorized data access or surveillance
|
| 72 |
+
- **Security Risks**: Hacking, exploits, or system compromise
|
| 73 |
+
- **Misinformation**: Generating or spreading false information
|
| 74 |
+
- **Illegal Activities**: Actions that violate laws or regulations
|
| 75 |
+
- **Ethical Concerns**: Manipulation, deception, or unfair bias
|
| 76 |
+
- **Discrimination**: Actions that target protected groups
|
| 77 |
+
|
| 78 |
+
**Output Format:**
|
| 79 |
+
You MUST respond with valid JSON only. Use this exact structure:
|
| 80 |
+
|
| 81 |
+
{
|
| 82 |
+
"is_safe": true,
|
| 83 |
+
"risk_level": "safe",
|
| 84 |
+
"reasoning": "Detailed explanation of the safety assessment",
|
| 85 |
+
"blocked_reasons": []
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
**Risk Levels:**
|
| 89 |
+
- `"safe"`: No concerns, proceed with execution
|
| 90 |
+
- `"low"`: Minor concerns but acceptable with monitoring
|
| 91 |
+
- `"medium"`: Moderate risk, requires careful handling
|
| 92 |
+
- `"high"`: Significant risk, should be avoided
|
| 93 |
+
- `"critical"`: Severe risk, must be blocked immediately
|
| 94 |
+
|
| 95 |
+
**Decision Rules:**
|
| 96 |
+
- If `risk_level` is "high" or "critical", set `is_safe: false`
|
| 97 |
+
- If `risk_level` is "medium", use judgment based on context
|
| 98 |
+
- If `risk_level` is "safe" or "low", set `is_safe: true`
|
| 99 |
+
- Always populate `blocked_reasons` array when `is_safe: false`
|
| 100 |
+
|
| 101 |
+
**Approval Bias:**
|
| 102 |
+
Default to allowing actions unless there is a clear safety concern. The system should enable legitimate tasks while protecting against harm.
|
| 103 |
+
|
| 104 |
+
**Examples:**
|
| 105 |
+
|
| 106 |
+
✓ SAFE: "Search for Python documentation on list comprehensions"
|
| 107 |
+
✓ SAFE: "Calculate the average of [1, 2, 3, 4, 5]"
|
| 108 |
+
✓ SAFE: "Summarize the key findings from the retrieved article"
|
| 109 |
+
|
| 110 |
+
✗ UNSAFE: "Generate a phishing email template"
|
| 111 |
+
✗ UNSAFE: "Find personal contact information for [person]"
|
| 112 |
+
✗ UNSAFE: "Create instructions for synthesizing harmful substances"
|
| 113 |
+
✗ UNSAFE: "Search for ways to bypass security systems"
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
SAFETY_USER_TEMPLATE = """**Step to Validate:**
|
| 117 |
+
{step_description}
|
| 118 |
+
|
| 119 |
+
**Full Context:**
|
| 120 |
+
- Task: {task}
|
| 121 |
+
- Step Number: {step_number} of {total_steps}
|
| 122 |
+
- Previous Steps: {previous_steps}
|
| 123 |
+
|
| 124 |
+
**Additional Context:**
|
| 125 |
+
{additional_context}
|
| 126 |
+
|
| 127 |
+
Please evaluate if this step is safe to execute. Return ONLY valid JSON."""
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ============================================================================
|
| 131 |
+
# EXECUTOR/ROUTER PROMPT
|
| 132 |
+
# ============================================================================
|
| 133 |
+
|
| 134 |
+
EXECUTOR_SYSTEM_PROMPT = """You are an intelligent action executor for a reasoning system. Your role is to execute approved steps and determine which tools (if any) are needed.
|
| 135 |
+
|
| 136 |
+
**Available Tools:**
|
| 137 |
+
1. **web_search**: Search the internet for current information
|
| 138 |
+
2. **web_fetch**: Retrieve full content from a specific URL
|
| 139 |
+
3. **calculate**: Perform mathematical computations
|
| 140 |
+
4. **code_execute**: Run Python code in a sandbox
|
| 141 |
+
5. **internal_reasoning**: Use pure reasoning without external tools
|
| 142 |
+
|
| 143 |
+
**Your Responsibilities:**
|
| 144 |
+
1. Determine which tool best accomplishes the step
|
| 145 |
+
2. Extract the specific parameters needed for the tool
|
| 146 |
+
3. Execute the action or call the appropriate tool
|
| 147 |
+
4. Return structured results
|
| 148 |
+
|
| 149 |
+
**Output Format:**
|
| 150 |
+
You MUST respond with valid JSON only:
|
| 151 |
+
|
| 152 |
+
{
|
| 153 |
+
"tool_needed": "web_search",
|
| 154 |
+
"tool_params": {
|
| 155 |
+
"query": "specific search query"
|
| 156 |
+
},
|
| 157 |
+
"reasoning": "Why this tool was selected"
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
OR if no external tool is needed:
|
| 161 |
+
|
| 162 |
+
{
|
| 163 |
+
"tool_needed": "internal_reasoning",
|
| 164 |
+
"tool_params": null,
|
| 165 |
+
"reasoning": "This can be solved through analysis alone",
|
| 166 |
+
"direct_result": "The answer or analysis"
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
**Tool Selection Guidelines:**
|
| 170 |
+
- Use `web_search` for: current events, real-time data, factual lookups
|
| 171 |
+
- Use `web_fetch` for: retrieving specific documents or web pages
|
| 172 |
+
- Use `calculate` for: mathematical operations, data analysis
|
| 173 |
+
- Use `code_execute` for: complex computations, data transformations
|
| 174 |
+
- Use `internal_reasoning` for: analysis, synthesis, planning, summarization
|
| 175 |
+
|
| 176 |
+
**Important:**
|
| 177 |
+
- Choose the MINIMAL tool necessary (don't over-engineer)
|
| 178 |
+
- Be specific with parameters (exact search terms, precise calculations)
|
| 179 |
+
- If a step can be done without tools, use `internal_reasoning`"""
|
| 180 |
+
|
| 181 |
+
EXECUTOR_USER_TEMPLATE = """**Step to Execute:**
|
| 182 |
+
{step_description}
|
| 183 |
+
|
| 184 |
+
**Context:**
|
| 185 |
+
- Task: {task}
|
| 186 |
+
- Expected Outcome: {expected_outcome}
|
| 187 |
+
- Requires Tools: {requires_tools}
|
| 188 |
+
- Previous Results: {previous_results}
|
| 189 |
+
|
| 190 |
+
Determine how to execute this step and return the appropriate JSON structure."""
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
# ============================================================================
|
| 194 |
+
# JUSTIFICATION PROMPT
|
| 195 |
+
# ============================================================================
|
| 196 |
+
|
| 197 |
+
JUSTIFICATION_SYSTEM_PROMPT = """You are a transparency and explainability agent. Your role is to explain WHY actions were taken in clear, understandable language.
|
| 198 |
+
|
| 199 |
+
**Your Responsibilities:**
|
| 200 |
+
1. Explain the reasoning behind the executed action
|
| 201 |
+
2. Connect the action to the overall task goal
|
| 202 |
+
3. Cite specific evidence or data that informed the decision
|
| 203 |
+
4. Note any alternative approaches that were considered
|
| 204 |
+
5. Make the reasoning transparent and auditable
|
| 205 |
+
|
| 206 |
+
**Output Format:**
|
| 207 |
+
You MUST respond with valid JSON only:
|
| 208 |
+
|
| 209 |
+
{
|
| 210 |
+
"step_number": 1,
|
| 211 |
+
"reasoning": "Clear natural language explanation of why this action was taken",
|
| 212 |
+
"evidence": [
|
| 213 |
+
"Specific fact or data point that supported this decision",
|
| 214 |
+
"Another supporting piece of evidence"
|
| 215 |
+
],
|
| 216 |
+
"alternatives_considered": [
|
| 217 |
+
"Alternative approach 1 and why it wasn't chosen",
|
| 218 |
+
"Alternative approach 2 and why it wasn't chosen"
|
| 219 |
+
]
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
**Explanation Guidelines:**
|
| 223 |
+
- Write for a technical but non-expert audience
|
| 224 |
+
- Be specific: cite actual data, tool outputs, or reasoning steps
|
| 225 |
+
- Connect each action to the broader task goal
|
| 226 |
+
- Acknowledge uncertainty when present
|
| 227 |
+
- Explain trade-offs in the decision-making process
|
| 228 |
+
|
| 229 |
+
**Good Justifications:**
|
| 230 |
+
✓ "Used web_search because the task requires current S&P 500 price (data changes daily). Retrieved price of $6,852.34 from reliable financial source. Alternative of using cached data was rejected due to staleness risk."
|
| 231 |
+
|
| 232 |
+
✓ "Applied internal_reasoning to synthesize findings because the step requires analysis of existing data, not new information retrieval. Combined results from steps 1-3 to identify the trend pattern. Alternative of using code_execute would be over-engineering for this simple synthesis task."
|
| 233 |
+
|
| 234 |
+
**Bad Justifications:**
|
| 235 |
+
✗ "Performed the action." (no explanation)
|
| 236 |
+
✗ "It seemed like the right thing to do." (vague)
|
| 237 |
+
✗ "The system told me to." (not transparent)"""
|
| 238 |
+
|
| 239 |
+
JUSTIFICATION_USER_TEMPLATE = """**Action Taken:**
|
| 240 |
+
- Step: {step_description}
|
| 241 |
+
- Tool Used: {tool_used}
|
| 242 |
+
- Result: {execution_result}
|
| 243 |
+
|
| 244 |
+
**Context:**
|
| 245 |
+
- Task: {task}
|
| 246 |
+
- Step Number: {step_number} of {total_steps}
|
| 247 |
+
- Expected Outcome: {expected_outcome}
|
| 248 |
+
|
| 249 |
+
Please provide a clear justification for why this action was taken and how it advances the task. Return ONLY valid JSON."""
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
# ============================================================================
|
| 253 |
+
# FINAL SYNTHESIS PROMPT
|
| 254 |
+
# ============================================================================
|
| 255 |
+
|
| 256 |
+
SYNTHESIS_SYSTEM_PROMPT = """You are a final synthesis agent. Your role is to compile all executed steps into a coherent final answer for the user.
|
| 257 |
+
|
| 258 |
+
**Your Responsibilities:**
|
| 259 |
+
1. Review all executed steps and their results
|
| 260 |
+
2. Synthesize findings into a clear, complete answer
|
| 261 |
+
3. Ensure the answer directly addresses the original task
|
| 262 |
+
4. Include relevant evidence and data
|
| 263 |
+
5. Maintain appropriate confidence levels
|
| 264 |
+
|
| 265 |
+
**Output Format:**
|
| 266 |
+
Return a natural language response (NOT JSON for this prompt). Structure your answer as:
|
| 267 |
+
|
| 268 |
+
1. **Direct Answer**: Lead with the answer to the task
|
| 269 |
+
2. **Supporting Evidence**: Key data or findings that support the answer
|
| 270 |
+
3. **Confidence Level**: Your certainty in this answer (high/medium/low)
|
| 271 |
+
4. **Caveats**: Any limitations or uncertainties
|
| 272 |
+
|
| 273 |
+
**Quality Guidelines:**
|
| 274 |
+
- Be concise but complete
|
| 275 |
+
- Cite specific data from the execution steps
|
| 276 |
+
- Acknowledge uncertainty where present
|
| 277 |
+
- Use clear, accessible language
|
| 278 |
+
- Ensure the answer is actionable"""
|
| 279 |
+
|
| 280 |
+
SYNTHESIS_USER_TEMPLATE = """**Original Task:**
|
| 281 |
+
{task}
|
| 282 |
+
|
| 283 |
+
**Executed Steps Summary:**
|
| 284 |
+
{steps_summary}
|
| 285 |
+
|
| 286 |
+
**Results from Each Step:**
|
| 287 |
+
{all_results}
|
| 288 |
+
|
| 289 |
+
Please synthesize these findings into a final answer for the user."""
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# ============================================================================
|
| 293 |
+
# ERROR HANDLING PROMPTS
|
| 294 |
+
# ============================================================================
|
| 295 |
+
|
| 296 |
+
ERROR_ANALYSIS_PROMPT = """You are an error analysis agent. A step in the reasoning chain has failed.
|
| 297 |
+
|
| 298 |
+
**Your Task:**
|
| 299 |
+
Analyze the error and determine:
|
| 300 |
+
1. What went wrong
|
| 301 |
+
2. Whether the error is recoverable
|
| 302 |
+
3. What corrective action should be taken
|
| 303 |
+
|
| 304 |
+
**Output JSON:**
|
| 305 |
+
{
|
| 306 |
+
"error_type": "tool_failure|validation_error|safety_block|timeout",
|
| 307 |
+
"is_recoverable": true,
|
| 308 |
+
"suggested_action": "retry|skip|abort|modify_step",
|
| 309 |
+
"explanation": "Clear explanation of the error and recommendation"
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
**Error Details:**
|
| 313 |
+
Step: {step_description}
|
| 314 |
+
Error: {error_message}
|
| 315 |
+
Context: {context}
|
| 316 |
+
|
| 317 |
+
Return ONLY valid JSON."""
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
# ============================================================================
|
| 321 |
+
# HELPER FUNCTIONS FOR PROMPT FORMATTING
|
| 322 |
+
# ============================================================================
|
| 323 |
+
|
| 324 |
+
def format_planner_prompt(task: str) -> dict:
|
| 325 |
+
"""Format the planner prompt with task context."""
|
| 326 |
+
return {
|
| 327 |
+
"system": PLANNER_SYSTEM_PROMPT,
|
| 328 |
+
"user": PLANNER_USER_TEMPLATE.format(task=task)
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def format_safety_prompt(
|
| 333 |
+
step_description: str,
|
| 334 |
+
task: str,
|
| 335 |
+
step_number: int,
|
| 336 |
+
total_steps: int,
|
| 337 |
+
previous_steps: str = "None",
|
| 338 |
+
additional_context: str = "None"
|
| 339 |
+
) -> dict:
|
| 340 |
+
"""Format the safety validation prompt."""
|
| 341 |
+
return {
|
| 342 |
+
"system": SAFETY_SYSTEM_PROMPT,
|
| 343 |
+
"user": SAFETY_USER_TEMPLATE.format(
|
| 344 |
+
step_description=step_description,
|
| 345 |
+
task=task,
|
| 346 |
+
step_number=step_number,
|
| 347 |
+
total_steps=total_steps,
|
| 348 |
+
previous_steps=previous_steps,
|
| 349 |
+
additional_context=additional_context
|
| 350 |
+
)
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
|
| 354 |
+
def format_executor_prompt(
|
| 355 |
+
step_description: str,
|
| 356 |
+
task: str,
|
| 357 |
+
expected_outcome: str,
|
| 358 |
+
requires_tools: bool,
|
| 359 |
+
previous_results: str = "None"
|
| 360 |
+
) -> dict:
|
| 361 |
+
"""Format the executor/router prompt."""
|
| 362 |
+
return {
|
| 363 |
+
"system": EXECUTOR_SYSTEM_PROMPT,
|
| 364 |
+
"user": EXECUTOR_USER_TEMPLATE.format(
|
| 365 |
+
step_description=step_description,
|
| 366 |
+
task=task,
|
| 367 |
+
expected_outcome=expected_outcome,
|
| 368 |
+
requires_tools=requires_tools,
|
| 369 |
+
previous_results=previous_results
|
| 370 |
+
)
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def format_justification_prompt(
|
| 375 |
+
step_description: str,
|
| 376 |
+
tool_used: str,
|
| 377 |
+
execution_result: str,
|
| 378 |
+
task: str,
|
| 379 |
+
step_number: int,
|
| 380 |
+
total_steps: int,
|
| 381 |
+
expected_outcome: str
|
| 382 |
+
) -> dict:
|
| 383 |
+
"""Format the justification prompt."""
|
| 384 |
+
return {
|
| 385 |
+
"system": JUSTIFICATION_SYSTEM_PROMPT,
|
| 386 |
+
"user": JUSTIFICATION_USER_TEMPLATE.format(
|
| 387 |
+
step_description=step_description,
|
| 388 |
+
tool_used=tool_used,
|
| 389 |
+
execution_result=execution_result,
|
| 390 |
+
task=task,
|
| 391 |
+
step_number=step_number,
|
| 392 |
+
total_steps=total_steps,
|
| 393 |
+
expected_outcome=expected_outcome
|
| 394 |
+
)
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def format_synthesis_prompt(task: str, steps_summary: str, all_results: str) -> dict:
|
| 399 |
+
"""Format the final synthesis prompt."""
|
| 400 |
+
return {
|
| 401 |
+
"system": SYNTHESIS_SYSTEM_PROMPT,
|
| 402 |
+
"user": SYNTHESIS_USER_TEMPLATE.format(
|
| 403 |
+
task=task,
|
| 404 |
+
steps_summary=steps_summary,
|
| 405 |
+
all_results=all_results
|
| 406 |
+
)
|
| 407 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
gradio>=5.0.0
|
| 3 |
+
python-dotenv>=1.0.0
|
| 4 |
+
|
| 5 |
+
# LangChain / LangGraph
|
| 6 |
+
langgraph>=0.2.0
|
| 7 |
+
langchain>=0.3.0
|
| 8 |
+
langchain-core>=0.3.0
|
| 9 |
+
langchain-anthropic>=0.2.0
|
| 10 |
+
|
| 11 |
+
# Data validation
|
| 12 |
+
pydantic>=2.0.0
|
| 13 |
+
|
| 14 |
+
# MCP Server
|
| 15 |
+
fastmcp>=0.1.0
|
| 16 |
+
|
| 17 |
+
# Utilities
|
| 18 |
+
httpx>=0.25.0
|
schemas.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic Models for Secure Reasoning MCP Server
|
| 3 |
+
Defines all input/output schemas for the agent and tool interfaces.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Optional, Dict, Any, Literal
|
| 6 |
+
from pydantic import BaseModel, Field
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# ============================================================================
|
| 11 |
+
# AGENT INPUT/OUTPUT MODELS
|
| 12 |
+
# ============================================================================
|
| 13 |
+
|
| 14 |
+
class TaskRequest(BaseModel):
|
| 15 |
+
"""External API request to start a reasoning task."""
|
| 16 |
+
task: str = Field(..., description="The task/query for the agent to solve")
|
| 17 |
+
user_id: Optional[str] = Field(None, description="Optional user identifier for audit trail")
|
| 18 |
+
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional context")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class StepPlan(BaseModel):
|
| 22 |
+
"""A single step in the agent's execution plan."""
|
| 23 |
+
step_number: int = Field(..., description="Sequential step index")
|
| 24 |
+
action: str = Field(..., description="What action to take")
|
| 25 |
+
expected_outcome: str = Field(..., description="What this step should achieve")
|
| 26 |
+
requires_tools: bool = Field(False, description="Whether this step needs tool execution")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ExecutionPlan(BaseModel):
|
| 30 |
+
"""The full plan generated by the agent."""
|
| 31 |
+
steps: List[StepPlan] = Field(..., description="Ordered list of steps")
|
| 32 |
+
total_steps: int = Field(..., description="Total number of steps")
|
| 33 |
+
generated_at: datetime = Field(default_factory=datetime.utcnow)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SafetyCheckResult(BaseModel):
|
| 37 |
+
"""Result from the safety validation LLM."""
|
| 38 |
+
is_safe: bool = Field(..., description="Whether the step is approved")
|
| 39 |
+
risk_level: Literal["safe", "low", "medium", "high", "critical"] = Field(..., description="Risk assessment")
|
| 40 |
+
reasoning: str = Field(..., description="Explanation of the safety decision")
|
| 41 |
+
blocked_reasons: Optional[List[str]] = Field(None, description="Specific safety violations if blocked")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ExecutionResult(BaseModel):
|
| 45 |
+
"""Result from executing a single step."""
|
| 46 |
+
success: bool = Field(..., description="Whether execution succeeded")
|
| 47 |
+
output: Any = Field(None, description="The result data")
|
| 48 |
+
error: Optional[str] = Field(None, description="Error message if failed")
|
| 49 |
+
tool_calls: List[str] = Field(default_factory=list, description="Tools that were invoked")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class Justification(BaseModel):
|
| 53 |
+
"""Agent's explanation for why it took an action."""
|
| 54 |
+
step_number: int
|
| 55 |
+
reasoning: str = Field(..., description="Natural language explanation")
|
| 56 |
+
evidence: Optional[List[str]] = Field(None, description="Supporting facts or data")
|
| 57 |
+
alternatives_considered: Optional[List[str]] = Field(None, description="Other approaches considered")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class TaskResponse(BaseModel):
|
| 61 |
+
"""Final response returned to the user."""
|
| 62 |
+
task_id: str = Field(..., description="Unique identifier for this execution")
|
| 63 |
+
status: Literal["completed", "failed", "blocked"] = Field(..., description="Final status")
|
| 64 |
+
result: Optional[Any] = Field(None, description="The final answer or output")
|
| 65 |
+
plan: ExecutionPlan = Field(..., description="The plan that was executed")
|
| 66 |
+
justifications: List[Justification] = Field(..., description="Reasoning for each step")
|
| 67 |
+
logs: List["CryptoLogEntry"] = Field(..., description="Cryptographic audit trail")
|
| 68 |
+
error: Optional[str] = Field(None, description="Error message if failed")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# ============================================================================
|
| 72 |
+
# CRYPTOGRAPHIC TOOL INTERFACES (for teammate's implementations)
|
| 73 |
+
# ============================================================================
|
| 74 |
+
|
| 75 |
+
class HashRequest(BaseModel):
|
| 76 |
+
"""Request to hash data."""
|
| 77 |
+
data: str = Field(..., description="Data to hash (JSON string or plain text)")
|
| 78 |
+
algorithm: Literal["sha256", "sha3_256", "blake2b"] = Field("sha256", description="Hash algorithm")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class HashResponse(BaseModel):
|
| 82 |
+
"""Response from hash tool."""
|
| 83 |
+
hash: str = Field(..., description="Hexadecimal hash digest")
|
| 84 |
+
algorithm: str = Field(..., description="Algorithm used")
|
| 85 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class MerkleUpdateRequest(BaseModel):
|
| 89 |
+
"""Request to add a hash to the Merkle tree."""
|
| 90 |
+
leaf_hash: str = Field(..., description="Hash to add as a new leaf")
|
| 91 |
+
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional context to store")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class MerkleUpdateResponse(BaseModel):
|
| 95 |
+
"""Response from Merkle tree update."""
|
| 96 |
+
merkle_root: str = Field(..., description="Updated Merkle root hash")
|
| 97 |
+
leaf_index: int = Field(..., description="Index of the new leaf")
|
| 98 |
+
proof: List[str] = Field(..., description="Merkle proof path")
|
| 99 |
+
tree_size: int = Field(..., description="Total leaves in tree")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class WORMWriteRequest(BaseModel):
|
| 103 |
+
"""Request to write immutable data to WORM storage."""
|
| 104 |
+
entry_id: str = Field(..., description="Unique identifier for this entry")
|
| 105 |
+
data: Dict[str, Any] = Field(..., description="Data to store permanently")
|
| 106 |
+
merkle_root: str = Field(..., description="Current Merkle root for verification")
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class WORMWriteResponse(BaseModel):
|
| 110 |
+
"""Response from WORM storage write."""
|
| 111 |
+
success: bool = Field(..., description="Whether write succeeded")
|
| 112 |
+
storage_path: str = Field(..., description="Where data was stored")
|
| 113 |
+
verification_hash: str = Field(..., description="Hash of the stored data for verification")
|
| 114 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class CryptoLogEntry(BaseModel):
|
| 118 |
+
"""A single entry in the cryptographic audit trail."""
|
| 119 |
+
step_number: int
|
| 120 |
+
action_hash: str = Field(..., description="Hash of the action taken")
|
| 121 |
+
merkle_root: str = Field(..., description="Merkle root after this action")
|
| 122 |
+
worm_path: Optional[str] = Field(None, description="WORM storage location")
|
| 123 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ============================================================================
|
| 127 |
+
# ERROR MODELS
|
| 128 |
+
# ============================================================================
|
| 129 |
+
|
| 130 |
+
class ErrorResponse(BaseModel):
|
| 131 |
+
"""Standard error response."""
|
| 132 |
+
error: str = Field(..., description="Error message")
|
| 133 |
+
error_type: str = Field(..., description="Category of error")
|
| 134 |
+
details: Optional[Dict[str, Any]] = Field(None, description="Additional error context")
|
| 135 |
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
server.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastmcp import FastMCP
|
| 2 |
+
from crypto_engine import hash_tool, worm_write_tool, proof_generate_tool, verify_proof_tool
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
# Initialize the MCP server
|
| 6 |
+
mcp = FastMCP("Secure Reasoning Server")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@mcp.tool()
|
| 10 |
+
def hash_data(data: str) -> str:
|
| 11 |
+
"""
|
| 12 |
+
Hash a string or JSON data using SHA-256.
|
| 13 |
+
Input: data (string or JSON-serializable object as string)
|
| 14 |
+
Output: SHA-256 hex digest
|
| 15 |
+
"""
|
| 16 |
+
return hash_tool(data)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@mcp.tool()
|
| 20 |
+
def write_to_worm(step_data: str, hash_value: str, merkle_root: str) -> str:
|
| 21 |
+
"""
|
| 22 |
+
Write a step record to WORM (Write Once, Read Many) storage.
|
| 23 |
+
Input: step_data (JSON string), hash_value (hex string), merkle_root (hex string)
|
| 24 |
+
Output: JSON record with id, timestamp, step, hash, and root
|
| 25 |
+
"""
|
| 26 |
+
step_dict = json.loads(step_data) if isinstance(step_data, str) else step_data
|
| 27 |
+
record = worm_write_tool(step_dict, hash_value, merkle_root)
|
| 28 |
+
return json.dumps(record)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@mcp.tool()
|
| 32 |
+
def generate_proof(record_id: int) -> str:
|
| 33 |
+
"""
|
| 34 |
+
Generate a Merkle proof for a specific record in the WORM log.
|
| 35 |
+
Input: record_id (integer, the ID of the record)
|
| 36 |
+
Output: JSON containing record_id, hash, merkle_proof, merkle_root, timestamp, and step_details
|
| 37 |
+
"""
|
| 38 |
+
proof = proof_generate_tool(record_id)
|
| 39 |
+
if proof is None:
|
| 40 |
+
return json.dumps({"error": f"Record with ID {record_id} not found"})
|
| 41 |
+
return json.dumps(proof)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@mcp.tool()
|
| 45 |
+
def verify_proof(target_hash: str, merkle_proof: str, merkle_root: str) -> str:
|
| 46 |
+
"""
|
| 47 |
+
Verify if a target_hash belongs to the merkle_root using the merkle_proof.
|
| 48 |
+
Input: target_hash (hex string), merkle_proof (JSON string of proof array), merkle_root (hex string)
|
| 49 |
+
Output: JSON with result (true/false) and verification status message
|
| 50 |
+
"""
|
| 51 |
+
proof_list = json.loads(merkle_proof) if isinstance(merkle_proof, str) else merkle_proof
|
| 52 |
+
is_valid = verify_proof_tool(target_hash, proof_list, merkle_root)
|
| 53 |
+
return json.dumps({
|
| 54 |
+
"verified": is_valid,
|
| 55 |
+
"message": "Proof verified successfully" if is_valid else "Proof verification failed - possible tampering"
|
| 56 |
+
})
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
mcp.run()
|
state.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
LangGraph State Definition for Secure Reasoning Agent
|
| 3 |
+
Tracks all state through the Chain-of-Checks workflow.
|
| 4 |
+
"""
|
| 5 |
+
from typing import TypedDict, List, Optional, Annotated
|
| 6 |
+
from operator import add
|
| 7 |
+
from langchain_core.messages import BaseMessage
|
| 8 |
+
|
| 9 |
+
from schemas import (
|
| 10 |
+
ExecutionPlan,
|
| 11 |
+
SafetyCheckResult,
|
| 12 |
+
CryptoLogEntry,
|
| 13 |
+
Justification,
|
| 14 |
+
ExecutionResult
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class AgentState(TypedDict):
|
| 19 |
+
"""
|
| 20 |
+
State tracked throughout the LangGraph execution.
|
| 21 |
+
|
| 22 |
+
The state flows through: Plan → Safety Check → Execute → Log → Justify → Loop
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
# ========================================================================
|
| 26 |
+
# CONVERSATION & CONTEXT
|
| 27 |
+
# ========================================================================
|
| 28 |
+
messages: Annotated[List[BaseMessage], add]
|
| 29 |
+
"""Chat history with the user and internal LLM calls. Uses 'add' reducer to append."""
|
| 30 |
+
|
| 31 |
+
task: str
|
| 32 |
+
"""The original user task/query."""
|
| 33 |
+
|
| 34 |
+
task_id: str
|
| 35 |
+
"""Unique identifier for this execution (for audit trail)."""
|
| 36 |
+
|
| 37 |
+
user_id: Optional[str]
|
| 38 |
+
"""Optional user identifier for multi-user environments."""
|
| 39 |
+
|
| 40 |
+
# ========================================================================
|
| 41 |
+
# PLANNING STATE
|
| 42 |
+
# ========================================================================
|
| 43 |
+
plan: Optional[ExecutionPlan]
|
| 44 |
+
"""The generated execution plan with all steps."""
|
| 45 |
+
|
| 46 |
+
current_step_index: int
|
| 47 |
+
"""Which step we're currently processing (0-indexed)."""
|
| 48 |
+
|
| 49 |
+
# ========================================================================
|
| 50 |
+
# SAFETY & VALIDATION
|
| 51 |
+
# ========================================================================
|
| 52 |
+
safety_status: Optional[SafetyCheckResult]
|
| 53 |
+
"""Result of safety check for current step. None if not yet checked."""
|
| 54 |
+
|
| 55 |
+
safety_blocked: bool
|
| 56 |
+
"""Quick flag: True if any step was blocked by safety guardrails."""
|
| 57 |
+
|
| 58 |
+
# ========================================================================
|
| 59 |
+
# EXECUTION STATE
|
| 60 |
+
# ========================================================================
|
| 61 |
+
execution_result: Optional[ExecutionResult]
|
| 62 |
+
"""Result from executing the current step."""
|
| 63 |
+
|
| 64 |
+
final_result: Optional[str]
|
| 65 |
+
"""The final answer/output when all steps complete."""
|
| 66 |
+
|
| 67 |
+
# ========================================================================
|
| 68 |
+
# AUDIT TRAIL & CRYPTOGRAPHIC LOGGING
|
| 69 |
+
# ========================================================================
|
| 70 |
+
logs: List[CryptoLogEntry]
|
| 71 |
+
"""Cryptographic proofs for each executed step (Merkle roots, hashes, etc.)."""
|
| 72 |
+
|
| 73 |
+
justifications: List[Justification]
|
| 74 |
+
"""Agent's reasoning for each action taken."""
|
| 75 |
+
|
| 76 |
+
# ========================================================================
|
| 77 |
+
# ERROR HANDLING
|
| 78 |
+
# ========================================================================
|
| 79 |
+
error: Optional[str]
|
| 80 |
+
"""Error message if execution fails."""
|
| 81 |
+
|
| 82 |
+
status: str
|
| 83 |
+
"""Current execution status: 'planning', 'executing', 'completed', 'failed', 'blocked'."""
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def create_initial_state(task: str, task_id: str, user_id: Optional[str] = None) -> AgentState:
|
| 87 |
+
"""
|
| 88 |
+
Factory function to create a fresh AgentState for a new task.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
task: The user's task/query
|
| 92 |
+
task_id: Unique identifier for this execution
|
| 93 |
+
user_id: Optional user identifier
|
| 94 |
+
|
| 95 |
+
Returns:
|
| 96 |
+
Initialized AgentState ready for LangGraph processing
|
| 97 |
+
"""
|
| 98 |
+
return AgentState(
|
| 99 |
+
messages=[],
|
| 100 |
+
task=task,
|
| 101 |
+
task_id=task_id,
|
| 102 |
+
user_id=user_id,
|
| 103 |
+
plan=None,
|
| 104 |
+
current_step_index=0,
|
| 105 |
+
safety_status=None,
|
| 106 |
+
safety_blocked=False,
|
| 107 |
+
execution_result=None,
|
| 108 |
+
final_result=None,
|
| 109 |
+
logs=[],
|
| 110 |
+
justifications=[],
|
| 111 |
+
error=None,
|
| 112 |
+
status="planning"
|
| 113 |
+
)
|