diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 7a7a37473f00b..15e6eabf15509 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -16,7 +16,10 @@ use alloy_rpc_types::{BlockId, BlockNumberOrTag::Latest}; use clap::CommandFactory; use clap_complete::generate; use eyre::{Result, WrapErr}; -use foundry_cli::utils::{self, LoadConfig}; +use foundry_cli::{ + json::{JsonEnvelope, print_json}, + utils::{self, LoadConfig}, +}; use foundry_common::{ abi::{get_error, get_event}, fmt::{format_tokens, format_uint_exp, serialize_value_as_json}, @@ -32,9 +35,35 @@ use foundry_common::{ use foundry_evm_networks::NetworkVariant; #[cfg(feature = "optimism")] use op_alloy_network::Optimism; +use serde::Serialize; use std::time::Instant; use tempo_alloy::TempoNetwork; +/// `cast abi-encode --machine` payload. +#[derive(Clone, Debug, Serialize)] +struct AbiEncodeData { + encoded: String, +} + +/// `cast abi-decode --machine` payload. +#[derive(Clone, Debug, Serialize)] +struct AbiDecodeData { + decoded: Vec, +} + +/// `cast keccak --machine` payload. +#[derive(Clone, Debug, Serialize)] +struct KeccakData { + hash: String, +} + +/// `cast 4byte --machine` payload. +#[derive(Clone, Debug, Serialize)] +struct FourByteData { + selector: String, + signatures: Vec, +} + /// Run the `cast` command-line interface. pub fn run() -> Result<()> { // Pre-setup so setup failures land in the machine envelope path. @@ -196,13 +225,23 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // ABI encoding & decoding CastSubcommand::DecodeAbi { sig, calldata, input } => { let tokens = SimpleCast::abi_decode(&sig, &calldata, input)?; - print_tokens(&tokens); + if foundry_cli::is_machine() { + let decoded = format_tokens(&tokens).collect(); + print_json(&JsonEnvelope::success(AbiDecodeData { decoded }))?; + } else { + print_tokens(&tokens); + } } CastSubcommand::AbiEncode { sig, packed, args } => { - if packed { - sh_println!("{}", SimpleCast::abi_encode_packed(&sig, &args)?)? + let encoded = if packed { + SimpleCast::abi_encode_packed(&sig, &args)? + } else { + SimpleCast::abi_encode(&sig, &args)? + }; + if foundry_cli::is_machine() { + print_json(&JsonEnvelope::success(AbiEncodeData { encoded }))?; } else { - sh_println!("{}", SimpleCast::abi_encode(&sig, &args)?)? + sh_println!("{encoded}")? } } CastSubcommand::AbiEncodeEvent { sig, args } => { @@ -603,13 +642,27 @@ pub async fn run_command(args: CastArgs) -> Result<()> { // 4Byte CastSubcommand::FourByte { selector } => { + // `--machine` requires argv so clap classifies parse errors. + if foundry_cli::is_machine() && selector.is_none() { + foundry_cli::machine::bail_machine_usage( + "`cast 4byte` under `--machine` requires the selector as a positional argument", + ); + } let selector = stdin::unwrap_line(selector)?; let sigs = decode_function_selector(selector).await?; - if sigs.is_empty() { - eyre::bail!("No matching function signatures found for selector `{selector}`"); - } - for sig in sigs { - sh_println!("{sig}")? + if foundry_cli::is_machine() { + let signatures = sigs.iter().map(ToString::to_string).collect(); + print_json(&JsonEnvelope::success(FourByteData { + selector: selector.to_string(), + signatures, + }))?; + } else { + if sigs.is_empty() { + eyre::bail!("No matching function signatures found for selector `{selector}`"); + } + for sig in sigs { + sh_println!("{sig}")? + } } } @@ -711,17 +764,15 @@ pub async fn run_command(args: CastArgs) -> Result<()> { Some(data) => data.into_bytes(), None => stdin::read_bytes(false)?, }; - match String::from_utf8(bytes) { - Ok(s) => { - let s = SimpleCast::keccak(&s)?; - sh_println!("{s}")? - } - Err(e) => { - let hash = keccak256(e.as_bytes()); - let s = hex::encode(hash); - sh_println!("0x{s}")? - } + let hash = match String::from_utf8(bytes) { + Ok(s) => SimpleCast::keccak(&s)?, + Err(e) => format!("0x{}", hex::encode(keccak256(e.as_bytes()))), }; + if foundry_cli::is_machine() { + print_json(&JsonEnvelope::success(KeccakData { hash }))?; + } else { + sh_println!("{hash}")? + } } CastSubcommand::HashMessage { message } => { let message = stdin::unwrap(message, false)?; @@ -950,24 +1001,27 @@ mod tests { assert!(errs.is_empty(), "dangling cast schema refs: {errs:?}"); } - /// Every adopted command must pin its exact `command_id`, output mode, - /// and schema refs. A drift in any of those is an agent-contract break. + /// Pins `command_id`, output mode, schema refs, `side_effects`, and + /// `reads_stdin` for every adopted command. #[test] fn registered_commands_pin_stable_ids() { - let pinned = std::thread::Builder::new() + use foundry_cli::introspect::SideEffects; + type Pinned = + (OutputMode, Option, Option, Option, SideEffects, bool); + let pins: Vec<(&str, Pinned)> = std::thread::Builder::new() .stack_size(16 * 1024 * 1024) .spawn(|| { let cmd = ::command(); let doc = build_document(&cmd, ®ISTRY); - fn find( - c: &foundry_cli::introspect::CommandInfo, - id: &str, - ) -> Option<(OutputMode, Option, Option)> { + fn find(c: &foundry_cli::introspect::CommandInfo, id: &str) -> Option { if c.command_id == id { return Some(( c.capabilities.output_mode, c.capabilities.result_schema_ref.clone(), c.capabilities.event_schema_ref.clone(), + c.capabilities.session_schema_ref.clone(), + c.capabilities.side_effects, + c.capabilities.reads_stdin, )); } for sub in &c.subcommands { @@ -977,21 +1031,72 @@ mod tests { } None } - doc.commands - .iter() - .find_map(|c| find(c, "cast.call")) - .expect("cast.call missing from cast introspect") + ["cast.call", "cast.abi-encode", "cast.abi-decode", "cast.keccak", "cast.4byte"] + .into_iter() + .map(|id| { + let pinned = doc + .commands + .iter() + .find_map(|c| find(c, id)) + .unwrap_or_else(|| panic!("{id} missing from cast introspect")); + (id, pinned) + }) + .collect() }) .expect("spawn worker thread") .join() .expect("worker thread join"); - let (mode, result_ref, event_ref) = pinned; - assert_eq!(mode, OutputMode::Envelope, "cast.call output_mode drift"); - assert_eq!( - result_ref.as_deref(), - Some("foundry:cast.call@v1"), - "cast.call result_schema_ref drift" - ); - assert_eq!(event_ref, None, "cast.call must not declare event_schema_ref"); + + struct Expected { + id: &'static str, + schema: &'static str, + side_effects: SideEffects, + reads_stdin: bool, + } + let expected = [ + Expected { + id: "cast.call", + schema: "foundry:cast.call@v1", + side_effects: SideEffects::Network, + reads_stdin: false, + }, + Expected { + id: "cast.abi-encode", + schema: "foundry:cast.abi-encode@v1", + side_effects: SideEffects::None, + reads_stdin: false, + }, + Expected { + id: "cast.abi-decode", + schema: "foundry:cast.abi-decode@v1", + side_effects: SideEffects::None, + reads_stdin: false, + }, + Expected { + id: "cast.keccak", + schema: "foundry:cast.keccak@v1", + side_effects: SideEffects::None, + reads_stdin: true, + }, + Expected { + id: "cast.4byte", + schema: "foundry:cast.4byte@v1", + side_effects: SideEffects::Network, + reads_stdin: false, + }, + ]; + for ( + (id, (mode, result_ref, event_ref, session_ref, side, stdin)), + Expected { id: eid, schema, side_effects, reads_stdin }, + ) in pins.iter().zip(&expected) + { + assert_eq!(id, eid, "pin order drift"); + assert_eq!(*mode, OutputMode::Envelope, "{id} output_mode drift"); + assert_eq!(result_ref.as_deref(), Some(*schema), "{id} result_schema_ref drift"); + assert_eq!(event_ref.as_deref(), None, "{id} must not declare event_schema_ref"); + assert_eq!(session_ref.as_deref(), None, "{id} must not declare session_schema_ref"); + assert_eq!(side, side_effects, "{id} side_effects drift"); + assert_eq!(stdin, reads_stdin, "{id} reads_stdin drift"); + } } } diff --git a/crates/cast/src/introspect.rs b/crates/cast/src/introspect.rs index 0227500a348d5..4cf7a46bb58c8 100644 --- a/crates/cast/src/introspect.rs +++ b/crates/cast/src/introspect.rs @@ -7,22 +7,91 @@ use foundry_cli::introspect::{ CapabilityMeta, CommandMeta, CommandRegistry, OutputMode, RegistryEntry, SideEffects, }; -/// Stable schema id for the `cast call` envelope payload. +/// Schema id for the `cast call` envelope payload. pub const CALL_RESULT_SCHEMA: &str = "foundry:cast.call@v1"; -static ENTRIES: &[RegistryEntry] = &[RegistryEntry { - path: &["call"], - meta: CommandMeta { - command_id: Some("cast.call"), - capabilities: CapabilityMeta { - output_mode: OutputMode::Envelope, - result_schema_ref: Some(CALL_RESULT_SCHEMA), - side_effects: SideEffects::Network, - ..CapabilityMeta::NONE +/// Schema id for the `cast abi-encode` envelope payload. +pub const ABI_ENCODE_RESULT_SCHEMA: &str = "foundry:cast.abi-encode@v1"; + +/// Schema id for the `cast abi-decode` envelope payload. +pub const ABI_DECODE_RESULT_SCHEMA: &str = "foundry:cast.abi-decode@v1"; + +/// Schema id for the `cast keccak` envelope payload. +pub const KECCAK_RESULT_SCHEMA: &str = "foundry:cast.keccak@v1"; + +/// Schema id for the `cast 4byte` envelope payload. +pub const FOUR_BYTE_RESULT_SCHEMA: &str = "foundry:cast.4byte@v1"; + +static ENTRIES: &[RegistryEntry] = &[ + RegistryEntry { + path: &["call"], + meta: CommandMeta { + command_id: Some("cast.call"), + capabilities: CapabilityMeta { + output_mode: OutputMode::Envelope, + result_schema_ref: Some(CALL_RESULT_SCHEMA), + side_effects: SideEffects::Network, + ..CapabilityMeta::NONE + }, + exit_codes: &[], + }, + }, + RegistryEntry { + path: &["abi-encode"], + meta: CommandMeta { + command_id: Some("cast.abi-encode"), + capabilities: CapabilityMeta { + output_mode: OutputMode::Envelope, + result_schema_ref: Some(ABI_ENCODE_RESULT_SCHEMA), + side_effects: SideEffects::None, + ..CapabilityMeta::NONE + }, + exit_codes: &[], + }, + }, + RegistryEntry { + path: &["decode-abi"], + meta: CommandMeta { + command_id: Some("cast.abi-decode"), + capabilities: CapabilityMeta { + output_mode: OutputMode::Envelope, + result_schema_ref: Some(ABI_DECODE_RESULT_SCHEMA), + side_effects: SideEffects::None, + ..CapabilityMeta::NONE + }, + exit_codes: &[], + }, + }, + RegistryEntry { + path: &["keccak"], + meta: CommandMeta { + command_id: Some("cast.keccak"), + capabilities: CapabilityMeta { + output_mode: OutputMode::Envelope, + result_schema_ref: Some(KECCAK_RESULT_SCHEMA), + side_effects: SideEffects::None, + reads_stdin: true, + ..CapabilityMeta::NONE + }, + exit_codes: &[], + }, + }, + RegistryEntry { + path: &["4byte"], + meta: CommandMeta { + command_id: Some("cast.4byte"), + capabilities: CapabilityMeta { + output_mode: OutputMode::Envelope, + result_schema_ref: Some(FOUR_BYTE_RESULT_SCHEMA), + side_effects: SideEffects::Network, + // `--machine` requires argv; stdin is human-only. + reads_stdin: false, + ..CapabilityMeta::NONE + }, + exit_codes: &[], }, - exit_codes: &[], }, -}]; +]; /// The `cast` command registry. Used by `--introspect` and by adoption code /// that needs to look up command metadata. diff --git a/crates/cast/tests/cli/agent_contract.rs b/crates/cast/tests/cli/agent_contract.rs index 9162734bae324..9502bfaa3986c 100644 --- a/crates/cast/tests/cli/agent_contract.rs +++ b/crates/cast/tests/cli/agent_contract.rs @@ -6,7 +6,7 @@ //! envelope on stdout (never raw clap text). use foundry_test_utils::agent_schema; -use serde_json::Value; +use serde_json::{Value, json}; // `cast --introspect` returns valid JSON conforming to // `foundry:introspect@v1`. @@ -30,8 +30,183 @@ casttest!(machine_mode_help_emits_success_envelope, |_prj, cmd| { .unwrap_or_else(|e| panic!("expected single envelope on stdout: {stdout}: {e}")); assert_eq!(envelope["success"], true); assert!(envelope["data"]["help"].is_string(), "missing data.help: {envelope}"); - assert_eq!(envelope["errors"], serde_json::json!([])); - assert_eq!(envelope["warnings"], serde_json::json!([])); + assert_eq!(envelope["errors"], json!([])); + assert_eq!(envelope["warnings"], json!([])); + agent_schema::validate("foundry:envelope@v1", &envelope); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine abi-encode` emits the encoded bytes inside an envelope. +casttest!(cast_abi_encode_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd + .args([ + "--machine", + "abi-encode", + "transfer(address,uint256)", + "0x0000000000000000000000000000000000000001", + "42", + ]) + .assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!(envelope["errors"], json!([])); + assert_eq!(envelope["warnings"], json!([])); + assert_eq!( + envelope["data"]["encoded"], + "0x0000000000000000000000000000000000000000000000000000000000000001\ + 000000000000000000000000000000000000000000000000000000000000002a" + ); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.abi-encode@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine abi-encode --packed` emits packed (non-padded) bytes. +casttest!(cast_abi_encode_packed_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd + .args(["--machine", "abi-encode", "--packed", "f(uint8,uint16)", "1", "2"]) + .assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!(envelope["data"]["encoded"], "0x010002"); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.abi-encode@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine abi-decode` emits the formatted token list. +casttest!(cast_abi_decode_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd + .args([ + "--machine", + "abi-decode", + "balanceOf(address)(uint256)", + "0x000000000000000000000000000000000000000000000000000000000000002a", + ]) + .assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!(envelope["errors"], json!([])); + assert_eq!(envelope["warnings"], json!([])); + assert_eq!(envelope["data"]["decoded"], json!(["42"])); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.abi-decode@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine abi-decode --input` decodes against the input types. +casttest!(cast_abi_decode_input_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd + .args([ + "--machine", + "abi-decode", + "--input", + "transfer(address,uint256)", + "0x0000000000000000000000000000000000000000000000000000000000000001\ + 000000000000000000000000000000000000000000000000000000000000002a", + ]) + .assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!( + envelope["data"]["decoded"], + json!(["0x0000000000000000000000000000000000000001", "42"]) + ); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.abi-decode@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine keccak ` emits the 0x-prefixed hash. +casttest!(cast_keccak_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd.args(["--machine", "keccak", "foundry"]).assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!(envelope["errors"], json!([])); + assert_eq!(envelope["warnings"], json!([])); + assert_eq!( + envelope["data"]["hash"], + "0x4eb2f10301a3ed7f2c31091074ca429f73cb8c51539e1a0005e132f70b8bb74a" + ); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.keccak@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine keccak` with no argument hashes stdin bytes. +casttest!(cast_keccak_machine_mode_reads_stdin, |_prj, cmd| { + let assert = cmd.args(["--machine", "keccak"]).stdin("foundry").assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!( + envelope["data"]["hash"], + "0x4eb2f10301a3ed7f2c31091074ca429f73cb8c51539e1a0005e132f70b8bb74a" + ); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.keccak@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine 4byte` echoes the selector with openchain.xyz signatures. +casttest!(flaky_cast_4byte_machine_mode_emits_envelope, |_prj, cmd| { + let assert = cmd.args(["--machine", "4byte", "0xa9059cbb"]).assert_success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = + serde_json::from_str(stdout.trim()).expect("stdout is exactly one JSON envelope"); + + assert_eq!(envelope["success"], true); + assert_eq!(envelope["errors"], json!([])); + assert_eq!(envelope["warnings"], json!([])); + assert_eq!(envelope["data"]["selector"], "0xa9059cbb"); + let signatures = envelope["data"]["signatures"].as_array().expect("signatures is an array"); + assert!( + signatures.iter().any(|s| s.as_str() == Some("transfer(address,uint256)")), + "expected `transfer(address,uint256)` in signatures: {envelope}" + ); + agent_schema::validate_envelope_data(&envelope, "foundry:cast.4byte@v1"); + + let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); + assert!(stderr.is_empty(), "stderr must be empty under --machine, got: {stderr}"); +}); + +// `cast --machine 4byte` without a positional selector exits with +// `cli.usage.invalid` (the stdin fallback is disabled under `--machine`). +casttest!(cast_4byte_machine_mode_rejects_stdin, |_prj, cmd| { + let assert = cmd.args(["--machine", "4byte"]).stdin("0xa9059cbb").assert_failure(); + assert_eq!(assert.get_output().status.code(), Some(2)); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + let envelope: Value = serde_json::from_str(stdout.trim()).expect("error envelope on stdout"); + + assert_eq!(envelope["success"], false); + assert!(envelope["data"].is_null(), "data must be null on failure: {envelope}"); + assert_eq!(envelope["warnings"], json!([])); + assert_eq!(envelope["errors"][0]["code"], "cli.usage.invalid"); agent_schema::validate("foundry:envelope@v1", &envelope); let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); @@ -50,7 +225,7 @@ casttest!(machine_mode_unknown_flag_emits_typed_envelope, |_prj, cmd| { assert!(envelope["data"].is_null(), "data must be null on failure: {envelope}"); assert_eq!(envelope["errors"].as_array().map(Vec::len), Some(1)); assert_eq!(envelope["errors"][0]["code"], "cli.usage.invalid"); - assert_eq!(envelope["warnings"], serde_json::json!([])); + assert_eq!(envelope["warnings"], json!([])); agent_schema::validate("foundry:envelope@v1", &envelope); let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap(); diff --git a/crates/cli/src/introspect/build.rs b/crates/cli/src/introspect/build.rs index d49369db7c801..f7e4f80ae08b8 100644 --- a/crates/cli/src/introspect/build.rs +++ b/crates/cli/src/introspect/build.rs @@ -45,12 +45,14 @@ pub fn collect_command_ids(doc: &IntrospectDocument) -> Vec { /// `long_running = true`. /// /// Schema refs, when present, must be non-empty and match -/// `^foundry:[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(\.event|\.session)?@v\d+$`. +/// `^foundry:[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*(\.event|\.session)?@v\d+$`. pub fn capability_violations(doc: &IntrospectDocument) -> Vec { static SCHEMA_REF_RE: OnceLock = OnceLock::new(); let schema_re = SCHEMA_REF_RE.get_or_init(|| { - regex::Regex::new(r"^foundry:[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(\.event|\.session)?@v\d+$") - .expect("schema-ref regex compiles") + regex::Regex::new( + r"^foundry:[a-z0-9][a-z0-9_-]*(\.[a-z0-9][a-z0-9_-]*)*(\.event|\.session)?@v\d+$", + ) + .expect("schema-ref regex compiles") }); fn check_ref( diff --git a/crates/test-utils/src/agent_schema.rs b/crates/test-utils/src/agent_schema.rs index 50b3e82950331..837c812c2a5a5 100644 --- a/crates/test-utils/src/agent_schema.rs +++ b/crates/test-utils/src/agent_schema.rs @@ -21,6 +21,10 @@ const SCHEMA_FILES: &[(&str, &str)] = &[ ("foundry:envelope@v1", "envelope.v1.json"), ("foundry:introspect@v1", "introspect.v1.json"), ("foundry:cast.call@v1", "cast.call.v1.json"), + ("foundry:cast.abi-encode@v1", "cast.abi-encode.v1.json"), + ("foundry:cast.abi-decode@v1", "cast.abi-decode.v1.json"), + ("foundry:cast.keccak@v1", "cast.keccak.v1.json"), + ("foundry:cast.4byte@v1", "cast.4byte.v1.json"), ("foundry:forge.build@v1", "forge.build.v1.json"), ("foundry:forge.test@v1", "forge.test.v1.json"), ("foundry:forge.test.event@v1", "forge.test.event.v1.json"), diff --git a/docs/agents/schemas/cast.4byte.v1.json b/docs/agents/schemas/cast.4byte.v1.json new file mode 100644 index 0000000000000..842733de52a38 --- /dev/null +++ b/docs/agents/schemas/cast.4byte.v1.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foundry:cast.4byte@v1", + "title": "cast 4byte result payload", + "description": "Shape of the `data` field in the `cast 4byte --machine` terminal envelope.", + "type": "object", + "required": ["selector", "signatures"], + "additionalProperties": false, + "properties": { + "selector": { + "type": "string", + "description": "0x-prefixed 4-byte function selector that was looked up.", + "pattern": "^0x[0-9a-fA-F]{8}$" + }, + "signatures": { + "type": "array", + "description": "Function signatures registered for `selector` in the openchain.xyz database. Empty when the database has no matches.", + "items": { "type": "string" } + } + } +} diff --git a/docs/agents/schemas/cast.abi-decode.v1.json b/docs/agents/schemas/cast.abi-decode.v1.json new file mode 100644 index 0000000000000..25ef7d8a7529e --- /dev/null +++ b/docs/agents/schemas/cast.abi-decode.v1.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foundry:cast.abi-decode@v1", + "title": "cast abi-decode result payload", + "description": "Shape of the `data` field in the `cast abi-decode --machine` terminal envelope.", + "type": "object", + "required": ["decoded"], + "additionalProperties": false, + "properties": { + "decoded": { + "type": "array", + "description": "One formatted string per decoded token, in declaration order, matching the human-mode line-per-token print.", + "items": { "type": "string" } + } + } +} diff --git a/docs/agents/schemas/cast.abi-encode.v1.json b/docs/agents/schemas/cast.abi-encode.v1.json new file mode 100644 index 0000000000000..4da95db3fe5d6 --- /dev/null +++ b/docs/agents/schemas/cast.abi-encode.v1.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foundry:cast.abi-encode@v1", + "title": "cast abi-encode result payload", + "description": "Shape of the `data` field in the `cast abi-encode --machine` terminal envelope.", + "type": "object", + "required": ["encoded"], + "additionalProperties": false, + "properties": { + "encoded": { + "type": "string", + "description": "0x-prefixed hex of the ABI-encoded arguments. Excludes the function selector. Reflects the `--packed` flag when set.", + "pattern": "^0x(?:[0-9a-fA-F]{2})*$" + } + } +} diff --git a/docs/agents/schemas/cast.keccak.v1.json b/docs/agents/schemas/cast.keccak.v1.json new file mode 100644 index 0000000000000..5fffadfc9be54 --- /dev/null +++ b/docs/agents/schemas/cast.keccak.v1.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "foundry:cast.keccak@v1", + "title": "cast keccak result payload", + "description": "Shape of the `data` field in the `cast keccak --machine` terminal envelope.", + "type": "object", + "required": ["hash"], + "additionalProperties": false, + "properties": { + "hash": { + "type": "string", + "description": "0x-prefixed Keccak-256 hash of the input bytes.", + "pattern": "^0x[0-9a-f]{64}$" + } + } +} diff --git a/docs/agents/schemas/introspect.v1.json b/docs/agents/schemas/introspect.v1.json index df32d872d326c..07ff087798f60 100644 --- a/docs/agents/schemas/introspect.v1.json +++ b/docs/agents/schemas/introspect.v1.json @@ -98,17 +98,17 @@ "result_schema_ref": { "type": "string", "description": "Per-command success-payload schema id. Must NOT carry an `.event` or `.session` suffix.", - "pattern": "^foundry:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*@v\\d+$" + "pattern": "^foundry:[a-z0-9][a-z0-9_-]*(\\.[a-z0-9][a-z0-9_-]*)*@v\\d+$" }, "event_schema_ref": { "type": "string", "description": "Per-record stream schema id. Must carry an `.event` suffix.", - "pattern": "^foundry:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*\\.event@v\\d+$" + "pattern": "^foundry:[a-z0-9][a-z0-9_-]*(\\.[a-z0-9][a-z0-9_-]*)*\\.event@v\\d+$" }, "session_schema_ref": { "type": "string", "description": "Per-record session schema id. Must carry a `.session` suffix.", - "pattern": "^foundry:[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*\\.session@v\\d+$" + "pattern": "^foundry:[a-z0-9][a-z0-9_-]*(\\.[a-z0-9][a-z0-9_-]*)*\\.session@v\\d+$" }, "reads_stdin": { "type": "boolean" }, "supports_output_path": { "type": "boolean" },