Skip to main content

truthlinked_mcp/
lib.rs

1//! TruthLinked on-chain MCP primitives for agent-native blockchain execution.
2//!
3//! This is not a server wrapping the chain.
4//! MCP tools, resources, and prompts are smart cells deployed on TruthLinked.
5//!
6//! Architecture:
7//!
8//!   McpRegistry cell   - the chain's MCP namespace. Stores all registered
9//!                            tool resource prompt cell ids. The agent's
10//!                            tools list is a read of this cell's storage.
11//!
12//!   McpTool cell       - each tool is a deployed Axiom cell with
13//!                            storage["schema"]     = json schema bytes
14//!                            storage["name"]       = tool name bytes
15//!                            storage["version"]    = semver bytes
16//!                            calling the tool is a CallCell on that cell.
17//!                            the tool's Axiom bytecode is the tool's logic.
18//!
19//!   McpResource cell   - a cell whose storage is the resource data.
20//!                            reading a resource reads cell storage slots.
21//!                            the resource is versioned by manifest_version.
22//!
23//!   McpPrompt cell     - a cell storing prompt templates in storage.
24//!                            validators approve prompts via the name registry.
25//!                            prompt provenance is provable: who deployed it,
26//!                            which block, what manifest hash.
27//!
28//!   AgentPolicy cell   - a Axiom cell that enforces what calls an agent
29//!                            is permitted to make. Called before every tool call
30//!                            via CallCellChain. Reverts the chain if policy
31//!                            is violated, the chain enforces it, not middleware.
32//!
33//!   AgentRegistry cell - maps agent_id to policy_cell_id.
34//!                            owner sets this. Agent cannot change it.
35//!                            stored on chain. Immutable once the owner locks it.
36//!
37//! New TransactionIntents:
38//!
39//!   RegisterMcpTool       - deploys a tool cell and registers in McpRegistry
40//!   RegisterMcpResource   - deploys a resource cell and registers in McpRegistry
41//!   RegisterMcpPrompt     - deploys a prompt cell and validator approves name
42//!   RegisterAgent         - binds agent_id to policy_cell_id in AgentRegistry
43//!   UpdateAgentPolicy     - owner calls policy cell to change permissions
44//!   SuspendAgent          - owner sets agent status to Suspended in AgentRegistry
45//!   McpToolCall           - agent calls a tool with policy check and execution,
46//!                           all atomic in one CallCellChain transaction
47//!
48//! The MCP protocol transport (websocket json-rpc) is a thin adapter that
49//! translates MCP wire messages into TruthLinked transactions and reads.
50//! Every tool call produces a transaction. Every resource read reads cell
51//! storage. The chain is the MCP server. The transport is just protocol glue.
52
53use im::HashMap as ImHashMap;
54use serde::{Deserialize, Serialize};
55use std::collections::HashMap;
56use truthlinked_core::pq_execution::{AccountId, CellCall, TransactionIntent};
57use truthlinked_governance::params as gp;
58use truthlinked_runtime::cells::{CellAccount, CellState};
59use truthlinked_runtime::compiler_aware::StorageKey;
60use truthlinked_runtime::types::{AccountRecord, CellUpdate, StateDiff};
61pub mod private_balance;
62pub mod zk_transfer;
63
64pub trait McpStateView {
65    fn cells(&self) -> &CellState;
66    fn accounts(&self) -> &ImHashMap<AccountId, AccountRecord>;
67}
68
69//
70// Storage key namespaces.
71// All MCP on-chain state lives in cell storage using these key prefixes.
72// Every key is 32 bytes. Multi-byte fields are packed with blake3 hashing.
73//
74
75/// Canonical storage keys for the McpRegistry cell
76pub mod registry_keys {
77    /// storage[TOOL_COUNT]          = u64 little-endian: total registered tools
78    pub const TOOL_COUNT: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_TOOL_COUNT_KEY;
79    /// storage[RESOURCE_COUNT]      = u64: total registered resources
80    pub const RESOURCE_COUNT: [u8; 32] =
81        truthlinked_core::constants::MCP_REGISTRY_RESOURCE_COUNT_KEY;
82    /// storage[PROMPT_COUNT]        = u64: total registered prompts
83    pub const PROMPT_COUNT: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_PROMPT_COUNT_KEY;
84    /// storage[REGISTRY_VERSION]    = u64: incremented on every registration
85    pub const REGISTRY_VER: [u8; 32] = truthlinked_core::constants::MCP_REGISTRY_VERSION_KEY;
86
87    /// storage[tool_entry(i)]       = [u8; 32]: cell_id of tool i
88    pub fn tool_entry(index: u64) -> [u8; 32] {
89        let mut k = truthlinked_core::constants::mcp_key(b"mcp:tool:");
90        k[16..24].copy_from_slice(&index.to_le_bytes());
91        k
92    }
93    /// storage[resource_entry(i)]   = [u8; 32]: cell_id of resource i
94    pub fn resource_entry(index: u64) -> [u8; 32] {
95        let mut k = truthlinked_core::constants::mcp_key(b"mcp:res:");
96        k[16..24].copy_from_slice(&index.to_le_bytes());
97        k
98    }
99    /// storage[prompt_entry(i)]     = [u8; 32]: cell_id of prompt i
100    pub fn prompt_entry(index: u64) -> [u8; 32] {
101        let mut k = truthlinked_core::constants::mcp_key(b"mcp:prompt:");
102        k[16..24].copy_from_slice(&index.to_le_bytes());
103        k
104    }
105    /// storage[name_to_tool(name)]  = [u8; 32]: cell_id for a named tool
106    pub fn name_to_tool(name: &str) -> [u8; 32] {
107        blake3_key(b"mcp:ntool:", name.as_bytes())
108    }
109    /// storage[name_to_resource(name)] = [u8; 32]: cell_id
110    pub fn name_to_resource(name: &str) -> [u8; 32] {
111        blake3_key(b"mcp:nres:", name.as_bytes())
112    }
113    /// storage[name_to_prompt(name)] = [u8; 32]
114    pub fn name_to_prompt(name: &str) -> [u8; 32] {
115        blake3_key(b"mcp:nprompt:", name.as_bytes())
116    }
117
118    pub fn key(prefix: &[u8]) -> [u8; 32] {
119        truthlinked_core::constants::mcp_key(prefix)
120    }
121    pub fn blake3_key(prefix: &[u8], data: &[u8]) -> [u8; 32] {
122        let mut input = prefix.to_vec();
123        input.extend_from_slice(data);
124        *blake3::hash(&input).as_bytes()
125    }
126}
127
128/// Canonical storage keys for a McpTool cell
129pub mod tool_keys {
130    /// storage[NAME]      = UTF-8 bytes of tool name (max 32 bytes, padded)
131    pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_TOOL_NAME_KEY;
132    /// storage[DESCRIPTION]= first 32 bytes of description hash (full desc in metadata)
133    pub const DESC_HASH: [u8; 32] = truthlinked_core::constants::MCP_TOOL_DESC_HASH_KEY;
134    /// storage[SCHEMA_HASH]= blake3(input_schema_json)
135    pub const SCHEMA_HASH: [u8; 32] = truthlinked_core::constants::MCP_TOOL_SCHEMA_HASH_KEY;
136    /// storage[CATEGORY]  = u8 category tag (0=read, 1=write, 2=admin)
137    pub const CATEGORY: [u8; 32] = truthlinked_core::constants::MCP_TOOL_CATEGORY_KEY;
138    /// storage[CALL_COUNT] = u64: total successful invocations (commutative Add)
139    pub const CALL_COUNT: [u8; 32] = truthlinked_core::constants::MCP_TOOL_CALL_COUNT_KEY;
140    /// storage[OWNER]      = [u8; 32]: who governs this tool's Axiom bytecode
141    pub const OWNER: [u8; 32] = truthlinked_core::constants::MCP_TOOL_OWNER_KEY;
142    /// storage[ENABLED]    = u8: 1=enabled, 0=disabled
143    pub const ENABLED: [u8; 32] = truthlinked_core::constants::MCP_TOOL_ENABLED_KEY;
144}
145
146/// Canonical storage keys for a McpResource cell
147pub mod resource_keys {
148    pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_NAME_KEY;
149    pub const URI_SCHEME: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_URI_SCHEME_KEY;
150    pub const MIME_TYPE: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_MIME_TYPE_KEY;
151    pub const CONTENT_HASH: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_CONTENT_HASH_KEY;
152    pub const UPDATED_AT: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_UPDATED_AT_KEY;
153    pub const READ_COUNT: [u8; 32] = truthlinked_core::constants::MCP_RESOURCE_READ_COUNT_KEY;
154
155    /// Dynamic content slot: resource data is stored at blake3("res:data:" || slot_key)
156    pub fn data_slot(slot_key: &[u8]) -> [u8; 32] {
157        super::registry_keys::blake3_key(b"res:data:", slot_key)
158    }
159}
160
161/// Canonical storage keys for a McpPrompt cell
162pub mod prompt_keys {
163    pub const NAME: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_NAME_KEY;
164    pub const TEMPLATE_HASH: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_TEMPLATE_HASH_KEY;
165    pub const ARG_COUNT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_ARG_COUNT_KEY;
166    pub const USE_COUNT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_USE_COUNT_KEY;
167    pub const APPROVED_AT: [u8; 32] = truthlinked_core::constants::MCP_PROMPT_APPROVED_AT_KEY;
168
169    /// Argument schema at index i
170    pub fn arg_schema(i: u8) -> [u8; 32] {
171        let mut k = truthlinked_core::constants::mcp_key(b"prompt:arg:");
172        k[16] = i;
173        k
174    }
175}
176
177/// Canonical storage keys for an AgentPolicy cell
178pub mod policy_keys {
179    pub const OWNER: [u8; 32] = truthlinked_core::constants::MCP_POLICY_OWNER_KEY;
180    pub const STATUS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_STATUS_KEY;
181    /// 0=active 1=suspended 2=revoked (stored as u8)
182    pub const ALLOW_READS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_READS_KEY;
183    pub const ALLOW_WRITES: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_WRITES_KEY;
184    pub const ALLOW_ADMIN: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ALLOW_ADMIN_KEY;
185    pub const RATE_LIMIT: [u8; 32] = truthlinked_core::constants::MCP_POLICY_RATE_LIMIT_KEY;
186    /// Max calls per minute: u32
187    pub const SPEND_PER_TX: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SPEND_PER_TX_KEY;
188    /// Max TRTH per tx: u128 LE
189    pub const SPEND_EPOCH: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SPEND_EPOCH_KEY;
190    pub const EPOCH_USED: [u8; 32] = truthlinked_core::constants::MCP_POLICY_EPOCH_USED_KEY;
191    pub const EPOCH_RESET_TS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_EPOCH_RESET_TS_KEY;
192    pub const ACTIONS_MIN: [u8; 32] = truthlinked_core::constants::MCP_POLICY_ACTIONS_MIN_KEY;
193    pub const MIN_WINDOW_TS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_MIN_WINDOW_TS_KEY;
194    pub const TOTAL_ACTIONS: [u8; 32] = truthlinked_core::constants::MCP_POLICY_TOTAL_ACTIONS_KEY;
195    pub const HITL_THRESHOLD: [u8; 32] = truthlinked_core::constants::MCP_POLICY_HITL_THRESHOLD_KEY;
196    pub const SUSPEND_REASON: [u8; 32] = truthlinked_core::constants::MCP_POLICY_SUSPEND_REASON_KEY;
197
198    /// Per-tool permission: blake3("pol:tool:" || tool_cell_id)
199    pub fn tool_permission(tool_id: &[u8; 32]) -> [u8; 32] {
200        super::registry_keys::blake3_key(b"pol:tool:", tool_id)
201    }
202}
203
204/// Canonical storage keys for the AgentRegistry cell
205pub mod agent_reg_keys {
206    pub const AGENT_COUNT: [u8; 32] = truthlinked_core::constants::MCP_AGENT_REGISTRY_COUNT_KEY;
207
208    /// agent_id -> policy_cell_id
209    pub fn agent_policy(agent_id: &[u8; 32]) -> [u8; 32] {
210        super::registry_keys::blake3_key(b"areg:pol:", agent_id)
211    }
212    /// agent_id -> owner_id
213    pub fn agent_owner(agent_id: &[u8; 32]) -> [u8; 32] {
214        super::registry_keys::blake3_key(b"areg:own:", agent_id)
215    }
216    /// index -> agent_id for enumerable registry scans.
217    pub fn agent_entry(index: u64) -> [u8; 32] {
218        let mut bytes = [0u8; 8];
219        bytes.copy_from_slice(&index.to_le_bytes());
220        super::registry_keys::blake3_key(b"areg:idx:", &bytes)
221    }
222
223    /// agent_id -> registered_at (u64)
224    pub fn agent_registered_at(agent_id: &[u8; 32]) -> [u8; 32] {
225        super::registry_keys::blake3_key(b"areg:reg:", agent_id)
226    }
227}
228
229//
230// NEW TRANSACTION INTENTS
231// These extend TransactionIntent in pq_execution.rs.
232// Listed here as the canonical definition - will be merged into the enum.
233//
234
235/// All new MCP-related intent variants to be added to TransactionIntent.
236/// Design rule: every intent maps to deterministic state transitions.
237/// The parallel executor's conflict domain extracts their storage keys.
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum McpIntent {
240    /// Deploy a tool cell and register it in the McpRegistry.
241    /// The tool's Axiom bytecode is its implementation. Schema lives in storage.
242    /// Anyone can deploy a tool. The registry is permissionless.
243    RegisterMcpTool {
244        /// Desired tool cell address
245        tool_id: AccountId,
246        /// Axiom bytecode implementing the tool's logic
247        bytecode: Vec<u8>,
248        /// Tool name (max 32 bytes)
249        name: String,
250        /// Full JSON schema (stored as blake3 hash on-chain, full bytes in calldata)
251        input_schema_json: Vec<u8>,
252        /// 0=read-only, 1=writes state, 2=admin
253        category: u8,
254        /// Storage slots the Axiom bytecode reads/writes (for parallel conflict detection)
255        declared_reads: Vec<[u8; 32]>,
256        declared_writes: Vec<[u8; 32]>,
257        commutative_keys: Vec<[u8; 32]>,
258        oracle_schema_ids: Vec<[u8; 32]>,
259        /// Address of the McpRegistry to register into
260        registry_id: AccountId,
261    },
262
263    /// Deploy a resource cell. Its storage IS the resource data.
264    /// Agents read resources by calling get_cell_storage on specific slots.
265    RegisterMcpResource {
266        resource_id: AccountId,
267        bytecode: Vec<u8>, // Axiom bytecode for dynamic resources (optional)
268        name: String,
269        uri_scheme: String, // e.g. "trth://chain/validators"
270        mime_type: String,  // "application/json", "text/plain", etc.
271        /// Initial content: map of slot_key -> content_bytes (hashed to storage key)
272        initial_data: Vec<(Vec<u8>, Vec<u8>)>,
273        declared_reads: Vec<[u8; 32]>,
274        declared_writes: Vec<[u8; 32]>,
275        oracle_schema_ids: Vec<[u8; 32]>,
276        registry_id: AccountId,
277    },
278
279    /// Deploy a prompt template cell.
280    /// Prompt templates go through the validator name registry for approval.
281    /// An approved prompt has a validator-certified name on-chain.
282    RegisterMcpPrompt {
283        prompt_id: AccountId,
284        name: String,
285        /// Full template bytes (stored as hash on-chain, full bytes in calldata)
286        template_bytes: Vec<u8>,
287        /// Argument definitions: (arg_name, arg_description, required)
288        arguments: Vec<(String, String, bool)>,
289        registry_id: AccountId,
290    },
291
292    /// Bind an agent's public key to a policy cell.
293    /// Only the owner_id account can call this.
294    /// The agent cannot register itself.
295    RegisterAgent {
296        agent_id: AccountId, // H(dilithium_pubkey)
297        policy_cell_id: AccountId,
298        agent_registry_id: AccountId,
299    },
300
301    /// Owner updates agent's policy by calling the policy cell directly.
302    /// This IS a CallCell transaction on the policy cell.
303    /// Listed here for clarity - the diff handler routes it as CallCell.
304    UpdateAgentPolicy {
305        policy_cell_id: AccountId,
306        updates: PolicyUpdate,
307    },
308
309    /// Owner suspends an agent immediately. Stored in AgentRegistry.
310    SuspendAgent {
311        agent_id: AccountId,
312        agent_registry_id: AccountId,
313        reason: String,
314    },
315
316    /// Owner reinstates a suspended agent.
317    ReinstateAgent {
318        agent_id: AccountId,
319        agent_registry_id: AccountId,
320    },
321
322    /// The core MCP tool call intent.
323    ///
324    /// This compiles into a CallCellChain:
325    ///   1. Call policy_cell with (agent_id, tool_id, calldata_hash) - reverts if blocked
326    ///   2. Call tool_cell with agent_calldata - executes the tool
327    ///   3. (Optional) Call action_log_cell with (tool_id, result_hash) - appends log entry
328    ///
329    /// The ENTIRE chain is atomic. If policy rejects, nothing is written.
330    /// If the tool fails, the policy gate's side effects are also rolled back.
331    /// The action log entry only lands if the tool succeeded.
332    McpToolCall {
333        agent_id: AccountId,
334        tool_id: AccountId,
335        tool_calldata: Vec<u8>,
336        value: u128,
337        gas_limit: u64,
338        policy_cell_id: AccountId,
339        action_log_id: Option<AccountId>,
340        timestamp: u64,
341    },
342}
343
344/// Policy fields an owner may update
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct PolicyUpdate {
347    pub status: Option<u8>, // 0=active 1=suspended 2=revoked
348    pub allow_reads: Option<bool>,
349    pub allow_writes: Option<bool>,
350    pub allow_admin: Option<bool>,
351    pub rate_limit: Option<u32>,
352    pub spend_per_tx: Option<u128>,
353    pub spend_epoch: Option<u128>,
354    pub hitl_threshold: Option<u128>,
355    /// Per-tool grants: (tool_cell_id, allowed)
356    pub tool_permissions: Vec<(AccountId, bool)>,
357}
358
359//
360// CONFLICT DOMAIN EXTRACTION
361// The parallel executor needs to know which storage slots each MCP intent touches.
362// This is called from compiler_aware.rs::ConcreteConflictDomain::from_transaction.
363//
364
365/// Extract conflict domain for MCP intents.
366/// This is the extension point for compiler_aware.rs.
367pub fn mcp_conflict_domain(intent: &McpIntent) -> (Vec<StorageKey>, Vec<StorageKey>) {
368    match intent {
369        McpIntent::RegisterMcpTool {
370            tool_id,
371            registry_id,
372            ..
373        } => {
374            // Writes: tool cell storage + registry TOOL_COUNT + registry tool_entry
375            // Read:   registry TOOL_COUNT (to get current index)
376            (
377                vec![StorageKey::CellStorage(
378                    *registry_id,
379                    registry_keys::TOOL_COUNT,
380                )],
381                vec![
382                    StorageKey::CellStorage(*registry_id, registry_keys::TOOL_COUNT),
383                    StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
384                    StorageKey::CellStorage(*tool_id, tool_keys::NAME),
385                ],
386            )
387        }
388
389        McpIntent::RegisterMcpResource {
390            resource_id,
391            registry_id,
392            ..
393        } => (
394            vec![StorageKey::CellStorage(
395                *registry_id,
396                registry_keys::RESOURCE_COUNT,
397            )],
398            vec![
399                StorageKey::CellStorage(*registry_id, registry_keys::RESOURCE_COUNT),
400                StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
401                StorageKey::CellStorage(*resource_id, resource_keys::NAME),
402            ],
403        ),
404
405        McpIntent::RegisterMcpPrompt {
406            prompt_id,
407            registry_id,
408            ..
409        } => (
410            vec![StorageKey::CellStorage(
411                *registry_id,
412                registry_keys::PROMPT_COUNT,
413            )],
414            vec![
415                StorageKey::CellStorage(*registry_id, registry_keys::PROMPT_COUNT),
416                StorageKey::CellStorage(*registry_id, registry_keys::REGISTRY_VER),
417                StorageKey::CellStorage(*prompt_id, prompt_keys::NAME),
418            ],
419        ),
420
421        McpIntent::RegisterAgent {
422            agent_id,
423            agent_registry_id,
424            ..
425        } => (
426            vec![StorageKey::CellStorage(
427                *agent_registry_id,
428                agent_reg_keys::AGENT_COUNT,
429            )],
430            vec![
431                StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_policy(agent_id)),
432                StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_owner(agent_id)),
433                StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::agent_entry(0)),
434                StorageKey::CellStorage(*agent_registry_id, agent_reg_keys::AGENT_COUNT),
435            ],
436        ),
437
438        McpIntent::SuspendAgent {
439            agent_id,
440            agent_registry_id,
441            ..
442        }
443        | McpIntent::ReinstateAgent {
444            agent_id,
445            agent_registry_id,
446        } => (
447            vec![],
448            vec![StorageKey::CellStorage(
449                *agent_registry_id,
450                agent_reg_keys::agent_policy(agent_id),
451            )],
452        ),
453
454        McpIntent::UpdateAgentPolicy {
455            policy_cell_id,
456            updates,
457        } => {
458            let mut writes = vec![
459                StorageKey::CellStorage(*policy_cell_id, policy_keys::STATUS),
460                StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_READS),
461                StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_WRITES),
462            ];
463            for (tool_id, _) in &updates.tool_permissions {
464                writes.push(StorageKey::CellStorage(
465                    *policy_cell_id,
466                    policy_keys::tool_permission(tool_id),
467                ));
468            }
469            (vec![], writes)
470        }
471
472        McpIntent::McpToolCall {
473            agent_id,
474            tool_id,
475            policy_cell_id,
476            action_log_id,
477            ..
478        } => {
479            // Reads: policy cell state (to verify before execution)
480            // Writes: policy TOTAL_ACTIONS (commutative Add - no conflict across agents),
481            //         policy ACTIONS_MIN (commutative Add),
482            //         policy EPOCH_USED (commutative Add),
483            //         tool CALL_COUNT (commutative Add),
484            //         action log (commutative Append)
485            // The tool's own declared_writes will be added by the cell execution layer.
486            let reads = vec![
487                StorageKey::CellStorage(*policy_cell_id, policy_keys::STATUS),
488                StorageKey::CellStorage(*policy_cell_id, policy_keys::ALLOW_WRITES),
489                StorageKey::CellStorage(*policy_cell_id, policy_keys::SPEND_EPOCH),
490            ];
491            let mut writes = vec![
492                // These are commutative - multiple agents can log concurrently
493                StorageKey::CellStorage(*policy_cell_id, policy_keys::TOTAL_ACTIONS),
494                StorageKey::CellStorage(*policy_cell_id, policy_keys::ACTIONS_MIN),
495                StorageKey::CellStorage(*tool_id, tool_keys::CALL_COUNT),
496            ];
497            if let Some(log_id) = action_log_id {
498                // Append to action log - commutative, zero conflict
499                writes.push(StorageKey::CellStorage(
500                    *log_id,
501                    registry_keys::blake3_key(b"log:", agent_id),
502                ));
503            }
504            (reads, writes)
505        }
506    }
507}
508
509//
510// DIFF COMPUTATION
511// These are the state transition functions for each MCP intent.
512// They integrate with State::compute_transaction_diff_internal in pq_execution.rs.
513//
514
515/// Compute the StateDiff for RegisterMcpTool.
516///
517/// Creates the tool cell account with:
518///   - Axiom bytecode as the implementation
519///   - Schema hash, name, category stored in cell storage
520///   - Commutative CALL_COUNT key registered so parallel invocations compose
521/// Updates McpRegistry storage to include the new tool.
522pub fn diff_register_tool(
523    state: &impl McpStateView,
524    sender: AccountId,
525    intent: &McpIntent,
526    timestamp: u64,
527) -> Result<StateDiff, String> {
528    let (
529        tool_id,
530        bytecode,
531        name,
532        input_schema_json,
533        category,
534        declared_reads,
535        declared_writes,
536        commutative_keys,
537        oracle_schema_ids,
538        registry_id,
539    ) = match intent {
540        McpIntent::RegisterMcpTool {
541            tool_id,
542            bytecode,
543            name,
544            input_schema_json,
545            category,
546            declared_reads,
547            declared_writes,
548            commutative_keys,
549            oracle_schema_ids,
550            registry_id,
551        } => (
552            tool_id,
553            bytecode,
554            name,
555            input_schema_json,
556            category,
557            declared_reads,
558            declared_writes,
559            commutative_keys,
560            oracle_schema_ids,
561            registry_id,
562        ),
563        _ => return Err("Wrong intent".into()),
564    };
565
566    if name.len() > 64 {
567        return Err("Tool name too long (max 64 bytes)".into());
568    }
569    if bytecode.is_empty() {
570        return Err("Tool bytecode is empty".into());
571    }
572    if bytecode.len() > gp::get_usize(gp::PARAM_MAX_CELL_BYTECODE_SIZE) {
573        return Err("Tool bytecode exceeds max size".into());
574    }
575    if bytecode.len() < 4 || &bytecode[0..4] != b"AXIO" {
576        return Err("Invalid Axiom bytecode: missing magic bytes".into());
577    }
578    truthlinked_runtime::cells::CellAccount::verify_manifest_against_bytecode(
579        bytecode,
580        declared_reads,
581        declared_writes,
582        &[],
583    )?;
584    truthlinked_runtime::cells::CellAccount::require_inferable(bytecode, &[])?;
585
586    // Tool cell must not already exist
587    if state.cells().cells.contains_key(tool_id) {
588        return Err(format!(
589            "Tool cell {} already deployed",
590            hex::encode(tool_id)
591        ));
592    }
593
594    // Registry cell must exist
595    let registry = state
596        .cells()
597        .cells
598        .get(registry_id)
599        .ok_or("McpRegistry cell not found")?;
600
601    // Rent-exempt deposit (refundable on close)
602    let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
603    let sender_account = state
604        .accounts()
605        .get(&sender)
606        .ok_or("Sender account not found")?;
607    if sender_account.balance < rent_deposit {
608        return Err("Insufficient balance for rent deposit".into());
609    }
610
611    // Build the tool's initial storage
612    let mut tool_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
613
614    // Name: first 32 bytes, padded
615    let mut name_bytes = [0u8; 32];
616    let n = name.len().min(32);
617    name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
618    tool_storage.insert(tool_keys::NAME, name_bytes);
619
620    // Schema hash
621    let schema_hash = *blake3::hash(input_schema_json).as_bytes();
622    tool_storage.insert(tool_keys::SCHEMA_HASH, schema_hash);
623
624    // Description hash (schema IS the description for now)
625    tool_storage.insert(tool_keys::DESC_HASH, schema_hash);
626
627    // Category
628    let mut cat_bytes = [0u8; 32];
629    cat_bytes[0] = *category;
630    tool_storage.insert(tool_keys::CATEGORY, cat_bytes);
631
632    // Enabled = 1
633    let mut enabled = [0u8; 32];
634    enabled[0] = 1;
635    tool_storage.insert(tool_keys::ENABLED, enabled);
636
637    // Owner = sender
638    tool_storage.insert(tool_keys::OWNER, sender);
639
640    // CALL_COUNT = 0 (commutative Add key - registered for zero-conflict parallel logging)
641    tool_storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
642
643    // Compute manifest hash binding bytecode + declared sets
644    let manifest_hash = truthlinked_runtime::cells::CellAccount::compute_manifest_hash(
645        bytecode,
646        declared_reads,
647        declared_writes,
648        commutative_keys,
649        oracle_schema_ids,
650    );
651
652    // Build the CellAccount
653    let tool_cell = truthlinked_runtime::cells::CellAccount {
654        cell_id: *tool_id,
655        owner: truthlinked_core::pq_execution::system_authority_id(),
656        bytecode: bytecode.clone(),
657        storage: tool_storage,
658        balance: 0,
659        rent_deposit,
660        is_token: false,
661        token_config: None,
662        created_at: timestamp,
663        upgraded_at: None,
664        last_rent_paid_height: 0,
665        rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
666        pending_owner: None,
667        is_immutable: false,
668        declared_reads: declared_reads.clone(),
669        declared_writes: declared_writes.clone(),
670        commutative_keys: commutative_keys.clone(),
671        storage_key_specs: vec![],
672        oracle_schema_ids: oracle_schema_ids.clone(),
673        governance_proposal: None,
674        manifest_version: 1,
675        manifest_hash,
676    };
677
678    // Update the registry: increment TOOL_COUNT and add tool_entry
679    let current_count = read_u64_from_storage(registry, &registry_keys::TOOL_COUNT);
680    let new_count = current_count.checked_add(1).ok_or("Tool count overflow")?;
681
682    let mut diff = StateDiff::default();
683
684    let mut sender_updated = sender_account.clone();
685    sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
686    diff.account_updates.insert(sender, sender_updated);
687
688    // Deploy the tool cell
689    diff.cell_updates.push(CellUpdate::Deploy {
690        cell_id: *tool_id,
691        cell: tool_cell,
692    });
693
694    // Update registry: TOOL_COUNT
695    diff.cell_updates.push(CellUpdate::StorageChange {
696        cell_id: *registry_id,
697        storage_diff: {
698            let mut m = HashMap::new();
699            // TOOL_COUNT
700            let mut count_bytes = [0u8; 32];
701            count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
702            m.insert(registry_keys::TOOL_COUNT, Some(count_bytes));
703            // tool_entry(current_count) = tool_id
704            m.insert(registry_keys::tool_entry(current_count), Some(*tool_id));
705            // name_to_tool(name) = tool_id
706            m.insert(registry_keys::name_to_tool(name), Some(*tool_id));
707            // increment registry version
708            let ver = read_u64_from_storage(registry, &registry_keys::REGISTRY_VER);
709            let mut ver_bytes = [0u8; 32];
710            ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
711            m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
712            m
713        },
714    });
715
716    diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
717
718    Ok(diff)
719}
720
721/// Compute the StateDiff for RegisterAgent.
722/// Binds agent_id -> (policy_cell_id, owner_id) in the AgentRegistry.
723pub fn diff_register_agent(
724    state: &impl McpStateView,
725    sender: AccountId,
726    intent: &McpIntent,
727    timestamp: u64,
728) -> Result<StateDiff, String> {
729    let (agent_id, policy_cell_id, agent_registry_id) = match intent {
730        McpIntent::RegisterAgent {
731            agent_id,
732            policy_cell_id,
733            agent_registry_id,
734        } => (agent_id, policy_cell_id, agent_registry_id),
735        _ => return Err("Wrong intent".into()),
736    };
737
738    // Registry must exist
739    let registry = state
740        .cells()
741        .cells
742        .get(agent_registry_id)
743        .ok_or("AgentRegistry cell not found")?;
744
745    // Policy cell must exist
746    if !state.cells().cells.contains_key(policy_cell_id) {
747        return Err("Policy cell not found".into());
748    }
749
750    // Owner and agent must be different accounts - agent cannot self-register.
751    // This ensures the owner can suspend/reinstate even if the agent key is compromised.
752    if &sender == agent_id {
753        return Err(
754            "Agent cannot register itself: owner and agent_id must be different accounts".into(),
755        );
756    }
757
758    // Agent must not already be registered
759    let existing_policy_slot = agent_reg_keys::agent_policy(agent_id);
760    if registry.storage.get(&existing_policy_slot) != Some(&[0u8; 32])
761        && registry.storage.contains_key(&existing_policy_slot)
762    {
763        return Err("Agent already registered".into());
764    }
765
766    let current_count = read_u64_from_storage(registry, &agent_reg_keys::AGENT_COUNT);
767    let new_count = current_count.checked_add(1).ok_or("Agent count overflow")?;
768
769    let mut diff = StateDiff::default();
770    diff.cell_updates.push(CellUpdate::StorageChange {
771        cell_id: *agent_registry_id,
772        storage_diff: {
773            let mut m = HashMap::new();
774            m.insert(
775                agent_reg_keys::agent_policy(agent_id),
776                Some(*policy_cell_id),
777            );
778            m.insert(agent_reg_keys::agent_owner(agent_id), Some(sender));
779            m.insert(agent_reg_keys::agent_entry(current_count), Some(*agent_id));
780            let mut ts_bytes = [0u8; 32];
781            ts_bytes[..8].copy_from_slice(&timestamp.to_le_bytes());
782            m.insert(
783                agent_reg_keys::agent_registered_at(agent_id),
784                Some(ts_bytes),
785            );
786            let mut count_bytes = [0u8; 32];
787            count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
788            m.insert(agent_reg_keys::AGENT_COUNT, Some(count_bytes));
789            m
790        },
791    });
792
793    // Initialize policy cell storage with safe defaults.
794    // Without this the policy cell is blank - enforce_mcp_policy reads zeros
795    // and the agent has no defined permissions.
796    diff.cell_updates.push(CellUpdate::StorageChange {
797        cell_id: *policy_cell_id,
798        storage_diff: {
799            let mut m = HashMap::new();
800            // STATUS = 0 (active)
801            m.insert(policy_keys::STATUS, Some([0u8; 32]));
802            // OWNER = sender
803            m.insert(policy_keys::OWNER, Some(sender));
804            // ALLOW_READS = 1
805            let mut allow_reads = [0u8; 32];
806            allow_reads[0] = 1;
807            m.insert(policy_keys::ALLOW_READS, Some(allow_reads));
808            // ALLOW_WRITES = 1
809            let mut allow_writes = [0u8; 32];
810            allow_writes[0] = 1;
811            m.insert(policy_keys::ALLOW_WRITES, Some(allow_writes));
812            // ALLOW_ADMIN = 0
813            m.insert(policy_keys::ALLOW_ADMIN, Some([0u8; 32]));
814            // RATE_LIMIT = 0 (unlimited)
815            m.insert(policy_keys::RATE_LIMIT, Some([0u8; 32]));
816            // SPEND_PER_TX = 0 (unlimited)
817            m.insert(policy_keys::SPEND_PER_TX, Some([0u8; 32]));
818            // SPEND_EPOCH = 0 (unlimited)
819            m.insert(policy_keys::SPEND_EPOCH, Some([0u8; 32]));
820            // TOTAL_ACTIONS = 0
821            m.insert(policy_keys::TOTAL_ACTIONS, Some([0u8; 32]));
822            m
823        },
824    });
825
826    diff.cu_fee = gp::get_u64(gp::PARAM_GAS_TRANSFER) as u128;
827    Ok(diff)
828}
829
830/// Compute the StateDiff for SuspendAgent / ReinstateAgent.
831pub fn diff_set_agent_status(
832    state: &impl McpStateView,
833    sender: AccountId,
834    intent: &McpIntent,
835) -> Result<StateDiff, String> {
836    let (agent_id, registry_id, new_status, reason) = match intent {
837        McpIntent::SuspendAgent {
838            agent_id,
839            agent_registry_id,
840            reason,
841        } => (agent_id, agent_registry_id, 1u8, reason.as_str()),
842        McpIntent::ReinstateAgent {
843            agent_id,
844            agent_registry_id,
845        } => (agent_id, agent_registry_id, 0u8, ""),
846        _ => return Err("Wrong intent".into()),
847    };
848
849    let registry = state
850        .cells()
851        .cells
852        .get(registry_id)
853        .ok_or("AgentRegistry not found")?;
854
855    // Only the registered owner can suspend/reinstate
856    let owner_slot = agent_reg_keys::agent_owner(agent_id);
857    let stored_owner = registry
858        .storage
859        .get(&owner_slot)
860        .copied()
861        .unwrap_or([0u8; 32]);
862    if stored_owner != sender {
863        return Err("Only the agent's registered owner can change agent status".into());
864    }
865
866    // Get the policy cell for this agent and update its STATUS key
867    let policy_id_bytes = registry
868        .storage
869        .get(&agent_reg_keys::agent_policy(agent_id))
870        .copied()
871        .ok_or("Agent not registered")?;
872
873    let mut diff = StateDiff::default();
874
875    // Update STATUS in the policy cell
876    let mut status_bytes = [0u8; 32];
877    status_bytes[0] = new_status;
878    diff.cell_updates.push(CellUpdate::StorageChange {
879        cell_id: policy_id_bytes,
880        storage_diff: {
881            let mut m = HashMap::new();
882            m.insert(policy_keys::STATUS, Some(status_bytes));
883            if !reason.is_empty() {
884                // Store first 32 bytes of reason hash
885                let reason_hash = *blake3::hash(reason.as_bytes()).as_bytes();
886                m.insert(policy_keys::SUSPEND_REASON, Some(reason_hash));
887            }
888            m
889        },
890    });
891
892    diff.cu_fee = gp::get_u64(gp::PARAM_GAS_TRANSFER) as u128;
893    Ok(diff)
894}
895
896/// Compile McpToolCall into a CallCellChain intent.
897///
898/// This is the key transformation. The McpToolCall intent is NOT a new execution
899/// path - it compiles into the existing CallCellChain, which the parallel
900/// executor already knows how to handle. The atomicity, rollback, and gas
901/// metering are inherited for free.
902///
903/// Call chain:
904///   [0] policy_cell(agent_id, tool_id, calldata_hash, value, timestamp)
905///       - Axiom policy logic: check status, rate limit, spend, tool permission
906///       - returns 1 byte: 0x01=permit, 0x00=deny
907///       - if deny: entire chain reverts
908///
909///   [1] tool_cell(agent_calldata)
910///       - Axiom tool logic: actual computation
911///       - return data = tool result
912///       - may read/write its declared storage slots
913///
914///   [2] (optional) action_log_cell(agent_id, tool_id, result_hash)
915///       - Axiom log appender: appends to the agent's on-chain action log
916///       - uses commutative Append storage key: zero conflict with other agents
917///       - uses result from [1] to hash the tool's output
918pub fn compile_tool_call_to_chain(intent: &McpIntent) -> Result<TransactionIntent, String> {
919    let (
920        agent_id,
921        tool_id,
922        tool_calldata,
923        value,
924        gas_limit,
925        policy_cell_id,
926        action_log_id,
927        timestamp,
928    ) = match intent {
929        McpIntent::McpToolCall {
930            agent_id,
931            tool_id,
932            tool_calldata,
933            value,
934            gas_limit,
935            policy_cell_id,
936            action_log_id,
937            timestamp,
938        } => (
939            agent_id,
940            tool_id,
941            tool_calldata,
942            value,
943            gas_limit,
944            policy_cell_id,
945            action_log_id,
946            timestamp,
947        ),
948        _ => return Err("Wrong intent type".into()),
949    };
950    let log_id = action_log_id
951        .as_ref()
952        .ok_or("McpToolCall requires action_log_id")?;
953
954    let calldata_hash = *blake3::hash(tool_calldata).as_bytes();
955    let mut policy_calldata = Vec::with_capacity(120);
956    policy_calldata.extend_from_slice(agent_id);
957    policy_calldata.extend_from_slice(tool_id);
958    policy_calldata.extend_from_slice(&calldata_hash);
959    policy_calldata.extend_from_slice(&value.to_le_bytes());
960    policy_calldata.extend_from_slice(&timestamp.to_le_bytes());
961
962    let mut calls = vec![
963        CellCall {
964            cell_id: *policy_cell_id,
965            calldata: policy_calldata,
966            value: 0,
967            use_result_from: None,
968        },
969        // Step 1: Tool execution
970        CellCall {
971            cell_id: *tool_id,
972            calldata: tool_calldata.clone(),
973            value: *value,
974            use_result_from: None, // Tool gets direct calldata, not policy result
975        },
976    ];
977
978    let mut log_calldata = Vec::with_capacity(96);
979    log_calldata.extend_from_slice(agent_id);
980    log_calldata.extend_from_slice(tool_id);
981    log_calldata.extend_from_slice(&timestamp.to_le_bytes());
982
983    calls.push(CellCall {
984        cell_id: *log_id,
985        calldata: log_calldata,
986        value: 0,
987        use_result_from: Some(1),
988    });
989
990    Ok(TransactionIntent::CallCellChain {
991        calls,
992        gas_limit: *gas_limit,
993    })
994}
995
996/// Compute the StateDiff for RegisterMcpResource.
997/// Deploys a resource cell and registers it in the McpRegistry.
998pub fn diff_register_resource(
999    state: &impl McpStateView,
1000    sender: AccountId,
1001    intent: &McpIntent,
1002    timestamp: u64,
1003) -> Result<StateDiff, String> {
1004    let (
1005        resource_id,
1006        bytecode,
1007        name,
1008        uri_scheme,
1009        mime_type,
1010        initial_data,
1011        declared_reads,
1012        declared_writes,
1013        oracle_schema_ids,
1014        registry_id,
1015    ) = match intent {
1016        McpIntent::RegisterMcpResource {
1017            resource_id,
1018            bytecode,
1019            name,
1020            uri_scheme,
1021            mime_type,
1022            initial_data,
1023            declared_reads,
1024            declared_writes,
1025            oracle_schema_ids,
1026            registry_id,
1027        } => (
1028            resource_id,
1029            bytecode,
1030            name,
1031            uri_scheme,
1032            mime_type,
1033            initial_data,
1034            declared_reads,
1035            declared_writes,
1036            oracle_schema_ids,
1037            registry_id,
1038        ),
1039        _ => return Err("Wrong intent".into()),
1040    };
1041
1042    if name.len() > 64 {
1043        return Err("Resource name too long".into());
1044    }
1045    if uri_scheme.len() > 64 {
1046        return Err("URI scheme too long".into());
1047    }
1048    if !bytecode.is_empty() {
1049        if bytecode.len() > gp::get_usize(gp::PARAM_MAX_CELL_BYTECODE_SIZE) {
1050            return Err("Resource bytecode exceeds max size".into());
1051        }
1052        if bytecode.len() < 4 || &bytecode[0..4] != b"AXIO" {
1053            return Err("Invalid Axiom bytecode: missing magic bytes".into());
1054        }
1055        truthlinked_runtime::cells::CellAccount::verify_manifest_against_bytecode(
1056            bytecode,
1057            declared_reads,
1058            declared_writes,
1059            &[],
1060        )?;
1061        truthlinked_runtime::cells::CellAccount::require_inferable(bytecode, &[])?;
1062    }
1063
1064    if state.cells().cells.contains_key(resource_id) {
1065        return Err(format!(
1066            "Resource cell {} already deployed",
1067            hex::encode(resource_id)
1068        ));
1069    }
1070
1071    let registry = state
1072        .cells()
1073        .cells
1074        .get(registry_id)
1075        .ok_or("McpRegistry cell not found")?;
1076
1077    // Rent-exempt deposit (refundable on close)
1078    let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
1079    let sender_account = state
1080        .accounts()
1081        .get(&sender)
1082        .ok_or("Sender account not found")?;
1083    if sender_account.balance < rent_deposit {
1084        return Err("Insufficient balance for rent deposit".into());
1085    }
1086
1087    // Build resource cell storage
1088    let mut resource_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
1089
1090    let mut name_bytes = [0u8; 32];
1091    let n = name.len().min(32);
1092    name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1093    resource_storage.insert(resource_keys::NAME, name_bytes);
1094
1095    let mut uri_bytes = [0u8; 32];
1096    let u = uri_scheme.len().min(32);
1097    uri_bytes[..u].copy_from_slice(&uri_scheme.as_bytes()[..u]);
1098    resource_storage.insert(resource_keys::URI_SCHEME, uri_bytes);
1099
1100    let mut mime_bytes = [0u8; 32];
1101    let m = mime_type.len().min(32);
1102    mime_bytes[..m].copy_from_slice(&mime_type.as_bytes()[..m]);
1103    resource_storage.insert(resource_keys::MIME_TYPE, mime_bytes);
1104
1105    // Timestamp
1106    let mut ts_bytes = [0u8; 32];
1107    ts_bytes[..8].copy_from_slice(&timestamp.to_le_bytes());
1108    resource_storage.insert(resource_keys::UPDATED_AT, ts_bytes);
1109
1110    // Initial data slots
1111    for (slot_key, content) in initial_data {
1112        if content.len() > 32 {
1113            return Err("Resource initial data values must be <= 32 bytes".into());
1114        }
1115        let storage_key = resource_keys::data_slot(slot_key);
1116        let mut value = [0u8; 32];
1117        let len = content.len().min(32);
1118        value[..len].copy_from_slice(&content[..len]);
1119        resource_storage.insert(storage_key, value);
1120    }
1121
1122    // Content hash of all initial data combined
1123    let all_data: Vec<u8> = initial_data
1124        .iter()
1125        .flat_map(|(k, v)| {
1126            let mut combined = k.clone();
1127            combined.extend_from_slice(v);
1128            combined
1129        })
1130        .collect();
1131    resource_storage.insert(
1132        resource_keys::CONTENT_HASH,
1133        *blake3::hash(&all_data).as_bytes(),
1134    );
1135
1136    let manifest_hash = truthlinked_runtime::cells::CellAccount::compute_manifest_hash(
1137        bytecode,
1138        declared_reads,
1139        declared_writes,
1140        &[],
1141        oracle_schema_ids,
1142    );
1143
1144    let resource_cell = truthlinked_runtime::cells::CellAccount {
1145        cell_id: *resource_id,
1146        owner: truthlinked_core::pq_execution::system_authority_id(),
1147        bytecode: bytecode.clone(),
1148        storage: resource_storage,
1149        balance: 0,
1150        rent_deposit,
1151        is_token: false,
1152        token_config: None,
1153        created_at: timestamp,
1154        upgraded_at: None,
1155        last_rent_paid_height: 0,
1156        rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
1157        pending_owner: None,
1158        is_immutable: false,
1159        declared_reads: declared_reads.clone(),
1160        declared_writes: declared_writes.clone(),
1161        commutative_keys: vec![],
1162        storage_key_specs: vec![],
1163        oracle_schema_ids: oracle_schema_ids.clone(),
1164        governance_proposal: None,
1165        manifest_version: 1,
1166        manifest_hash,
1167    };
1168
1169    let current_count = read_u64_from_storage(registry, &registry_keys::RESOURCE_COUNT);
1170    let new_count = current_count
1171        .checked_add(1)
1172        .ok_or("Resource count overflow")?;
1173
1174    let mut diff = StateDiff::default();
1175
1176    let mut sender_updated = sender_account.clone();
1177    sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
1178    diff.account_updates.insert(sender, sender_updated);
1179
1180    diff.cell_updates.push(CellUpdate::Deploy {
1181        cell_id: *resource_id,
1182        cell: resource_cell,
1183    });
1184
1185    diff.cell_updates.push(CellUpdate::StorageChange {
1186        cell_id: *registry_id,
1187        storage_diff: {
1188            let mut m = HashMap::new();
1189            let mut count_bytes = [0u8; 32];
1190            count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
1191            m.insert(registry_keys::RESOURCE_COUNT, Some(count_bytes));
1192            m.insert(
1193                registry_keys::resource_entry(current_count),
1194                Some(*resource_id),
1195            );
1196            m.insert(registry_keys::name_to_resource(name), Some(*resource_id));
1197            let ver = read_u64_from_storage(registry, &registry_keys::REGISTRY_VER);
1198            let mut ver_bytes = [0u8; 32];
1199            ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
1200            m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
1201            m
1202        },
1203    });
1204
1205    diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
1206    Ok(diff)
1207}
1208
1209/// Compute the StateDiff for RegisterMcpPrompt.
1210/// Deploys a prompt cell and registers it in the McpRegistry.
1211/// Prompts go through the validator name registry for on-chain approval.
1212pub fn diff_register_prompt(
1213    state: &impl McpStateView,
1214    sender: AccountId,
1215    intent: &McpIntent,
1216    timestamp: u64,
1217) -> Result<StateDiff, String> {
1218    let (prompt_id, name, template_bytes, arguments, registry_id) = match intent {
1219        McpIntent::RegisterMcpPrompt {
1220            prompt_id,
1221            name,
1222            template_bytes,
1223            arguments,
1224            registry_id,
1225        } => (prompt_id, name, template_bytes, arguments, registry_id),
1226        _ => return Err("Wrong intent".into()),
1227    };
1228
1229    if name.len() > 64 {
1230        return Err("Prompt name too long".into());
1231    }
1232    if arguments.len() > 255 {
1233        return Err("Too many arguments (max 255)".into());
1234    }
1235
1236    if state.cells().cells.contains_key(prompt_id) {
1237        return Err(format!(
1238            "Prompt cell {} already deployed",
1239            hex::encode(prompt_id)
1240        ));
1241    }
1242
1243    let registry = state
1244        .cells()
1245        .cells
1246        .get(registry_id)
1247        .ok_or("McpRegistry cell not found")?;
1248
1249    // Rent-exempt deposit (refundable on close)
1250    let rent_deposit = gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE);
1251    let sender_account = state
1252        .accounts()
1253        .get(&sender)
1254        .ok_or("Sender account not found")?;
1255    if sender_account.balance < rent_deposit {
1256        return Err("Insufficient balance for rent deposit".into());
1257    }
1258
1259    let mut prompt_storage: HashMap<[u8; 32], [u8; 32]> = HashMap::new();
1260
1261    let mut name_bytes = [0u8; 32];
1262    let n = name.len().min(32);
1263    name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1264    prompt_storage.insert(prompt_keys::NAME, name_bytes);
1265
1266    // Template hash - full template stored off-chain, hash lives on-chain
1267    let template_hash = *blake3::hash(template_bytes).as_bytes();
1268    prompt_storage.insert(prompt_keys::TEMPLATE_HASH, template_hash);
1269
1270    // Argument count
1271    let mut argc_bytes = [0u8; 32];
1272    argc_bytes[0] = arguments.len() as u8;
1273    prompt_storage.insert(prompt_keys::ARG_COUNT, argc_bytes);
1274
1275    // Each argument name (first 32 bytes)
1276    for (i, (arg_name, _desc, _required)) in arguments.iter().enumerate() {
1277        let mut arg_bytes = [0u8; 32];
1278        let n = arg_name.len().min(32);
1279        arg_bytes[..n].copy_from_slice(&arg_name.as_bytes()[..n]);
1280        prompt_storage.insert(prompt_keys::arg_schema(i as u8), arg_bytes);
1281    }
1282
1283    // approved_at = 0 (not yet validator-approved - requires name-registry system cell approval)
1284    prompt_storage.insert(prompt_keys::APPROVED_AT, [0u8; 32]);
1285    prompt_storage.insert(prompt_keys::USE_COUNT, [0u8; 32]);
1286
1287    let manifest_hash =
1288        truthlinked_runtime::cells::CellAccount::compute_manifest_hash(&[], &[], &[], &[], &[]);
1289
1290    let prompt_cell = truthlinked_runtime::cells::CellAccount {
1291        cell_id: *prompt_id,
1292        owner: truthlinked_core::pq_execution::system_authority_id(),
1293        bytecode: vec![], // Prompts have no executable bytecode - pure storage
1294        storage: prompt_storage,
1295        balance: 0,
1296        rent_deposit,
1297        is_token: false,
1298        token_config: None,
1299        created_at: timestamp,
1300        upgraded_at: None,
1301        last_rent_paid_height: 0,
1302        rent_grace_blocks: gp::get_u64(gp::PARAM_STORAGE_RENT_GRACE_PERIOD_BLOCKS),
1303        pending_owner: None,
1304        is_immutable: false,
1305        declared_reads: vec![],
1306        declared_writes: vec![],
1307        commutative_keys: vec![prompt_keys::USE_COUNT], // commutative: agents increment USE_COUNT concurrently
1308        storage_key_specs: vec![],
1309        oracle_schema_ids: vec![],
1310        governance_proposal: None,
1311        manifest_version: 1,
1312        manifest_hash,
1313    };
1314
1315    let current_count = read_u64_from_storage(registry, &registry_keys::PROMPT_COUNT);
1316    let new_count = current_count
1317        .checked_add(1)
1318        .ok_or("Prompt count overflow")?;
1319
1320    let mut diff = StateDiff::default();
1321
1322    let mut sender_updated = sender_account.clone();
1323    sender_updated.balance = sender_updated.balance.saturating_sub(rent_deposit);
1324    diff.account_updates.insert(sender, sender_updated);
1325
1326    diff.cell_updates.push(CellUpdate::Deploy {
1327        cell_id: *prompt_id,
1328        cell: prompt_cell,
1329    });
1330
1331    diff.cell_updates.push(CellUpdate::StorageChange {
1332        cell_id: *registry_id,
1333        storage_diff: {
1334            let mut m = HashMap::new();
1335            let mut count_bytes = [0u8; 32];
1336            count_bytes[..8].copy_from_slice(&new_count.to_le_bytes());
1337            m.insert(registry_keys::PROMPT_COUNT, Some(count_bytes));
1338            m.insert(registry_keys::prompt_entry(current_count), Some(*prompt_id));
1339            m.insert(registry_keys::name_to_prompt(name), Some(*prompt_id));
1340            let ver = read_u64_from_storage(registry, &registry_keys::REGISTRY_VER);
1341            let mut ver_bytes = [0u8; 32];
1342            ver_bytes[..8].copy_from_slice(&(ver + 1).to_le_bytes());
1343            m.insert(registry_keys::REGISTRY_VER, Some(ver_bytes));
1344            m
1345        },
1346    });
1347
1348    // NOTE: Prompt is DEPLOYED but NOT YET APPROVED.
1349    // The deployer must then call the name-registry system cell and wait for validator votes.
1350    // Only approved prompts appear in prompts/list (filtered by APPROVED_AT != 0).
1351    diff.cu_fee = gp::get_u64(gp::PARAM_GAS_DEPLOY_CELL) as u128;
1352    Ok(diff)
1353}
1354
1355//
1356// MCP PROTOCOL TRANSPORT
1357// This is the ONLY part that is not on-chain. It is the thinnest possible
1358// wire adapter: JSON-RPC 2.0 over WebSocket. It translates MCP method calls
1359// into TruthLinked transactions and on-chain storage reads.
1360// There is NO policy enforcement here. The chain does that.
1361//
1362
1363// CHAIN STATE READERS
1364// These read from cell storage - the chain IS the data source.
1365//
1366
1367/// Read all registered tools from McpRegistry storage.
1368/// Filters to only tools the agent is permitted to call per their policy.
1369fn is_native_mcp_tool(name: &str) -> bool {
1370    matches!(
1371        name,
1372        "get_chain_info"
1373            | "get_balance"
1374            | "get_validators"
1375            | "get_token_info"
1376            | "get_cell_info"
1377            | "get_transaction"
1378            | "get_staking_info"
1379            | "get_oracle_result"
1380            | "get_account_history"
1381            | "submit_transaction"
1382            | "http_fetch"
1383            | "get_sdk"
1384            | "faucet"
1385    )
1386}
1387
1388pub fn enumerate_tools(
1389    state: &impl McpStateView,
1390    registry_id: &AccountId,
1391    agent_reg_id: &AccountId,
1392    agent_id: &AccountId,
1393) -> Vec<serde_json::Value> {
1394    let registry = match state.cells().cells.get(registry_id) {
1395        Some(r) => r,
1396        None => return vec![],
1397    };
1398
1399    let tool_count = read_u64_from_storage(registry, &registry_keys::TOOL_COUNT);
1400
1401    // Get agent's policy cell to filter permitted tools
1402    let policy_cell = state.cells().cells.get(agent_reg_id).and_then(|ar| {
1403        let pol_id = ar
1404            .storage
1405            .get(&agent_reg_keys::agent_policy(agent_id))
1406            .copied()?;
1407        state.cells().cells.get(&pol_id)
1408    });
1409
1410    (0..tool_count).filter_map(|i| {
1411        let tool_id = registry.storage.get(&registry_keys::tool_entry(i)).copied()?;
1412        let tool = state.cells().cells.get(&tool_id)?;
1413
1414        // Check if enabled
1415        let enabled = tool.storage.get(&tool_keys::ENABLED)
1416            .map(|b| b[0] == 1).unwrap_or(false);
1417        if !enabled { return None; }
1418
1419        // Check policy permission for this tool
1420        if let Some(policy) = policy_cell {
1421            let perm = policy.storage.get(&policy_keys::tool_permission(&tool_id))
1422                .map(|b| b[0] == 1)
1423                .unwrap_or(false);
1424            // If allow_reads is true and tool is category 0, permit regardless
1425            let allow_reads = policy.storage.get(&policy_keys::ALLOW_READS)
1426                .map(|b| b[0] == 1).unwrap_or(false);
1427            let category = tool.storage.get(&tool_keys::CATEGORY)
1428                .map(|b| b[0]).unwrap_or(0);
1429            if !perm && !(allow_reads && category == 0) {
1430                return None;
1431            }
1432        }
1433
1434        let name_bytes = tool.storage.get(&tool_keys::NAME).copied().unwrap_or([0u8; 32]);
1435        let name = String::from_utf8_lossy(name_bytes.split(|&b| b == 0).next().unwrap_or(&[]))
1436            .to_string();
1437
1438        if !is_native_mcp_tool(&name) {
1439            return None;
1440        }
1441
1442        let builtin_schemas: std::collections::HashMap<&str, serde_json::Value> = [
1443            ("get_chain_info",      serde_json::json!({"type":"object","properties":{},"required":[]})),
1444            ("get_balance",         serde_json::json!({"type":"object","properties":{"account_id":{"type":"string","description":"32-byte account ID as hex"}},"required":["account_id"]})),
1445            ("get_validators",      serde_json::json!({"type":"object","properties":{},"required":[]})),
1446            ("get_token_info",      serde_json::json!({"type":"object","properties":{},"required":[]})),
1447            ("get_cell_info",       serde_json::json!({"type":"object","properties":{"cell_id":{"type":"string","description":"32-byte cell ID as hex"}},"required":["cell_id"]})),
1448            ("get_transaction",     serde_json::json!({"type":"object","properties":{"tx_hash":{"type":"string","description":"32-byte tx hash as hex"}},"required":["tx_hash"]})),
1449            ("get_staking_info",    serde_json::json!({"type":"object","properties":{},"required":[]})),
1450            ("get_oracle_result",   serde_json::json!({"type":"object","properties":{"request_id":{"type":"string","description":"32-byte oracle request ID as hex"}},"required":["request_id"]})),
1451            ("get_account_history", serde_json::json!({"type":"object","properties":{"account_id":{"type":"string"},"limit":{"type":"integer","default":20}},"required":["account_id"]})),
1452            ("submit_transaction",  serde_json::json!({"type":"object","properties":{"tx_hex":{"type":"string","description":"Hex-encoded bincode-serialized signed Transaction"}},"required":["tx_hex"]})),
1453            ("http_fetch",          serde_json::json!({"type":"object","properties":{"url":{"type":"string"},"cell_id":{"type":"string","description":"Cell that will consume the result (64-char hex)"}},"required":["url","cell_id"]})),
1454            ("get_sdk",             serde_json::json!({"type":"object","properties":{},"required":[]})),
1455            ("faucet",              serde_json::json!({"type":"object","properties":{"account_id":{"type":"string","description":"Your 64-char hex account ID"},"pubkey":{"type":"string","description":"Your ML-DSA public key hex"}},"required":["account_id","pubkey"]})),
1456        ].into_iter().collect();
1457
1458        let builtin_descs: std::collections::HashMap<&str, &str> = [
1459            ("get_chain_info",      "Get current chain height, genesis hash, finalized height, and network info."),
1460            ("get_balance",         "Get TRTH balance for an account. Input: account_id as 64-char hex."),
1461            ("get_validators",      "List all active validators with stake and status."),
1462            ("get_token_info",      "Get TRTH token metadata: name, symbol, decimals, total supply."),
1463            ("get_cell_info",       "Get cell account info by cell_id as 64-char hex."),
1464            ("get_transaction",     "Get transaction details by tx_hash as 64-char hex."),
1465            ("get_staking_info",    "Get staking state: total staked, validator count, epoch info."),
1466            ("get_oracle_result",   "Poll the result of an oracle HTTP fetch by request_id."),
1467            ("get_account_history", "Get recent transaction history for an account."),
1468            ("submit_transaction",  "Submit a pre-signed transaction (hex-encoded bincode). Returns tx_hash."),
1469            ("http_fetch",          "Queue an oracle HTTP GET. Validators fetch and commit-reveal the response. Returns request_id."),
1470            ("get_sdk",              "Get Axiom CLI build and usage instructions. Axiom CLI generates ML-DSA-65 keypairs, signs transactions, and submits them to TruthLinked."),
1471            ("faucet",               "Claim testnet TRTH tokens with Axiom CLI. Run axiom account-create first, then axiom faucet --from mykeys.json. 12-hour cooldown per account."),
1472        ].into_iter().collect();
1473
1474        let input_schema = builtin_schemas.get(name.as_str())
1475            .cloned()
1476            .unwrap_or_else(|| serde_json::json!({"type":"object"}));
1477        let description = builtin_descs.get(name.as_str())
1478            .map(|s| s.to_string())
1479            .unwrap_or_else(|| format!("On-chain tool cell {}.", &hex::encode(tool_id)[..8]));
1480
1481        Some(serde_json::json!({
1482            "name": name,
1483            "description": description,
1484            "inputSchema": input_schema,
1485            "_meta": {
1486                "cell_id": hex::encode(tool_id),
1487                "category": tool.storage.get(&tool_keys::CATEGORY).map(|b| b[0]).unwrap_or(0),
1488                "manifest_version": tool.manifest_version,
1489                "manifest_hash": hex::encode(tool.manifest_hash),
1490                "call_count": read_u64_from_storage(tool, &tool_keys::CALL_COUNT)
1491            }
1492        }))
1493    }).collect()
1494}
1495
1496pub fn enumerate_resources(
1497    state: &impl McpStateView,
1498    registry_id: &AccountId,
1499) -> Vec<serde_json::Value> {
1500    let registry = match state.cells().cells.get(registry_id) {
1501        Some(r) => r,
1502        None => return vec![],
1503    };
1504
1505    let count = read_u64_from_storage(registry, &registry_keys::RESOURCE_COUNT);
1506
1507    (0..count).filter_map(|i| {
1508        let res_id = registry.storage.get(&registry_keys::resource_entry(i)).copied()?;
1509        let res = state.cells().cells.get(&res_id)?;
1510
1511        let name_bytes = res.storage.get(&resource_keys::NAME).copied().unwrap_or([0u8; 32]);
1512        let name = utf8_from_padded(&name_bytes);
1513
1514        let uri_bytes = res.storage.get(&resource_keys::URI_SCHEME).copied().unwrap_or([0u8; 32]);
1515        let uri_scheme = utf8_from_padded(&uri_bytes);
1516
1517        let mime_bytes = res.storage.get(&resource_keys::MIME_TYPE).copied().unwrap_or([0u8; 32]);
1518        let mime = utf8_from_padded(&mime_bytes);
1519
1520        Some(serde_json::json!({
1521            "uri": format!("trth://{}", hex::encode(res_id)),
1522            "name": name,
1523            "description": format!("On-chain resource cell. URI scheme: {}. Manifest v{}.", uri_scheme, res.manifest_version),
1524            "mimeType": mime
1525        }))
1526    }).collect()
1527}
1528
1529pub fn enumerate_prompts(
1530    state: &impl McpStateView,
1531    registry_id: &AccountId,
1532) -> Vec<serde_json::Value> {
1533    let registry = match state.cells().cells.get(registry_id) {
1534        Some(r) => r,
1535        None => return vec![],
1536    };
1537
1538    let count = read_u64_from_storage(registry, &registry_keys::PROMPT_COUNT);
1539
1540    (0..count).filter_map(|i| {
1541        let prompt_id = registry.storage.get(&registry_keys::prompt_entry(i)).copied()?;
1542        let prompt = state.cells().cells.get(&prompt_id)?;
1543
1544        let approved_at = read_u64_from_storage(prompt, &prompt_keys::APPROVED_AT);
1545        if approved_at == 0 {
1546            return None;
1547        }
1548
1549        let name_bytes = prompt.storage.get(&prompt_keys::NAME).copied().unwrap_or([0u8; 32]);
1550        let name = utf8_from_padded(&name_bytes);
1551
1552        let arg_count = prompt.storage.get(&prompt_keys::ARG_COUNT)
1553            .map(|b| b[0] as u8).unwrap_or(0);
1554
1555        let arguments: Vec<serde_json::Value> = (0..arg_count).map(|j| {
1556            let arg_bytes = prompt.storage.get(&prompt_keys::arg_schema(j))
1557                .copied().unwrap_or([0u8; 32]);
1558            serde_json::json!({
1559                "name": utf8_from_padded(&arg_bytes),
1560                "required": true
1561            })
1562        }).collect();
1563
1564        Some(serde_json::json!({
1565            "name": name,
1566            "description": format!("On-chain prompt cell {}. Validator-approved. Manifest v{}.", &hex::encode(prompt_id)[..8], prompt.manifest_version),
1567            "arguments": arguments,
1568            "_meta": {
1569                "cell_id": hex::encode(prompt_id),
1570                "manifest_hash": hex::encode(prompt.manifest_hash),
1571                "use_count": read_u64_from_storage(prompt, &prompt_keys::USE_COUNT)
1572            }
1573        }))
1574    }).collect()
1575}
1576
1577pub fn read_resource(state: &impl McpStateView, registry_id: &AccountId, uri: &str) -> String {
1578    // URI format: trth://<64-hex-cell-id>
1579    //             trth://<64-hex-cell-id>/<slot-key-hex>
1580    //             trth://name/<resource-name>/<slot-key-hex>
1581    let path = uri.trim_start_matches("trth://");
1582    let parts: Vec<&str> = path.splitn(3, '/').collect();
1583
1584    let (cell_id_hex, slot_key_opt) = match parts.as_slice() {
1585        [cid] => (*cid, None),
1586        [cid, slot] => (*cid, Some(*slot)),
1587        ["name", name, slot] => {
1588            // Look up by name via registry
1589            if let Some(registry) = state.cells().cells.get(registry_id) {
1590                let res_id_bytes = registry.storage.get(&registry_keys::name_to_resource(name));
1591                if let Some(id_bytes) = res_id_bytes {
1592                    return read_resource(
1593                        state,
1594                        registry_id,
1595                        &format!("trth://{}/{}", hex::encode(id_bytes), slot),
1596                    );
1597                }
1598            }
1599            return serde_json::json!({ "error": "Resource name not found" }).to_string();
1600        }
1601        _ => return serde_json::json!({ "error": "Invalid URI format" }).to_string(),
1602    };
1603
1604    let cell_id_bytes = match hex::decode(cell_id_hex) {
1605        Ok(b) if b.len() == 32 => {
1606            let mut a = [0u8; 32];
1607            a.copy_from_slice(&b);
1608            a
1609        }
1610        _ => return serde_json::json!({ "error": "Invalid cell ID in URI" }).to_string(),
1611    };
1612
1613    let cell = match state.cells().cells.get(&cell_id_bytes) {
1614        Some(c) => c,
1615        None => return serde_json::json!({ "error": "Resource cell not found" }).to_string(),
1616    };
1617
1618    if let Some(slot_hex) = slot_key_opt {
1619        let slot_bytes = match hex::decode(slot_hex) {
1620            Ok(b) if b.len() == 32 => {
1621                let mut a = [0u8; 32];
1622                a.copy_from_slice(&b);
1623                a
1624            }
1625            Ok(b) => resource_keys::data_slot(&b),
1626            _ => return serde_json::json!({ "error": "Invalid slot key" }).to_string(),
1627        };
1628
1629        let value = cell
1630            .storage
1631            .get(&slot_bytes)
1632            .map(|v| hex::encode(v))
1633            .unwrap_or_else(|| {
1634                "0000000000000000000000000000000000000000000000000000000000000000".into()
1635            });
1636
1637        serde_json::json!({
1638            "cell_id": hex::encode(cell_id_bytes),
1639            "slot_key": hex::encode(slot_bytes),
1640            "value": value,
1641            "manifest_version": cell.manifest_version
1642        })
1643        .to_string()
1644    } else {
1645        // Read all storage (up to 256 slots - do not dump unbounded state)
1646        let slots: HashMap<String, String> = cell
1647            .storage
1648            .iter()
1649            .take(256)
1650            .map(|(k, v)| (hex::encode(k), hex::encode(v)))
1651            .collect();
1652
1653        serde_json::json!({
1654            "cell_id":     hex::encode(cell_id_bytes),
1655            "manifest_version": cell.manifest_version,
1656            "manifest_hash":   hex::encode(cell.manifest_hash),
1657            "storage_slots":   slots.len(),
1658            "storage":         slots
1659        })
1660        .to_string()
1661    }
1662}
1663
1664pub fn get_prompt(
1665    state: &impl McpStateView,
1666    registry_id: &AccountId,
1667    name: &str,
1668) -> Option<serde_json::Value> {
1669    let registry = state.cells().cells.get(registry_id)?;
1670    let prompt_id = registry
1671        .storage
1672        .get(&registry_keys::name_to_prompt(name))
1673        .copied()?;
1674    let prompt = state.cells().cells.get(&prompt_id)?;
1675
1676    let template_hash = prompt
1677        .storage
1678        .get(&prompt_keys::TEMPLATE_HASH)
1679        .map(hex::encode)
1680        .unwrap_or_default();
1681
1682    let template_preview = if template_hash.len() >= 8 {
1683        &template_hash[..8]
1684    } else {
1685        &template_hash
1686    };
1687
1688    Some(serde_json::json!({
1689        "messages": [{
1690            "role": "user",
1691            "content": {
1692                "type": "text",
1693                "text": format!(
1694                    "On-chain prompt '{}'. Template hash: {}. Fetch full template via: resources/read trth://{}/template",
1695                    name, template_preview, hex::encode(prompt_id)
1696                )
1697            }
1698        }],
1699        "_meta": {
1700            "cell_id":  hex::encode(prompt_id),
1701            "template_hash": template_hash,
1702            "manifest_version": prompt.manifest_version,
1703            "manifest_hash": hex::encode(prompt.manifest_hash)
1704        }
1705    }))
1706}
1707
1708//
1709// CHAIN GENESIS HELPERS
1710// Called during chain genesis to deploy the protocol cells.
1711//
1712
1713/// Canonical MCP protocol cell addresses (well-known, deterministic)
1714pub mod protocol_addresses {
1715    /// blake3("truthlinked:mcp:registry:v1")[..32]
1716    pub fn mcp_registry() -> [u8; 32] {
1717        *blake3::hash(b"truthlinked:mcp:registry:v1").as_bytes()
1718    }
1719    /// blake3("truthlinked:mcp:agent_registry:v1")[..32]
1720    pub fn agent_registry() -> [u8; 32] {
1721        *blake3::hash(b"truthlinked:mcp:agent_registry:v1").as_bytes()
1722    }
1723    /// blake3("truthlinked:mcp:action_log:v1")[..32]
1724    pub fn action_log() -> [u8; 32] {
1725        *blake3::hash(b"truthlinked:mcp:action_log:v1").as_bytes()
1726    }
1727}
1728
1729/// Deploy all MCP protocol cells at genesis.
1730/// This is called from genesis.rs during chain initialization.
1731pub fn deploy_mcp_genesis_cells(
1732    cell_state: &mut CellState,
1733    genesis_authority: AccountId,
1734    timestamp: u64,
1735) -> Result<(), String> {
1736    // McpRegistry: no bytecode - pure storage cell, governed by chain rules
1737    cell_state.deploy_cell(
1738        protocol_addresses::mcp_registry(),
1739        genesis_authority,
1740        vec![], // No bytecode needed - storage is read directly by the transport
1741        {
1742            let mut m = HashMap::new();
1743            let zero = [0u8; 32];
1744            m.insert(registry_keys::TOOL_COUNT, zero);
1745            m.insert(registry_keys::RESOURCE_COUNT, zero);
1746            m.insert(registry_keys::PROMPT_COUNT, zero);
1747            m.insert(registry_keys::REGISTRY_VER, zero);
1748            m
1749        },
1750        0,
1751        timestamp,
1752        vec![
1753            registry_keys::TOOL_COUNT,
1754            registry_keys::RESOURCE_COUNT,
1755            registry_keys::PROMPT_COUNT,
1756        ],
1757        vec![
1758            registry_keys::TOOL_COUNT,
1759            registry_keys::RESOURCE_COUNT,
1760            registry_keys::PROMPT_COUNT,
1761            registry_keys::REGISTRY_VER,
1762        ],
1763        vec![], // No commutative keys - registry writes are serialized
1764        Vec::new(),
1765        Vec::new(),
1766    )?;
1767
1768    // AgentRegistry
1769    cell_state.deploy_cell(
1770        protocol_addresses::agent_registry(),
1771        genesis_authority,
1772        vec![],
1773        {
1774            let mut m = HashMap::new();
1775            m.insert(agent_reg_keys::AGENT_COUNT, [0u8; 32]);
1776            m
1777        },
1778        0,
1779        timestamp,
1780        vec![],
1781        vec![agent_reg_keys::AGENT_COUNT],
1782        vec![],
1783        Vec::new(),
1784        Vec::new(),
1785    )?;
1786
1787    // ActionLog: commutative Append per agent - zero conflict across agents
1788    cell_state.deploy_cell(
1789        protocol_addresses::action_log(),
1790        genesis_authority,
1791        vec![],
1792        HashMap::new(),
1793        0,
1794        timestamp,
1795        vec![],
1796        vec![],
1797        // Every agent's log slot is commutative
1798        vec![], // Populated dynamically as agents register
1799        Vec::new(),
1800        Vec::new(),
1801    )?;
1802
1803    // Register built-in read-only tools (no bytecode - transport handles natively)
1804    let builtin_tools: &[(&str, &str, u8, &str)] = &[
1805        ("get_chain_info",        "Get current chain height, genesis hash, finalized height, and network info.", 0,
1806         r#"{"type":"object","properties":{},"required":[]}"#),
1807        ("get_balance",           "Get TRTH balance for an account. Input: account_id as 64-char hex.", 0,
1808         r#"{"type":"object","properties":{"account_id":{"type":"string","description":"32-byte account ID as hex"}},"required":["account_id"]}"#),
1809        ("get_validators",        "List all active validators with stake and status.", 0,
1810         r#"{"type":"object","properties":{},"required":[]}"#),
1811        ("get_token_info",        "Get TRTH token metadata: name, symbol, decimals, total supply.", 0,
1812         r#"{"type":"object","properties":{},"required":[]}"#),
1813        ("get_cell_info",         "Get cell account info by cell_id as 64-char hex.", 0,
1814         r#"{"type":"object","properties":{"cell_id":{"type":"string","description":"32-byte cell ID as hex"}},"required":["cell_id"]}"#),
1815        ("get_transaction",       "Get transaction details by tx_hash as 64-char hex.", 0,
1816         r#"{"type":"object","properties":{"tx_hash":{"type":"string","description":"32-byte tx hash as hex"}},"required":["tx_hash"]}"#),
1817        ("get_staking_info",      "Get staking state: total staked, validator count, epoch info.", 0,
1818         r#"{"type":"object","properties":{},"required":[]}"#),
1819        ("get_oracle_result",     "Poll the result of an oracle HTTP fetch by request_id.", 0,
1820         r#"{"type":"object","properties":{"request_id":{"type":"string","description":"32-byte oracle request ID as hex"}},"required":["request_id"]}"#),
1821        ("get_account_history",   "Get recent transaction history for an account.", 0,
1822         r#"{"type":"object","properties":{"account_id":{"type":"string"},"limit":{"type":"integer","default":20}},"required":["account_id"]}"#),
1823        ("submit_transaction",    "Submit a pre-signed transaction (hex-encoded bincode). Returns tx_hash.", 1,
1824         r#"{"type":"object","properties":{"tx_hex":{"type":"string","description":"Hex-encoded bincode-serialized signed Transaction"}},"required":["tx_hex"]}"#),
1825        ("http_fetch",            "Queue an oracle HTTP GET request. Validators fetch and commit-reveal the response. Returns request_id to poll with get_oracle_result.", 1,
1826         r#"{"type":"object","properties":{"url":{"type":"string","description":"URL to fetch"},"cell_id":{"type":"string","description":"Cell that will consume the result (64-char hex)"}},"required":["url","cell_id"]}"#),
1827        ("get_sdk",               "Get Axiom CLI build and usage instructions. Axiom CLI generates ML-DSA-65 keypairs and signs transactions.", 0,
1828         r#"{"type":"object","properties":{},"required":[]}"#),
1829        ("faucet",                "Claim testnet TRTH tokens. Requires account_id and pubkey from axiom account-create. 12-hour cooldown per account.", 0,
1830         r#"{"type":"object","properties":{"account_id":{"type":"string","description":"Your 64-char hex account ID"},"pubkey":{"type":"string","description":"Your Dilithium public key hex"}},"required":["account_id","pubkey"]}"#),
1831    ];
1832
1833    let registry_id = protocol_addresses::mcp_registry();
1834
1835    for (i, (name, _desc, category, schema_json)) in builtin_tools.iter().enumerate() {
1836        let tool_id =
1837            *blake3::hash(format!("truthlinked:mcp:builtin:{}", name).as_bytes()).as_bytes();
1838
1839        let schema_hash = *blake3::hash(schema_json.as_bytes()).as_bytes();
1840        let mut name_bytes = [0u8; 32];
1841        let n = name.len().min(32);
1842        name_bytes[..n].copy_from_slice(&name.as_bytes()[..n]);
1843
1844        let mut cat_bytes = [0u8; 32];
1845        cat_bytes[0] = *category;
1846        let mut enabled = [0u8; 32];
1847        enabled[0] = 1;
1848        let mut count_bytes = [0u8; 32];
1849        count_bytes[..8].copy_from_slice(&(i as u64).to_le_bytes());
1850
1851        let mut tool_storage = HashMap::new();
1852        tool_storage.insert(tool_keys::NAME, name_bytes);
1853        tool_storage.insert(tool_keys::SCHEMA_HASH, schema_hash);
1854        tool_storage.insert(tool_keys::DESC_HASH, schema_hash);
1855        tool_storage.insert(tool_keys::CATEGORY, cat_bytes);
1856        tool_storage.insert(tool_keys::ENABLED, enabled);
1857        tool_storage.insert(tool_keys::OWNER, genesis_authority);
1858        tool_storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
1859
1860        cell_state.deploy_cell(
1861            tool_id,
1862            genesis_authority,
1863            vec![], // no bytecode - transport handles natively
1864            tool_storage,
1865            0,
1866            timestamp,
1867            vec![],
1868            vec![tool_keys::CALL_COUNT],
1869            vec![tool_keys::CALL_COUNT], // commutative - parallel calls do not conflict
1870            Vec::new(),
1871            Vec::new(),
1872        )?;
1873
1874        // Register in McpRegistry storage
1875        let idx = i as u64;
1876        let mut new_count = [0u8; 32];
1877        new_count[..8].copy_from_slice(&(idx + 1).to_le_bytes());
1878        let mut ver_bytes = [0u8; 32];
1879        ver_bytes[..8].copy_from_slice(&(idx + 1).to_le_bytes());
1880
1881        let registry = cell_state
1882            .cells
1883            .get_mut(&registry_id)
1884            .ok_or("McpRegistry cell missing")?;
1885        registry
1886            .storage
1887            .insert(registry_keys::tool_entry(idx), tool_id);
1888        registry
1889            .storage
1890            .insert(registry_keys::name_to_tool(name), tool_id);
1891        registry
1892            .storage
1893            .insert(registry_keys::TOOL_COUNT, new_count);
1894        registry
1895            .storage
1896            .insert(registry_keys::REGISTRY_VER, ver_bytes);
1897    }
1898
1899    tracing::info!(
1900        " MCP protocol cells deployed: registry={} agent_reg={} action_log={} builtin_tools={}",
1901        hex::encode(protocol_addresses::mcp_registry()),
1902        hex::encode(protocol_addresses::agent_registry()),
1903        hex::encode(protocol_addresses::action_log()),
1904        builtin_tools.len(),
1905    );
1906
1907    Ok(())
1908}
1909
1910//
1911// UTILITIES
1912//
1913
1914fn read_u64_from_storage(cell: &CellAccount, key: &[u8; 32]) -> u64 {
1915    cell.storage
1916        .get(key)
1917        .map(|b| u64::from_le_bytes(b[..8].try_into().unwrap_or([0u8; 8])))
1918        .unwrap_or(0)
1919}
1920
1921fn utf8_from_padded(bytes: &[u8; 32]) -> String {
1922    String::from_utf8_lossy(bytes.split(|&b| b == 0).next().unwrap_or(&[])).to_string()
1923}
1924
1925#[cfg(test)]
1926mod tests {
1927    use super::*;
1928
1929    struct TestState {
1930        cells: CellState,
1931        accounts: ImHashMap<AccountId, AccountRecord>,
1932        params: ImHashMap<[u8; 32], [u8; 32]>,
1933    }
1934
1935    impl McpStateView for TestState {
1936        fn cells(&self) -> &CellState {
1937            &self.cells
1938        }
1939
1940        fn accounts(&self) -> &ImHashMap<AccountId, AccountRecord> {
1941            &self.accounts
1942        }
1943    }
1944
1945    impl truthlinked_governance::params::ParamState for TestState {
1946        fn params(&self) -> &ImHashMap<[u8; 32], [u8; 32]> {
1947            &self.params
1948        }
1949
1950        fn params_mut(&mut self) -> &mut ImHashMap<[u8; 32], [u8; 32]> {
1951            &mut self.params
1952        }
1953    }
1954
1955    fn setup_state_with_registry() -> TestState {
1956        let mut state = TestState {
1957            cells: CellState::new(),
1958            accounts: ImHashMap::new(),
1959            params: ImHashMap::new(),
1960        };
1961        truthlinked_governance::params::insert_genesis_params(&mut state);
1962        truthlinked_governance::params::rehydrate_from_state(&state);
1963
1964        let genesis_authority = [1u8; 32];
1965        deploy_mcp_genesis_cells(&mut state.cells, genesis_authority, 0).unwrap();
1966        assert!(
1967            truthlinked_governance::params::get_param_by_key(
1968                &truthlinked_governance::params::param_key(
1969                    truthlinked_governance::params::PARAM_MAX_CELL_BYTECODE_SIZE,
1970                )
1971            )
1972            .is_some(),
1973            "MCP tests require max cell bytecode genesis param"
1974        );
1975        state
1976    }
1977
1978    #[test]
1979    fn register_tool_rejects_invalid_bytecode() {
1980        let mut state = setup_state_with_registry();
1981        state.accounts.insert(
1982            [9u8; 32],
1983            AccountRecord {
1984                pubkey_bytes: vec![],
1985                balance: gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE) * 2,
1986                compute_escrow_trth: 0,
1987                nonce: 0,
1988                nfts: vec![],
1989            },
1990        );
1991        let intent = McpIntent::RegisterMcpTool {
1992            tool_id: [2u8; 32],
1993            bytecode: vec![1, 2, 3], // invalid Axiom bytecode
1994            name: "tool".into(),
1995            input_schema_json: b"{}".to_vec(),
1996            category: 0,
1997            declared_reads: vec![],
1998            declared_writes: vec![],
1999            commutative_keys: vec![],
2000            oracle_schema_ids: vec![],
2001            registry_id: protocol_addresses::mcp_registry(),
2002        };
2003        let err = diff_register_tool(&state, [9u8; 32], &intent, 0).unwrap_err();
2004        assert!(
2005            err.contains("Invalid Axiom bytecode") || err.contains("Invalid bytecode"),
2006            "got: {err}"
2007        );
2008    }
2009
2010    #[test]
2011    fn enumerate_tools_hides_custom_registered_tools() {
2012        let mut state = setup_state_with_registry();
2013        let registry_id = protocol_addresses::mcp_registry();
2014        let custom_tool_id = [7u8; 32];
2015
2016        let mut name_bytes = [0u8; 32];
2017        name_bytes[.."custom-tool".len()].copy_from_slice(b"custom-tool");
2018        let mut category = [0u8; 32];
2019        category[0] = 1;
2020        let mut enabled = [0u8; 32];
2021        enabled[0] = 1;
2022        let mut storage = HashMap::new();
2023        storage.insert(tool_keys::NAME, name_bytes);
2024        storage.insert(tool_keys::CATEGORY, category);
2025        storage.insert(tool_keys::ENABLED, enabled);
2026        storage.insert(tool_keys::SCHEMA_HASH, [0u8; 32]);
2027        storage.insert(tool_keys::DESC_HASH, [0u8; 32]);
2028        storage.insert(tool_keys::OWNER, [1u8; 32]);
2029        storage.insert(tool_keys::CALL_COUNT, [0u8; 32]);
2030
2031        state
2032            .cells
2033            .deploy_cell(
2034                custom_tool_id,
2035                [1u8; 32],
2036                vec![],
2037                storage,
2038                0,
2039                0,
2040                vec![],
2041                vec![tool_keys::CALL_COUNT],
2042                vec![tool_keys::CALL_COUNT],
2043                Vec::new(),
2044                Vec::new(),
2045            )
2046            .unwrap();
2047
2048        let registry = state.cells.cells.get_mut(&registry_id).unwrap();
2049        let count = read_u64_from_storage(registry, &registry_keys::TOOL_COUNT);
2050        registry
2051            .storage
2052            .insert(registry_keys::tool_entry(count), custom_tool_id);
2053        let mut new_count = [0u8; 32];
2054        new_count[..8].copy_from_slice(&(count + 1).to_le_bytes());
2055        registry
2056            .storage
2057            .insert(registry_keys::TOOL_COUNT, new_count);
2058
2059        let tools = enumerate_tools(
2060            &state,
2061            &registry_id,
2062            &protocol_addresses::agent_registry(),
2063            &[0u8; 32],
2064        );
2065        assert!(tools
2066            .iter()
2067            .any(|tool| tool.get("name").and_then(|v| v.as_str()) == Some("get_chain_info")));
2068        assert!(!tools
2069            .iter()
2070            .any(|tool| tool.get("name").and_then(|v| v.as_str()) == Some("custom-tool")));
2071    }
2072
2073    #[test]
2074    fn register_resource_rejects_large_initial_data() {
2075        let mut state = setup_state_with_registry();
2076        state.accounts.insert(
2077            [9u8; 32],
2078            AccountRecord {
2079                pubkey_bytes: vec![],
2080                balance: gp::get_u128(gp::PARAM_STORAGE_RENT_LIFETIME_FEE) * 2,
2081                compute_escrow_trth: 0,
2082                nonce: 0,
2083                nfts: vec![],
2084            },
2085        );
2086        let intent = McpIntent::RegisterMcpResource {
2087            resource_id: [3u8; 32],
2088            bytecode: vec![],
2089            name: "res".into(),
2090            uri_scheme: "trth".into(),
2091            mime_type: "application/json".into(),
2092            initial_data: vec![(b"slot".to_vec(), vec![0u8; 33])],
2093            declared_reads: vec![],
2094            declared_writes: vec![],
2095            oracle_schema_ids: vec![],
2096            registry_id: protocol_addresses::mcp_registry(),
2097        };
2098        let err = diff_register_resource(&state, [9u8; 32], &intent, 0).unwrap_err();
2099        assert!(err.contains("<= 32 bytes"));
2100    }
2101}