b2230765034 commited on
Commit
af6094d
·
0 Parent(s):

Initial commit - Secure Reasoning MCP Server

Browse files
Files changed (10) hide show
  1. README.md +65 -0
  2. app.py +148 -0
  3. crypto_engine.py +333 -0
  4. graph.py +702 -0
  5. mock_tools.py +221 -0
  6. prompts.py +407 -0
  7. requirements.txt +18 -0
  8. schemas.py +135 -0
  9. server.py +60 -0
  10. 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
+ [![Hugging Face Spaces](https://img.shields.io/badge/🤗%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces)
22
+ [![Built with Gradio](https://img.shields.io/badge/Built%20with-Gradio%205-orange)](https://gradio.app)
23
+ [![Powered by LangGraph](https://img.shields.io/badge/Powered%20by-LangGraph-green)](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
+ )