Skip to main content

newton_cli/commands/
policy.rs

1use alloy::primitives::Address;
2use clap::{Parser, Subcommand};
3use eyre::{Context, Result};
4use newton_prover_chainio::policy::PolicyController;
5use newton_prover_core::{
6    common::parse_intent,
7    config::NewtonAvsConfig,
8    rego::{evaluate, KycIdentityData, Value as RegoValue},
9};
10use serde_json::Value;
11use std::path::PathBuf;
12use tracing::{self, info};
13
14use crate::{commands::utils, config::NewtonCliConfig};
15
16/// Policy commands
17#[derive(Debug, Parser)]
18#[command(name = "policy")]
19pub struct PolicyCommand {
20    #[command(subcommand)]
21    pub subcommand: PolicySubcommand,
22}
23
24#[derive(Debug, Subcommand)]
25pub enum PolicySubcommand {
26    /// Deploy policy
27    Deploy(DeployCommand),
28    /// Simulate Rego policy execution
29    Simulate(SimulateCommand),
30}
31
32fn validate_non_empty_path(s: &str) -> Result<PathBuf, String> {
33    if s.trim().is_empty() {
34        Err(String::from("Path cannot be empty"))
35    } else {
36        Ok(PathBuf::from(s))
37    }
38}
39
40/// Normalize intent JSON by converting value and chainId to number strings
41/// Supports: number strings, numbers, and hex strings (with or without 0x prefix)
42fn normalize_intent(mut intent: serde_json::Value) -> eyre::Result<serde_json::Value> {
43    // Normalize value field
44    if let Some(value_field) = intent.get_mut("value") {
45        let normalized = normalize_number_field(value_field).with_context(|| "Failed to normalize value field")?;
46        *value_field = serde_json::Value::String(normalized);
47    }
48
49    // Normalize chainId field (optional)
50    if let Some(chain_id_field) = intent.get_mut("chainId") {
51        let normalized = normalize_number_field(chain_id_field).with_context(|| "Failed to normalize chainId field")?;
52        *chain_id_field = serde_json::Value::String(normalized);
53    }
54
55    Ok(intent)
56}
57
58/// Normalize a number field that can be a string, number, or hex string
59/// Returns a decimal number string
60fn normalize_number_field(value: &serde_json::Value) -> eyre::Result<String> {
61    match value {
62        serde_json::Value::String(s) => {
63            // Check if it's a hex string
64            if s.starts_with("0x") || s.starts_with("0X") {
65                // Parse hex string - try as u64 first, then U256 for very large values
66                let hex_str = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap();
67
68                // Try parsing as u64 first (for chainId)
69                if let Ok(num) = u64::from_str_radix(hex_str, 16) {
70                    Ok(num.to_string())
71                } else {
72                    // Try parsing as U256 (for large value fields)
73                    let num = alloy::primitives::U256::from_str_radix(hex_str, 16)
74                        .map_err(|e| eyre::eyre!("Invalid hex string '{}': {}", s, e))?;
75                    Ok(num.to_string())
76                }
77            } else {
78                // Already a decimal string, validate it's a valid number
79                // Try parsing as U256 first (supports very large numbers), then u64
80                if s.parse::<alloy::primitives::U256>().is_ok() || s.parse::<u64>().is_ok() {
81                    Ok(s.clone())
82                } else {
83                    Err(eyre::eyre!("Invalid number string '{}'", s))
84                }
85            }
86        }
87        serde_json::Value::Number(n) => {
88            // Convert JSON number to string
89            if let Some(u) = n.as_u64() {
90                Ok(u.to_string())
91            } else if let Some(i) = n.as_i64() {
92                if i < 0 {
93                    return Err(eyre::eyre!("Negative numbers are not supported: {}", i));
94                }
95                Ok(i.to_string())
96            } else {
97                // For very large numbers (f64), convert to string
98                // Note: JSON numbers are limited to f64 precision, so this may lose precision
99                Ok(n.to_string())
100            }
101        }
102        _ => Err(eyre::eyre!("Expected string or number, got: {}", value)),
103    }
104}
105
106/// Simulate Rego policy execution command
107#[derive(Debug, Parser)]
108pub struct SimulateCommand {
109    /// Path to the WASM component file
110    #[arg(long)]
111    wasm_file: PathBuf,
112
113    /// Path to the Rego policy file
114    #[arg(long)]
115    rego_file: PathBuf,
116
117    /// Path to the intent JSON file
118    #[arg(long)]
119    intent_json: PathBuf,
120
121    /// Entrypoint rule to evaluate (e.g., "max_gas_price.allow" or "data.max_gas_price.allow")
122    /// The "data." prefix will be automatically added if not present
123    #[arg(long, default_value = "allow")]
124    entrypoint: String,
125
126    /// Path to JSON file containing arguments for WASM execution
127    /// If not provided, an empty JSON object "{}" will be used
128    #[arg(long)]
129    wasm_args: Option<PathBuf>,
130
131    /// Path to JSON file containing policy parameters data
132    /// If not provided, an empty JSON object "{}" will be used for the params field
133    #[arg(long)]
134    policy_params_data: Option<PathBuf>,
135
136    /// Path to JSON file containing KYC identity data for simulation.
137    /// Expected fields: status, selected_country_code, address_subdivision,
138    /// address_country_code, birthdate, expiration_date, issue_date, issuing_authority.
139    /// If not provided, a default approved US/CA identity is used.
140    #[arg(long)]
141    identity_data: Option<PathBuf>,
142}
143
144/// Deploy policy command
145#[derive(Debug, Parser)]
146pub struct DeployCommand {
147    #[arg(long, env = "PRIVATE_KEY")]
148    private_key: Option<String>,
149
150    #[arg(long, env = "RPC_URL")]
151    rpc_url: Option<String>,
152
153    /// Address of the deployed policy data contract
154    #[arg(long)]
155    policy_data_address: Address,
156
157    #[arg(long, value_parser = validate_non_empty_path)]
158    policy_cids: PathBuf,
159}
160
161impl DeployCommand {
162    /// Deploy policy contract
163    ///
164    /// This function deploys a policy contract using the Rust chainio functions.
165    /// It returns the deployed policy address.
166    async fn deploy_policy(
167        private_key: &str,
168        rpc_url: &str,
169        policy_cid: &str,
170        schema_cid: &str,
171        entrypoint: &str,
172        policy_data_address: Address,
173        policy_metadata_cid: &str,
174    ) -> Result<Address> {
175        let controller = PolicyController::new(private_key.to_string(), rpc_url.to_string());
176
177        // Create policy_data array with the deployed policy data address
178        let policy_data = vec![policy_data_address];
179
180        tracing::info!(
181            "Deploying policy:\n  entrypoint: {}\n  policyCid: {}\n  schemaCid: {}\n  policyData: {:?}\n  metadataCid: {} \n",
182            entrypoint,
183            policy_cid,
184            schema_cid,
185            policy_data,
186            policy_metadata_cid
187        );
188
189        let (_receipt, policy_address) = controller
190            .deploy_policy(
191                policy_cid.to_string(),
192                schema_cid.to_string(),
193                entrypoint.to_string(),
194                policy_data,
195                policy_metadata_cid.to_string(),
196            )
197            .await
198            .map_err(|e| eyre::eyre!("Failed to deploy policy: {}", e))?;
199
200        tracing::info!("Policy deployed successfully at address: {}", policy_address);
201
202        Ok(policy_address)
203    }
204
205    /// Execute the deploy command
206    pub async fn execute(self: Box<Self>, _config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
207        // Get values from args or env, with error if still missing
208        let private_key = self
209            .private_key
210            .ok_or_else(|| eyre::eyre!("private_key is required (use --private-key or PRIVATE_KEY env var)"))?;
211
212        let rpc_url = self
213            .rpc_url
214            .ok_or_else(|| eyre::eyre!("rpc_url is required (use --rpc-url or RPC_URL env var)"))?;
215
216        // Read from policy_cids.json file
217        let json_content = std::fs::read_to_string(&self.policy_cids)
218            .with_context(|| format!("Failed to read policy_cids.json: {:?}", self.policy_cids))?;
219        let json: Value = serde_json::from_str(&json_content)
220            .with_context(|| format!("Failed to parse policy_cids.json: {:?}", self.policy_cids))?;
221
222        // Extract values from policy_cids.json
223        let policy_cid = json.get("policyCid").and_then(|v| v.as_str()).unwrap_or("");
224        let schema_cid = json.get("schemaCid").and_then(|v| v.as_str()).unwrap_or("");
225        let entrypoint = json.get("entrypoint").and_then(|v| v.as_str()).unwrap_or("");
226        let policy_metadata_cid = json.get("policyMetadataCid").and_then(|v| v.as_str()).unwrap_or("");
227
228        // Deploy policy using the provided policy data address
229        Self::deploy_policy(
230            &private_key,
231            &rpc_url,
232            policy_cid,
233            schema_cid,
234            entrypoint,
235            self.policy_data_address,
236            policy_metadata_cid,
237        )
238        .await?;
239        Ok(())
240    }
241}
242
243impl SimulateCommand {
244    /// Execute the simulate command
245    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
246        info!("Starting Rego simulation...");
247
248        // Read intent JSON file
249        info!("Reading intent JSON from: {:?}", self.intent_json);
250        let intent_json_str = std::fs::read_to_string(&self.intent_json)
251            .with_context(|| format!("Failed to read intent JSON file: {:?}", self.intent_json))?;
252        let mut intent_value: serde_json::Value = serde_json::from_str(&intent_json_str)
253            .with_context(|| format!("Failed to parse intent JSON: {:?}", self.intent_json))?;
254
255        // Normalize intent (convert value and chainId to number strings)
256        info!("Normalizing intent fields...");
257        intent_value = normalize_intent(intent_value).with_context(|| "Failed to normalize intent fields")?;
258
259        // Read WASM args if provided, otherwise use empty JSON object
260        let wasm_input = if let Some(wasm_args_path) = &self.wasm_args {
261            info!("Reading WASM args from: {:?}", wasm_args_path);
262            let wasm_args_str = std::fs::read_to_string(wasm_args_path)
263                .with_context(|| format!("Failed to read WASM args file: {:?}", wasm_args_path))?;
264            // Validate it's valid JSON
265            serde_json::from_str::<serde_json::Value>(&wasm_args_str)
266                .with_context(|| format!("Failed to parse WASM args as JSON: {:?}", wasm_args_path))?;
267            wasm_args_str
268        } else {
269            "{}".to_string()
270        };
271
272        // Execute WASM to get policy data
273        info!("Executing WASM file: {:?}", self.wasm_file);
274        let wasm_output = utils::execute_wasm(self.wasm_file, wasm_input, config)
275            .await
276            .with_context(|| "Failed to execute WASM file")?;
277
278        // Parse WASM output as JSON
279        let policy_data: serde_json::Value = serde_json::from_str(&wasm_output)
280            .with_context(|| format!("Failed to parse WASM output as JSON: {}", wasm_output))?;
281
282        // Parse intent
283        info!("Parsing intent...");
284        let parsed_intent = parse_intent(intent_value).with_context(|| "Failed to parse intent")?;
285
286        // Convert parsed intent to string
287        let parsed_intent_str: String = parsed_intent.into();
288
289        // Read Rego policy file
290        info!("Reading Rego policy from: {:?}", self.rego_file);
291        let policy = std::fs::read_to_string(&self.rego_file)
292            .with_context(|| format!("Failed to read Rego policy file: {:?}", self.rego_file))?;
293
294        // Read policy params data if provided, otherwise use empty JSON object
295        let policy_params: serde_json::Value = if let Some(policy_params_path) = &self.policy_params_data {
296            info!("Reading policy params data from: {:?}", policy_params_path);
297            let policy_params_str = std::fs::read_to_string(policy_params_path)
298                .with_context(|| format!("Failed to read policy params data file: {:?}", policy_params_path))?;
299            serde_json::from_str(&policy_params_str)
300                .with_context(|| format!("Failed to parse policy params data as JSON: {:?}", policy_params_path))?
301        } else {
302            serde_json::json!({})
303        };
304
305        // Construct policy params and data
306        let policy_params_and_data = serde_json::json!({
307            "params": policy_params,
308            "data": policy_data,
309        });
310        let policy_params_and_data_str = policy_params_and_data.to_string();
311
312        // Print the data object that will be used in the rego evaluation
313        info!("\n=== Data object for Rego evaluation ===");
314        info!("{}", serde_json::to_string_pretty(&policy_params_and_data)?);
315        info!("==========================================\n");
316
317        // Normalize entrypoint: add "data." prefix if not present
318        let entrypoint = if self.entrypoint.starts_with("data.") {
319            self.entrypoint.clone()
320        } else {
321            format!("data.{}", self.entrypoint)
322        };
323
324        let identity_data = if let Some(identity_path) = &self.identity_data {
325            info!("Reading identity data from: {:?}", identity_path);
326            let identity_str = std::fs::read_to_string(identity_path)
327                .with_context(|| format!("Failed to read identity data file: {:?}", identity_path))?;
328            let v: serde_json::Value = serde_json::from_str(&identity_str)
329                .with_context(|| format!("Failed to parse identity data JSON: {:?}", identity_path))?;
330            KycIdentityData {
331                reference_date: v
332                    .get("reference_date")
333                    .and_then(|x| x.as_str())
334                    .unwrap_or("")
335                    .to_string(),
336                status: v.get("status").and_then(|x| x.as_str()).unwrap_or("").to_string(),
337                selected_country_code: v
338                    .get("selected_country_code")
339                    .and_then(|x| x.as_str())
340                    .unwrap_or("")
341                    .to_string(),
342                address_subdivision: v
343                    .get("address_subdivision")
344                    .and_then(|x| x.as_str())
345                    .unwrap_or("")
346                    .to_string(),
347                address_country_code: v
348                    .get("address_country_code")
349                    .and_then(|x| x.as_str())
350                    .unwrap_or("")
351                    .to_string(),
352                birthdate: v.get("birthdate").and_then(|x| x.as_str()).unwrap_or("").to_string(),
353                expiration_date: v
354                    .get("expiration_date")
355                    .and_then(|x| x.as_str())
356                    .unwrap_or("")
357                    .to_string(),
358                issue_date: v.get("issue_date").and_then(|x| x.as_str()).unwrap_or("").to_string(),
359                issuing_authority: v
360                    .get("issuing_authority")
361                    .and_then(|x| x.as_str())
362                    .unwrap_or("")
363                    .to_string(),
364            }
365        } else {
366            KycIdentityData {
367                status: "approved".to_string(),
368                address_subdivision: "CA".to_string(),
369                address_country_code: "US".to_string(),
370                ..Default::default()
371            }
372        };
373
374        // Evaluate the policy
375        info!("Evaluating policy with entrypoint: {}", entrypoint);
376        let result = evaluate(
377            policy,
378            &policy_params_and_data_str,
379            &parsed_intent_str,
380            Some(Box::new(identity_data)),
381            &entrypoint,
382            None,
383        )
384        .with_context(|| "Failed to evaluate Rego policy")?;
385
386        // Print the result
387        match result {
388            RegoValue::Bool(b) => {
389                info!("Evaluation result: {}", b);
390                if b {
391                    info!("  Policy evaluation: ALLOWED");
392                } else {
393                    info!("  Policy evaluation: DENIED");
394                }
395            }
396            _ => {
397                info!("Evaluation result: {:?}", result);
398                if result == RegoValue::Undefined {
399                    info!("  Policy evaluation: UNDEFINED (denied)");
400                } else {
401                    info!("? Policy evaluation: {:?}", result);
402                }
403            }
404        }
405
406        Ok(())
407    }
408}
409
410impl PolicyCommand {
411    /// Execute the policy command
412    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
413        match self.subcommand {
414            PolicySubcommand::Deploy(command) => {
415                Box::new(command).execute(config).await?;
416            }
417            PolicySubcommand::Simulate(command) => {
418                Box::new(command).execute(config).await?;
419            }
420        }
421        Ok(())
422    }
423}