feat(mcp): add policy_cbor_path arg to wallet_plutus_mint_unsigned

Mirrors the reference_script_path workaround already on wallet_send.
Lets the caller hand aldabra a path to a hex-CBOR file inside the
container instead of pasting the hex inline as a JSON-RPC arg, which
lets us bypass the 2026-05-07 MCP large-string transport bug (>~ 4500
char hex strings get a 1-byte truncation between Claude Code and
aldabra's stdio reader, surfacing as 'odd length' decode errors and
blocking debug-build minting policies).

policy_cbor_hex becomes Option<String>; new policy_cbor_path:
Option<String> sits next to it. New resolver helper
resolve_policy_cbor_bytes mirrors resolve_ref_script_bytes — at most
one of the two may be set, exactly one must be set. Whitespace is
stripped from file contents so the file may have trailing newlines.

Unblocks Track #33 / preprod_test3: debug-build validators that
overshoot the transport ceiling can now be minted via path arg.
This commit is contained in:
Kayos 2026-05-08 18:50:26 -07:00
parent 1b9968cf3b
commit e679874939

View file

@ -103,6 +103,48 @@ fn resolve_ref_script_bytes(
}
}
/// Resolve the Plutus minting-policy CBOR from EITHER an inline
/// hex argument OR a file path inside the container. Caller passes
/// both raw options; this fn enforces the "exactly one" rule and
/// reads the file when path is set.
///
/// Mirrors [`resolve_ref_script_bytes`] — same workaround for the
/// 2026-05-07 MCP transport bug where hex strings >~ 4500 chars
/// get a 1-byte truncation between Claude Code and aldabra's stdio
/// reader, surfacing as "odd length" hex decode errors and blocking
/// debug-build minting policies. Reading from a file inside the
/// container bypasses the JSON-RPC arg path entirely.
fn resolve_policy_cbor_bytes(
cbor_hex: Option<&str>,
path: Option<&str>,
) -> Result<Vec<u8>, String> {
match (cbor_hex, path) {
(Some(_), Some(_)) => Err(
"set at most one of policy_cbor_hex / policy_cbor_path".into(),
),
(Some(s), None) => {
let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
hex_decode(&cleaned).map_err(|e| format!("decode policy_cbor_hex: {e}"))
}
(None, Some(p)) => {
let raw = std::fs::read_to_string(p)
.map_err(|e| format!("read policy_cbor_path '{p}': {e}"))?;
let cleaned: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
if cleaned.is_empty() {
return Err(format!(
"policy_cbor_path '{p}' contained no hex characters"
));
}
hex_decode(&cleaned).map_err(|e| {
format!("decode policy_cbor_path '{p}' contents: {e}")
})
}
(None, None) => {
Err("must set exactly one of policy_cbor_hex / policy_cbor_path".into())
}
}
}
/// Parse a user-supplied script-kind string ("PlutusV1" / "PlutusV2"
/// / "PlutusV3" / "Native") into the pallas `ScriptKind` enum used
/// by the reference-script attachment helper. Case-insensitive,
@ -398,7 +440,22 @@ pub struct PolicyCreateArgs {
pub struct PlutusMintUnsignedArgs {
/// Plutus minting policy script CBOR (hex). 28-byte blake2b
/// hash with the version tag becomes the policy_id.
pub policy_cbor_hex: String,
/// At most one of `policy_cbor_hex` or `policy_cbor_path` may
/// be set; exactly one must be set.
#[serde(default)]
pub policy_cbor_hex: Option<String>,
/// Path INSIDE THE ALDABRA CONTAINER to a file containing
/// hex-encoded Plutus policy CBOR. Use INSTEAD of
/// `policy_cbor_hex` for scripts >~ 4500 chars to bypass the
/// MCP large-string transport bug (caught 2026-05-07: hex strings
/// > ~4500 chars get a 1-byte truncation + structural rearrangement
/// somewhere between Claude Code and aldabra's stdio reader,
/// surfacing as "odd length" hex decode errors). File contents
/// may include leading/trailing whitespace; only hex chars are
/// decoded. At most one of `policy_cbor_hex` or `policy_cbor_path`
/// may be set; exactly one must be set.
#[serde(default)]
pub policy_cbor_path: Option<String>,
/// Plutus version: "v1", "v2", or "v3".
pub policy_version: String,
/// PlutusData CBOR redeemer (hex) for the mint redeemer entry.
@ -1612,12 +1669,13 @@ impl WalletService {
#[tool(
name = "wallet_plutus_mint_unsigned",
description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create."
description = "Build (no sign) a Plutus-policy mint with custom output. Distinct from wallet_mint (native script only). Args: policy_cbor_hex OR policy_cbor_path (use path for >~4500-char scripts to bypass the MCP large-string transport bug) + policy_version (v1/v2/v3) + redeemer_cbor_hex + ex_units (mem+steps), mint_assets (array of {asset_name_hex, quantity}), dest_address + dest_lovelace + dest_extra_assets + dest_inline_datum_cbor_hex (req for script addrs), required_input_refs (array of 'txhash#index' UTxOs that MUST be spent — e.g. gstOutRef for parameterized policies). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial → wallet_submit_signed_tx. Designed for Agora-style DAO bringup: governor bootstrap, stake bootstrap, proposal create."
)]
async fn wallet_plutus_mint_unsigned(
&self,
#[tool(aggr)] PlutusMintUnsignedArgs {
policy_cbor_hex,
policy_cbor_path,
policy_version,
redeemer_cbor_hex,
mint_assets,
@ -1639,8 +1697,10 @@ impl WalletService {
));
}
let policy_cbor =
hex_decode(&policy_cbor_hex).map_err(|e| format!("decode policy_cbor: {e}"))?;
let policy_cbor = resolve_policy_cbor_bytes(
policy_cbor_hex.as_deref(),
policy_cbor_path.as_deref(),
)?;
let redeemer_cbor =
hex_decode(&redeemer_cbor_hex).map_err(|e| format!("decode redeemer: {e}"))?;
let policy_ver = match policy_version.trim().to_ascii_lowercase().as_str() {