From f3b8fe6b22a9ada06e1163d2afd67ab21d6e7751 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Fri, 13 Mar 2026 15:50:42 -0400 Subject: [PATCH 1/6] Add configurable failure mode behavior to module loading + check module imports at load time --- runtime/plaid/resources/config/loading.toml | 8 +- runtime/plaid/src/functions/api.rs | 632 +++++++------------- runtime/plaid/src/functions/mod.rs | 1 + runtime/plaid/src/loader/errors.rs | 11 + runtime/plaid/src/loader/mod.rs | 85 ++- 5 files changed, 288 insertions(+), 449 deletions(-) diff --git a/runtime/plaid/resources/config/loading.toml b/runtime/plaid/resources/config/loading.toml index d463122d..a21ebbdb 100644 --- a/runtime/plaid/resources/config/loading.toml +++ b/runtime/plaid/resources/config/loading.toml @@ -6,6 +6,7 @@ module_dir = "../compiled_modules/" compiler_backend = "cranelift" readiness_check_file = "./plaid_ready" +panic_on_module_load_failure = true test_mode = true test_mode_exemptions = [ @@ -31,7 +32,7 @@ test_mode_exemptions = [ "test_dynamodb.wasm", "test_mnr_return_cert.wasm", "test_tls_cert_with_sni.wasm", - "test_gh_id_conversion.wasm" + "test_gh_id_conversion.wasm", ] [loading.persistent_response_size] @@ -148,3 +149,8 @@ default = "Unlimited" [loading.module_signing] authorized_signers = ["{plaid-secret{public-key}}"] signatures_required = 1 +panic_on_invalid_signature = true + +[loading.failure_behavior] +panic_on_module_parsing_failure = true +panic_on_modele_compilation_failure = true diff --git a/runtime/plaid/src/functions/api.rs b/runtime/plaid/src/functions/api.rs index c54af192..5f109ae4 100644 --- a/runtime/plaid/src/functions/api.rs +++ b/runtime/plaid/src/functions/api.rs @@ -615,469 +615,243 @@ impl_new_sub_module_function_with_error_buffer!(blockchain, evm, get_block, ALLO // Bloom filter functions impl_new_function_with_error_buffer!(bloom_filter, build_with_items, ALLOW_IN_TEST_MODE); -pub fn to_api_function( - name: &str, - mut store: &mut Store, - env: FunctionEnv, -) -> Option { - Some(match name { - // The below are types that deal with getting data into the guest that comes - // from the message itself. - "fetch_data" => Function::new_typed_with_env(&mut store, &env, super::message::fetch_data), - "fetch_source" => { - Function::new_typed_with_env(&mut store, &env, super::message::fetch_source) - } - "fetch_data_and_source" => { - Function::new_typed_with_env(&mut store, &env, super::message::fetch_data_and_source) - } - "get_accessory_data" => { - Function::new_typed_with_env(&mut store, &env, super::runtime_data::get_accessory_data) - } - "get_secrets" => { - Function::new_typed_with_env(&mut store, &env, super::runtime_data::get_secrets) - } - "get_headers" => { - Function::new_typed_with_env(&mut store, &env, super::message::get_headers) - } - "get_query_params" => { - Function::new_typed_with_env(&mut store, &env, super::message::get_query_params) - } - "fetch_random_bytes" => { - Function::new_typed_with_env(&mut store, &env, super::internal::fetch_random_bytes) +/// Generates `to_api_function` and `is_known_api_function` from a single source-of-truth list. +/// +/// `with_env` entries produce `Function::new_typed_with_env` +/// `without_env` entries produce `Function::new_typed` (for host functions that need no env, e.g. `get_time`). +/// +/// Both generated functions share exactly the same match arms, so adding a new host function +/// only requires one change in the invocation below. +macro_rules! define_api_functions { + ( + with_env: [ + $($(#[$we_attr:meta])* $we_name:literal => $we_fn:expr),* $(,)? + ], + without_env: [ + $($(#[$woe_attr:meta])* $woe_name:literal => $woe_fn:expr),* $(,)? + ] $(,)? + ) => { + pub fn to_api_function( + name: &str, + mut store: &mut Store, + env: FunctionEnv, + ) -> Option { + Some(match name { + $( + $(#[$we_attr])* + $we_name => Function::new_typed_with_env(&mut store, &env, $we_fn), + )* + $( + $(#[$woe_attr])* + $woe_name => Function::new_typed(&mut store, $woe_fn), + )* + _ => return None, + }) + } + + /// Returns `true` if `name` is a host function known to Plaid. + pub fn is_known_api_function(name: &str) -> bool { + match name { + $( + $(#[$we_attr])* + $we_name => true, + )* + $( + $(#[$woe_attr])* + $woe_name => true, + )* + _ => false, + } } + }; +} + +define_api_functions! { + with_env: [ + // Message / request data + "fetch_data" => super::message::fetch_data, + "fetch_source" => super::message::fetch_source, + "fetch_data_and_source" => super::message::fetch_data_and_source, + "get_accessory_data" => super::runtime_data::get_accessory_data, + "get_secrets" => super::runtime_data::get_secrets, + "get_headers" => super::message::get_headers, + "get_query_params" => super::message::get_query_params, + "fetch_random_bytes" => super::internal::fetch_random_bytes, // The below are types that deal with Plaid specific internals like // the data base or caching systems. These usually have specific implementations // so are broken out into their own module. - "get_response" => { - Function::new_typed_with_env(&mut store, &env, super::response::get_response) - } - "set_response" => { - Function::new_typed_with_env(&mut store, &env, super::response::set_response) - } - "set_error_context" => { - Function::new_typed_with_env(&mut store, &env, super::internal::set_error_context) - } - "print_debug_string" => { - Function::new_typed_with_env(&mut store, &env, super::internal::print_debug_string) - } - "get_time" => Function::new_typed(&mut store, super::internal::get_time), - "storage_insert" => Function::new_typed_with_env(&mut store, &env, super::storage::insert), - "storage_insert_shared" => { - Function::new_typed_with_env(&mut store, &env, super::storage::insert_shared) - } - "storage_get" => Function::new_typed_with_env(&mut store, &env, super::storage::get), - "storage_get_shared" => { - Function::new_typed_with_env(&mut store, &env, super::storage::get_shared) - } - "storage_delete" => Function::new_typed_with_env(&mut store, &env, super::storage::delete), - "storage_delete_shared" => { - Function::new_typed_with_env(&mut store, &env, super::storage::delete_shared) - } - "storage_list_keys" => { - Function::new_typed_with_env(&mut store, &env, super::storage::list_keys) - } - "storage_list_keys_shared" => { - Function::new_typed_with_env(&mut store, &env, super::storage::list_keys_shared) - } - "cache_insert" => Function::new_typed_with_env(&mut store, &env, super::cache::insert), - "cache_get" => Function::new_typed_with_env(&mut store, &env, super::cache::get), - "log_back" => Function::new_typed_with_env(&mut store, &env, super::internal::log_back), - "log_back_unlimited" => { - Function::new_typed_with_env(&mut store, &env, super::internal::log_back_unlimited) - } - // Npm Calls - "npm_publish_empty_stub" => { - Function::new_typed_with_env(&mut store, &env, npm_publish_empty_stub) - } - - "npm_set_team_permission_on_package" => { - Function::new_typed_with_env(&mut store, &env, npm_set_team_permission_on_package) - } - - "npm_create_granular_token_for_packages" => { - Function::new_typed_with_env(&mut store, &env, npm_create_granular_token_for_packages) - } - - "npm_delete_granular_token" => { - Function::new_typed_with_env(&mut store, &env, npm_delete_granular_token) - } - - "npm_list_granular_tokens" => { - Function::new_typed_with_env(&mut store, &env, npm_list_granular_tokens) - } + "get_response" => super::response::get_response, + "set_response" => super::response::set_response, + "set_error_context" => super::internal::set_error_context, + "print_debug_string" => super::internal::print_debug_string, + "storage_insert" => super::storage::insert, + "storage_insert_shared" => super::storage::insert_shared, + "storage_get" => super::storage::get, + "storage_get_shared" => super::storage::get_shared, + "storage_delete" => super::storage::delete, + "storage_delete_shared" => super::storage::delete_shared, + "storage_list_keys" => super::storage::list_keys, + "storage_list_keys_shared" => super::storage::list_keys_shared, + "cache_insert" => super::cache::insert, + "cache_get" => super::cache::get, + "log_back" => super::internal::log_back, + "log_back_unlimited" => super::internal::log_back_unlimited, - "npm_delete_package" => Function::new_typed_with_env(&mut store, &env, npm_delete_package), - - "npm_add_user_to_team" => { - Function::new_typed_with_env(&mut store, &env, npm_add_user_to_team) - } - - "npm_remove_user_from_team" => { - Function::new_typed_with_env(&mut store, &env, npm_remove_user_from_team) - } - - "npm_remove_user_from_organization" => { - Function::new_typed_with_env(&mut store, &env, npm_remove_user_from_organization) - } - - "npm_invite_user_to_organization" => { - Function::new_typed_with_env(&mut store, &env, npm_invite_user_to_organization) - } - - "npm_get_org_user_list" => { - Function::new_typed_with_env(&mut store, &env, npm_get_org_user_list) - } - - "npm_get_org_users_without_2fa" => { - Function::new_typed_with_env(&mut store, &env, npm_get_org_users_without_2fa) - } - - "npm_list_packages_with_team_permission" => { - Function::new_typed_with_env(&mut store, &env, npm_list_packages_with_team_permission) - } - - "npm_get_token_details" => { - Function::new_typed_with_env(&mut store, &env, npm_get_token_details) - } - - // Okta Calls - "okta_remove_user_from_group" => { - Function::new_typed_with_env(&mut store, &env, okta_remove_user_from_group) - } - "okta_get_user_data" => Function::new_typed_with_env(&mut store, &env, okta_get_user_data), + // Npm Calls + "npm_publish_empty_stub" => npm_publish_empty_stub, + "npm_set_team_permission_on_package" => npm_set_team_permission_on_package, + "npm_create_granular_token_for_packages" => npm_create_granular_token_for_packages, + "npm_delete_granular_token" => npm_delete_granular_token, + "npm_list_granular_tokens" => npm_list_granular_tokens, + "npm_delete_package" => npm_delete_package, + "npm_add_user_to_team" => npm_add_user_to_team, + "npm_remove_user_from_team" => npm_remove_user_from_team, + "npm_remove_user_from_organization" => npm_remove_user_from_organization, + "npm_invite_user_to_organization" => npm_invite_user_to_organization, + "npm_get_org_user_list" => npm_get_org_user_list, + "npm_get_org_users_without_2fa" => npm_get_org_users_without_2fa, + "npm_list_packages_with_team_permission" => npm_list_packages_with_team_permission, + "npm_get_token_details" => npm_get_token_details, + + // Okta + "okta_remove_user_from_group" => okta_remove_user_from_group, + "okta_get_user_data" => okta_get_user_data, // AES calls - "cryptography_aes_128_cbc_encrypt" => { - Function::new_typed_with_env(&mut store, &env, cryptography_aes_128_cbc_encrypt) - } - "cryptography_aes_128_cbc_decrypt" => { - Function::new_typed_with_env(&mut store, &env, cryptography_aes_128_cbc_decrypt) - } + "cryptography_aes_128_cbc_encrypt" => cryptography_aes_128_cbc_encrypt, + "cryptography_aes_128_cbc_decrypt" => cryptography_aes_128_cbc_decrypt, // GitHub Calls - "github_remove_user_from_repo" => { - Function::new_typed_with_env(&mut store, &env, github_remove_user_from_repo) - } - "github_add_user_to_repo" => { - Function::new_typed_with_env(&mut store, &env, github_add_user_to_repo) - } - "github_add_user_to_team" => { - Function::new_typed_with_env(&mut store, &env, github_add_user_to_team) - } - "github_remove_user_from_team" => { - Function::new_typed_with_env(&mut store, &env, github_remove_user_from_team) - } - "github_make_graphql_query" => { - Function::new_typed_with_env(&mut store, &env, github_make_graphql_query) - } - "github_make_advanced_graphql_query" => { - Function::new_typed_with_env(&mut store, &env, github_make_advanced_graphql_query) - } - "github_fetch_commit" => { - Function::new_typed_with_env(&mut store, &env, github_fetch_commit) - } - "github_list_files" => Function::new_typed_with_env(&mut store, &env, github_list_files), - "github_fetch_file_with_custom_media_type" => { - Function::new_typed_with_env(&mut store, &env, github_fetch_file_with_custom_media_type) - } - "github_list_fpat_requests_for_org" => { - Function::new_typed_with_env(&mut store, &env, github_list_fpat_requests_for_org) - } - "github_review_fpat_requests_for_org" => { - Function::new_typed_with_env(&mut store, &env, github_review_fpat_requests_for_org) - } - "github_get_repos_for_fpat" => { - Function::new_typed_with_env(&mut store, &env, github_get_repos_for_fpat) - } - "github_get_branch_protection_rules" => { - Function::new_typed_with_env(&mut store, &env, github_get_branch_protection_rules) - } - "github_get_branch_protection_ruleset" => { - Function::new_typed_with_env(&mut store, &env, github_get_branch_protection_ruleset) - } - "github_get_repository_collaborators" => { - Function::new_typed_with_env(&mut store, &env, github_get_repository_collaborators) - } - "github_get_custom_properties_values" => { - Function::new_typed_with_env(&mut store, &env, github_get_custom_properties_values) - } - "github_check_codeowners_file" => { - Function::new_typed_with_env(&mut store, &env, github_check_codeowners_file) - } - "github_update_branch_protection_rule" => { - Function::new_typed_with_env(&mut store, &env, github_update_branch_protection_rule) - } - "github_create_environment_for_repo" => { - Function::new_typed_with_env(&mut store, &env, github_create_environment_for_repo) - } - "github_configure_secret" => { - Function::new_typed_with_env(&mut store, &env, github_configure_secret) - } - "github_create_deployment_branch_protection_rule" => Function::new_typed_with_env( - &mut store, - &env, - github_create_deployment_branch_protection_rule, - ), - "github_search_code" => Function::new_typed_with_env(&mut store, &env, github_search_code), - "github_add_users_to_org_copilot" => { - Function::new_typed_with_env(&mut store, &env, github_add_users_to_org_copilot) - } - "github_remove_users_from_org_copilot" => { - Function::new_typed_with_env(&mut store, &env, github_remove_users_from_org_copilot) - } - "github_list_seats_in_org_copilot" => { - Function::new_typed_with_env(&mut store, &env, github_list_seats_in_org_copilot) - } - "github_trigger_repo_dispatch" => { - Function::new_typed_with_env(&mut store, &env, github_trigger_repo_dispatch) - } - "github_check_org_membership_of_user" => { - Function::new_typed_with_env(&mut store, &env, github_check_org_membership_of_user) - } - "github_comment_on_pull_request" => { - Function::new_typed_with_env(&mut store, &env, github_comment_on_pull_request) - } - "github_delete_deploy_key" => { - Function::new_typed_with_env(&mut store, &env, github_delete_deploy_key) - } - "github_pull_request_request_reviewers" => { - Function::new_typed_with_env(&mut store, &env, github_pull_request_request_reviewers) - } - "github_require_signed_commits" => { - Function::new_typed_with_env(&mut store, &env, github_require_signed_commits) - } - "github_get_weekly_commit_count" => { - Function::new_typed_with_env(&mut store, &env, github_get_weekly_commit_count) - } - "github_add_repo_to_team" => { - Function::new_typed_with_env(&mut store, &env, github_add_repo_to_team) - } - "github_remove_repo_from_team" => { - Function::new_typed_with_env(&mut store, &env, github_remove_repo_from_team) - } - "github_get_reference" => { - Function::new_typed_with_env(&mut store, &env, github_get_reference) - } - "github_create_reference" => { - Function::new_typed_with_env(&mut store, &env, github_create_reference) - } - "github_get_pull_requests" => { - Function::new_typed_with_env(&mut store, &env, github_get_pull_requests) - } - "github_create_pull_request" => { - Function::new_typed_with_env(&mut store, &env, github_create_pull_request) - } - "github_create_file" => Function::new_typed_with_env(&mut store, &env, github_create_file), - "github_get_repo_sbom" => { - Function::new_typed_with_env(&mut store, &env, github_get_repo_sbom) - } - "github_add_labels" => Function::new_typed_with_env(&mut store, &env, github_add_labels), - "github_get_user_id_from_username" => { - Function::new_typed_with_env(&mut store, &env, github_get_user_id_from_username) - } - "github_get_username_from_user_id" => { - Function::new_typed_with_env(&mut store, &env, github_get_username_from_user_id) - } - "github_get_repo_id_from_repo_name" => { - Function::new_typed_with_env(&mut store, &env, github_get_repo_id_from_repo_name) - } - "github_get_repo_name_from_repo_id" => { - Function::new_typed_with_env(&mut store, &env, github_get_repo_name_from_repo_id) - } + "github_add_user_to_repo" => github_add_user_to_repo, + "github_remove_user_from_repo" => github_remove_user_from_repo, + "github_add_user_to_team" => github_add_user_to_team, + "github_remove_user_from_team" => github_remove_user_from_team, + "github_make_graphql_query" => github_make_graphql_query, + "github_make_advanced_graphql_query" => github_make_advanced_graphql_query, + "github_fetch_commit" => github_fetch_commit, + "github_list_files" => github_list_files, + "github_fetch_file_with_custom_media_type" => github_fetch_file_with_custom_media_type, + "github_list_fpat_requests_for_org" => github_list_fpat_requests_for_org, + "github_review_fpat_requests_for_org" => github_review_fpat_requests_for_org, + "github_get_repos_for_fpat" => github_get_repos_for_fpat, + "github_get_branch_protection_rules" => github_get_branch_protection_rules, + "github_get_branch_protection_ruleset" => github_get_branch_protection_ruleset, + "github_get_repository_collaborators" => github_get_repository_collaborators, + "github_get_custom_properties_values" => github_get_custom_properties_values, + "github_check_codeowners_file" => github_check_codeowners_file, + "github_update_branch_protection_rule" => github_update_branch_protection_rule, + "github_create_environment_for_repo" => github_create_environment_for_repo, + "github_configure_secret" => github_configure_secret, + "github_create_deployment_branch_protection_rule" => github_create_deployment_branch_protection_rule, + "github_search_code" => github_search_code, + "github_add_users_to_org_copilot" => github_add_users_to_org_copilot, + "github_remove_users_from_org_copilot" => github_remove_users_from_org_copilot, + "github_list_seats_in_org_copilot" => github_list_seats_in_org_copilot, + "github_trigger_repo_dispatch" => github_trigger_repo_dispatch, + "github_check_org_membership_of_user" => github_check_org_membership_of_user, + "github_comment_on_pull_request" => github_comment_on_pull_request, + "github_delete_deploy_key" => github_delete_deploy_key, + "github_pull_request_request_reviewers" => github_pull_request_request_reviewers, + "github_require_signed_commits" => github_require_signed_commits, + "github_get_weekly_commit_count" => github_get_weekly_commit_count, + "github_add_repo_to_team" => github_add_repo_to_team, + "github_remove_repo_from_team" => github_remove_repo_from_team, + "github_get_reference" => github_get_reference, + "github_create_reference" => github_create_reference, + "github_get_pull_requests" => github_get_pull_requests, + "github_create_pull_request" => github_create_pull_request, + "github_create_file" => github_create_file, + "github_get_repo_sbom" => github_get_repo_sbom, + "github_add_labels" => github_add_labels, + "github_get_user_id_from_username" => github_get_user_id_from_username, + "github_get_username_from_user_id" => github_get_username_from_user_id, + "github_get_repo_id_from_repo_name" => github_get_repo_id_from_repo_name, + "github_get_repo_name_from_repo_id" => github_get_repo_name_from_repo_id, // Slack Calls - "slack_post_to_named_webhook" => { - Function::new_typed_with_env(&mut store, &env, slack_post_to_named_webhook) - } - "slack_post_to_arbitrary_webhook" => { - Function::new_typed_with_env(&mut store, &env, slack_post_to_arbitrary_webhook) - } - "slack_post_message" => Function::new_typed_with_env(&mut store, &env, slack_post_message), - "slack_views_open" => Function::new_typed_with_env(&mut store, &env, slack_views_open), - "slack_get_id_from_email" => { - Function::new_typed_with_env(&mut store, &env, slack_get_id_from_email) - } - "slack_get_presence" => Function::new_typed_with_env(&mut store, &env, slack_get_presence), - "slack_get_dnd" => Function::new_typed_with_env(&mut store, &env, slack_get_dnd), - "slack_user_info" => Function::new_typed_with_env(&mut store, &env, slack_user_info), - "slack_create_channel" => { - Function::new_typed_with_env(&mut store, &env, slack_create_channel) - } - "slack_invite_to_channel" => { - Function::new_typed_with_env(&mut store, &env, slack_invite_to_channel) - } + "slack_post_to_named_webhook" => slack_post_to_named_webhook, + "slack_post_to_arbitrary_webhook" => slack_post_to_arbitrary_webhook, + "slack_post_message" => slack_post_message, + "slack_views_open" => slack_views_open, + "slack_get_id_from_email" => slack_get_id_from_email, + "slack_get_presence" => slack_get_presence, + "slack_get_dnd" => slack_get_dnd, + "slack_user_info" => slack_user_info, + "slack_create_channel" => slack_create_channel, + "slack_invite_to_channel" => slack_invite_to_channel, // General Calls - "general_simple_json_post_request" => { - Function::new_typed_with_env(&mut store, &env, general_simple_json_post_request) - } - "general_make_named_request" => { - Function::new_typed_with_env(&mut store, &env, general_make_named_request) - } - - "general_retrieve_tls_certificate_with_sni" => Function::new_typed_with_env( - &mut store, - &env, - general_retrieve_tls_certificate_with_sni, - ), + "general_simple_json_post_request" => general_simple_json_post_request, + "general_make_named_request" => general_make_named_request, + "general_retrieve_tls_certificate_with_sni" => general_retrieve_tls_certificate_with_sni, // Jira Calls - "jira_create_issue" => Function::new_typed_with_env(&mut store, &env, jira_create_issue), - "jira_get_issue" => Function::new_typed_with_env(&mut store, &env, jira_get_issue), - "jira_update_issue" => Function::new_typed_with_env(&mut store, &env, jira_update_issue), - "jira_get_user" => Function::new_typed_with_env(&mut store, &env, jira_get_user), - "jira_post_comment" => Function::new_typed_with_env(&mut store, &env, jira_post_comment), - "jira_search_issues" => Function::new_typed_with_env(&mut store, &env, jira_search_issues), + "jira_create_issue" => jira_create_issue, + "jira_get_issue" => jira_get_issue, + "jira_update_issue" => jira_update_issue, + "jira_get_user" => jira_get_user, + "jira_post_comment" => jira_post_comment, + "jira_search_issues" => jira_search_issues, // KMS calls - #[cfg(feature = "aws")] - "aws_kms_generate_mac" => { - Function::new_typed_with_env(&mut store, &env, aws_kms_generate_mac) - } - - #[cfg(feature = "aws")] - "aws_kms_verify_mac" => Function::new_typed_with_env(&mut store, &env, aws_kms_verify_mac), - - #[cfg(feature = "aws")] - "aws_kms_sign_arbitrary_message" => { - Function::new_typed_with_env(&mut store, &env, aws_kms_sign_arbitrary_message) - } - - #[cfg(feature = "aws")] - "aws_kms_get_public_key" => { - Function::new_typed_with_env(&mut store, &env, aws_kms_get_public_key) - } + #[cfg(feature = "aws")] "aws_kms_generate_mac" => aws_kms_generate_mac, + #[cfg(feature = "aws")] "aws_kms_verify_mac" => aws_kms_verify_mac, + #[cfg(feature = "aws")] "aws_kms_sign_arbitrary_message" => aws_kms_sign_arbitrary_message, + #[cfg(feature = "aws")] "aws_kms_get_public_key" => aws_kms_get_public_key, // DynamoDB calls - #[cfg(feature = "aws")] - "aws_dynamodb_put_item" => { - Function::new_typed_with_env(&mut store, &env, aws_dynamodb_put_item) - } - - #[cfg(feature = "aws")] - "aws_dynamodb_delete_item" => { - Function::new_typed_with_env(&mut store, &env, aws_dynamodb_delete_item) - } - - #[cfg(feature = "aws")] - "aws_dynamodb_query" => Function::new_typed_with_env(&mut store, &env, aws_dynamodb_query), + #[cfg(feature = "aws")] "aws_dynamodb_put_item" => aws_dynamodb_put_item, + #[cfg(feature = "aws")] "aws_dynamodb_delete_item" => aws_dynamodb_delete_item, + #[cfg(feature = "aws")] "aws_dynamodb_query" => aws_dynamodb_query, + + // S3 calls + #[cfg(feature = "aws")] "aws_s3_delete_object" => aws_s3_delete_object, + #[cfg(feature = "aws")] "aws_s3_get_object" => aws_s3_get_object, + #[cfg(feature = "aws")] "aws_s3_get_object_attributes" => aws_s3_get_object_attributes, + #[cfg(feature = "aws")] "aws_s3_list_objects" => aws_s3_list_objects, + #[cfg(feature = "aws")] "aws_s3_list_object_versions" => aws_s3_list_object_versions, + #[cfg(feature = "aws")] "aws_s3_put_object" => aws_s3_put_object, + #[cfg(feature = "aws")] "aws_s3_put_object_tags" => aws_s3_put_object_tags, // GCP - #[cfg(feature = "gcp")] - "gcp_google_docs_upload_file" => { - Function::new_typed_with_env(&mut store, &env, gcp_google_docs_upload_file) - } - - #[cfg(feature = "gcp")] - "gcp_google_docs_copy_file" => { - Function::new_typed_with_env(&mut store, &env, gcp_google_docs_copy_file) - } - - #[cfg(feature = "gcp")] - "gcp_google_docs_create_folder" => { - Function::new_typed_with_env(&mut store, &env, gcp_google_docs_create_folder) - } - - #[cfg(feature = "gcp")] - "gcp_google_docs_create_doc_from_markdown" => { - Function::new_typed_with_env(&mut store, &env, gcp_google_docs_create_doc_from_markdown) - } - - #[cfg(feature = "gcp")] - "gcp_google_docs_create_sheet_from_csv" => { - Function::new_typed_with_env(&mut store, &env, gcp_google_docs_create_sheet_from_csv) - } + #[cfg(feature = "gcp")] "gcp_google_docs_upload_file" => gcp_google_docs_upload_file, + #[cfg(feature = "gcp")] "gcp_google_docs_copy_file" => gcp_google_docs_copy_file, + #[cfg(feature = "gcp")] "gcp_google_docs_create_folder" => gcp_google_docs_create_folder, + #[cfg(feature = "gcp")] "gcp_google_docs_create_doc_from_markdown" => gcp_google_docs_create_doc_from_markdown, + #[cfg(feature = "gcp")] "gcp_google_docs_create_sheet_from_csv" => gcp_google_docs_create_sheet_from_csv, // PagerDuty Calls - "pagerduty_trigger_incident" => { - Function::new_typed_with_env(&mut store, &env, pagerduty_trigger_incident) - } + "pagerduty_trigger_incident" => pagerduty_trigger_incident, // Rustica Calls - "rustica_new_mtls_cert" => { - Function::new_typed_with_env(&mut store, &env, rustica_new_mtls_cert) - } + "rustica_new_mtls_cert" => rustica_new_mtls_cert, // Yubikey Calls - "yubikey_verify_otp" => Function::new_typed_with_env(&mut store, &env, yubikey_verify_otp), - - // S3 Calls - #[cfg(feature = "aws")] - "aws_s3_delete_object" => { - Function::new_typed_with_env(&mut store, &env, aws_s3_delete_object) - } - - #[cfg(feature = "aws")] - "aws_s3_get_object" => Function::new_typed_with_env(&mut store, &env, aws_s3_get_object), - - #[cfg(feature = "aws")] - "aws_s3_get_object_attributes" => { - Function::new_typed_with_env(&mut store, &env, aws_s3_get_object_attributes) - } - - #[cfg(feature = "aws")] - "aws_s3_list_objects" => { - Function::new_typed_with_env(&mut store, &env, aws_s3_list_objects) - } - - #[cfg(feature = "aws")] - "aws_s3_list_object_versions" => { - Function::new_typed_with_env(&mut store, &env, aws_s3_list_object_versions) - } - - #[cfg(feature = "aws")] - "aws_s3_put_object" => Function::new_typed_with_env(&mut store, &env, aws_s3_put_object), - - #[cfg(feature = "aws")] - "aws_s3_put_object_tags" => { - Function::new_typed_with_env(&mut store, &env, aws_s3_put_object_tags) - } + "yubikey_verify_otp" => yubikey_verify_otp, // Splunk Calls - "splunk_post_hec" => Function::new_typed_with_env(&mut store, &env, splunk_post_hec), + "splunk_post_hec" => splunk_post_hec, // Web Calls - "web_issue_jwt" => Function::new_typed_with_env(&mut store, &env, web_issue_jwt), + "web_issue_jwt" => web_issue_jwt, // Blockchain calls - "blockchain_evm_get_transaction_by_hash" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_transaction_by_hash) - } - "blockchain_evm_get_transaction_receipt" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_transaction_receipt) - } - "blockchain_evm_send_raw_transaction" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_send_raw_transaction) - } - "blockchain_evm_get_transaction_count" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_transaction_count) - } - "blockchain_evm_get_balance" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_balance) - } - "blockchain_evm_estimate_gas" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_estimate_gas) - } - "blockchain_evm_eth_call" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_eth_call) - } - "blockchain_evm_gas_price" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_gas_price) - } - "blockchain_evm_get_logs" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_logs) - } - "blockchain_evm_get_block" => { - Function::new_typed_with_env(&mut store, &env, blockchain_evm_get_block) - } + "blockchain_evm_get_transaction_by_hash" => blockchain_evm_get_transaction_by_hash, + "blockchain_evm_get_transaction_receipt" => blockchain_evm_get_transaction_receipt, + "blockchain_evm_send_raw_transaction" => blockchain_evm_send_raw_transaction, + "blockchain_evm_get_transaction_count" => blockchain_evm_get_transaction_count, + "blockchain_evm_get_balance" => blockchain_evm_get_balance, + "blockchain_evm_estimate_gas" => blockchain_evm_estimate_gas, + "blockchain_evm_eth_call" => blockchain_evm_eth_call, + "blockchain_evm_gas_price" => blockchain_evm_gas_price, + "blockchain_evm_get_logs" => blockchain_evm_get_logs, + "blockchain_evm_get_block" => blockchain_evm_get_block, // Bloomfilter calls - "bloom_filter_build_with_items" => { - Function::new_typed_with_env(&mut store, &env, bloom_filter_build_with_items) - } - - // No match - _ => return None, - }) + "bloom_filter_build_with_items" => bloom_filter_build_with_items, + ], + without_env: [ + "get_time" => super::internal::get_time, + ], } diff --git a/runtime/plaid/src/functions/mod.rs b/runtime/plaid/src/functions/mod.rs index e28cca03..d2fe2487 100644 --- a/runtime/plaid/src/functions/mod.rs +++ b/runtime/plaid/src/functions/mod.rs @@ -9,6 +9,7 @@ mod storage; use memory::*; +pub use api::is_known_api_function; use api::to_api_function; use wasmer::{Exports, Function, FunctionEnv, Module, Store}; diff --git a/runtime/plaid/src/loader/errors.rs b/runtime/plaid/src/loader/errors.rs index c1adcf40..2b4a9b22 100644 --- a/runtime/plaid/src/loader/errors.rs +++ b/runtime/plaid/src/loader/errors.rs @@ -1,5 +1,7 @@ use std::fmt::Display; +use crate::storage::StorageError; + #[derive(Debug)] pub enum Errors { InvalidFileType(String, String), @@ -7,6 +9,8 @@ pub enum Errors { SigningError(sshcerts::error::Error), NotEnoughValidSignatures(usize, usize), FileError(std::io::Error), + MissingFunction(String), + StorageError(StorageError), } impl Display for Errors { @@ -23,6 +27,13 @@ impl Display for Errors { "Expected {expected} valid signatures but only received {received}" ), Self::FileError(error) => write!(f, "IO error: {error}"), + Self::MissingFunction(name) => { + write!(f, "Module imports unknown host function: {name}") + } + Self::StorageError(e) => write!( + f, + "Plaid encountered a storage error during module load: {e}" + ), } } } diff --git a/runtime/plaid/src/loader/mod.rs b/runtime/plaid/src/loader/mod.rs index fd792426..2c52022f 100644 --- a/runtime/plaid/src/loader/mod.rs +++ b/runtime/plaid/src/loader/mod.rs @@ -30,6 +30,7 @@ use wasmer::sys::Cranelift; use wasmer::{sys::BaseTunables, Engine, Module, Pages}; use wasmer_middlewares::Metering; +use crate::functions::is_known_api_function; use crate::storage::Storage; /// Limit imposed on some resource @@ -163,6 +164,18 @@ pub struct Configuration { /// If this value is set, Plaid will treat it as an absolute path, create a text file and write "READY" /// when the system is fully up and ready to receive traffic. pub readiness_check_file: Option, + /// Defines how Plaid will behave when it encounters failures during module load. By default, Plaid will skip + /// any failed module. + #[serde(default)] + pub failure_behavior: FailureBehavior, +} + +#[derive(Default, Deserialize)] +pub struct FailureBehavior { + /// If set, panics when Plaid fails to parse the filename and bytes of a provided wasm file + pub panic_on_module_parsing_failure: bool, + /// If set, panics when Plaid fails to compile a WASM blob to a `PlaidModule` + pub panic_on_modele_compilation_failure: bool, } /// Deserializer for a LimitedAmount where none of the provided values can be 0. @@ -212,6 +225,11 @@ pub struct ModuleSigningConfiguration { pub signature_namespace: String, /// The number of valid signatures required on each module pub signatures_required: usize, + /// If this value is set, Plaid will panic if a module signature is invalid + /// + /// Defaults to `false` if not provided. + #[serde(default)] + pub panic_on_invalid_signature: bool, } /// Deserializer for a public key @@ -379,17 +397,29 @@ impl PlaidModule { engine.set_tunables(tunables); // Compile the module using the middleware and tunables we just set up - let mut module = - Module::new(&engine, module_bytes).map_err(|e: wasmer::CompileError| { - error!("Failed to compile module [{filename}]. Error: {e}"); - Errors::CompileError(e) - })?; + let mut module = Module::new(&engine, module_bytes).map_err(Errors::CompileError)?; module.set_name(&filename); + // Validate that every import the module requires can be satisfied + for import in module.imports() { + let function_name = import.name(); + + if function_name.starts_with("__wbindgen") { + continue; + } + + if !is_known_api_function(function_name) { + return Err(Errors::MissingFunction(function_name.to_string())); + } + } + // Count bytes already in storage let storage_current_bytes: u64 = match storage { None => 0, - Some(s) => s.get_namespace_byte_size(filename).await.unwrap(), + Some(s) => s + .get_namespace_byte_size(filename) + .await + .map_err(Errors::StorageError)?, }; let storage_current = Arc::new(RwLock::new(storage_current_bytes)); @@ -475,13 +505,18 @@ pub async fn load( for path in module_paths { let (filename, module_bytes) = match path { - Ok(path) => { - if let Ok(filename_and_bytes) = read_and_parse_modules(&path) { - filename_and_bytes - } else { - continue; + Ok(path) => match read_and_parse_modules(&path) { + Ok(filename_and_bytes) => filename_and_bytes, + Err(e) => { + let file_path = path.path().to_string_lossy().into_owned(); + if config.failure_behavior.panic_on_module_parsing_failure { + panic!("Failed to parse module at [{file_path}]: {e}") + } else { + error!("Failed to parse module at [{file_path}]: {e}. Skipping load"); + continue; + } } - } + }, Err(e) => { error!("Bad entry in modules directory - skipping. Error: {e}"); continue; @@ -492,10 +527,14 @@ pub async fn load( // rule signing. If any rule does not have enough valid signatures it will not be loaded. if let Some(signing) = &config.module_signing { if let Err(e) = check_module_signatures(signing, &filename, &module_bytes) { - error!( - "Module [{filename}] failed signature verification: {e}. Skipping module load" - ); - continue; + if signing.panic_on_invalid_signature { + panic!("Module [{filename}] failed signature verification: {e}") + } else { + error!( + "Module [{filename}] failed signature verification: {e}. Skipping module load" + ); + continue; + } } } @@ -523,7 +562,7 @@ pub async fn load( let test_mode = config.test_mode && !config.test_mode_exemptions.contains(&filename); // Configure and compile module - let Ok(mut plaid_module) = PlaidModule::configure_and_compile( + let mut plaid_module = match PlaidModule::configure_and_compile( &filename, &config.computation_amount, &config.memory_page_count, @@ -535,8 +574,16 @@ pub async fn load( &config.compiler_backend, ) .await - else { - continue; + { + Ok(pm) => pm, + Err(e) => { + if config.failure_behavior.panic_on_modele_compilation_failure { + panic!("Module [{filename}] failed to load: {e}") + } else { + error!("Module [{filename}] failed to load: {e}. Skipping module load"); + continue; + } + } }; // Persistent response is available to be set per module. This allows it to persistently From 2464a4b2b388f2cfe1cef9c541469ec910ec5c34 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Fri, 13 Mar 2026 15:54:48 -0400 Subject: [PATCH 2/6] Fix typo --- runtime/plaid/resources/config/loading.toml | 2 +- runtime/plaid/src/loader/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/runtime/plaid/resources/config/loading.toml b/runtime/plaid/resources/config/loading.toml index a21ebbdb..31e69685 100644 --- a/runtime/plaid/resources/config/loading.toml +++ b/runtime/plaid/resources/config/loading.toml @@ -153,4 +153,4 @@ panic_on_invalid_signature = true [loading.failure_behavior] panic_on_module_parsing_failure = true -panic_on_modele_compilation_failure = true +panic_on_module_compilation_failure = true diff --git a/runtime/plaid/src/loader/mod.rs b/runtime/plaid/src/loader/mod.rs index 2c52022f..5e83069f 100644 --- a/runtime/plaid/src/loader/mod.rs +++ b/runtime/plaid/src/loader/mod.rs @@ -175,7 +175,7 @@ pub struct FailureBehavior { /// If set, panics when Plaid fails to parse the filename and bytes of a provided wasm file pub panic_on_module_parsing_failure: bool, /// If set, panics when Plaid fails to compile a WASM blob to a `PlaidModule` - pub panic_on_modele_compilation_failure: bool, + pub panic_on_module_compilation_failure: bool, } /// Deserializer for a LimitedAmount where none of the provided values can be 0. @@ -577,7 +577,7 @@ pub async fn load( { Ok(pm) => pm, Err(e) => { - if config.failure_behavior.panic_on_modele_compilation_failure { + if config.failure_behavior.panic_on_module_compilation_failure { panic!("Module [{filename}] failed to load: {e}") } else { error!("Module [{filename}] failed to load: {e}. Skipping module load"); From 19e2f665c6e621a865d643a5c9046b2e4fc0b435 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Fri, 13 Mar 2026 15:56:16 -0400 Subject: [PATCH 3/6] Update macro comment --- runtime/plaid/src/functions/api.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/runtime/plaid/src/functions/api.rs b/runtime/plaid/src/functions/api.rs index 5f109ae4..fa59caec 100644 --- a/runtime/plaid/src/functions/api.rs +++ b/runtime/plaid/src/functions/api.rs @@ -619,9 +619,6 @@ impl_new_function_with_error_buffer!(bloom_filter, build_with_items, ALLOW_IN_TE /// /// `with_env` entries produce `Function::new_typed_with_env` /// `without_env` entries produce `Function::new_typed` (for host functions that need no env, e.g. `get_time`). -/// -/// Both generated functions share exactly the same match arms, so adding a new host function -/// only requires one change in the invocation below. macro_rules! define_api_functions { ( with_env: [ From db7a1350427138109082b3c42125eda57f7dab8d Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Fri, 13 Mar 2026 15:59:09 -0400 Subject: [PATCH 4/6] Defaults for failure behavior mode fields --- runtime/plaid/src/loader/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/plaid/src/loader/mod.rs b/runtime/plaid/src/loader/mod.rs index 5e83069f..b492101b 100644 --- a/runtime/plaid/src/loader/mod.rs +++ b/runtime/plaid/src/loader/mod.rs @@ -173,8 +173,10 @@ pub struct Configuration { #[derive(Default, Deserialize)] pub struct FailureBehavior { /// If set, panics when Plaid fails to parse the filename and bytes of a provided wasm file + #[serde(default)] pub panic_on_module_parsing_failure: bool, /// If set, panics when Plaid fails to compile a WASM blob to a `PlaidModule` + #[serde(default)] pub panic_on_module_compilation_failure: bool, } From 9598998c82c330b2ee72e109455c1f1d544105cc Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Tue, 17 Mar 2026 14:03:23 -0400 Subject: [PATCH 5/6] Simplify configuration setup + panic by default --- runtime/plaid/src/loader/mod.rs | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/runtime/plaid/src/loader/mod.rs b/runtime/plaid/src/loader/mod.rs index b492101b..368a8400 100644 --- a/runtime/plaid/src/loader/mod.rs +++ b/runtime/plaid/src/loader/mod.rs @@ -164,20 +164,11 @@ pub struct Configuration { /// If this value is set, Plaid will treat it as an absolute path, create a text file and write "READY" /// when the system is fully up and ready to receive traffic. pub readiness_check_file: Option, - /// Defines how Plaid will behave when it encounters failures during module load. By default, Plaid will skip - /// any failed module. - #[serde(default)] - pub failure_behavior: FailureBehavior, -} - -#[derive(Default, Deserialize)] -pub struct FailureBehavior { - /// If set, panics when Plaid fails to parse the filename and bytes of a provided wasm file - #[serde(default)] - pub panic_on_module_parsing_failure: bool, - /// If set, panics when Plaid fails to compile a WASM blob to a `PlaidModule` - #[serde(default)] - pub panic_on_module_compilation_failure: bool, + /// If this value is set, Plaid will panic if a module fails to load + /// + /// Defaults to `true` if not provided. + #[serde(default = "default_panic_on_load_failure")] + pub panic_on_module_load_failure: bool, } /// Deserializer for a LimitedAmount where none of the provided values can be 0. @@ -229,11 +220,15 @@ pub struct ModuleSigningConfiguration { pub signatures_required: usize, /// If this value is set, Plaid will panic if a module signature is invalid /// - /// Defaults to `false` if not provided. - #[serde(default)] + /// Defaults to `true` if not provided. + #[serde(default = "default_panic_on_load_failure")] pub panic_on_invalid_signature: bool, } +fn default_panic_on_load_failure() -> bool { + true +} + /// Deserializer for a public key fn pubkey_deserializer<'de, D>(deserializer: D) -> Result, D::Error> where @@ -511,7 +506,7 @@ pub async fn load( Ok(filename_and_bytes) => filename_and_bytes, Err(e) => { let file_path = path.path().to_string_lossy().into_owned(); - if config.failure_behavior.panic_on_module_parsing_failure { + if config.panic_on_module_load_failure { panic!("Failed to parse module at [{file_path}]: {e}") } else { error!("Failed to parse module at [{file_path}]: {e}. Skipping load"); @@ -579,7 +574,7 @@ pub async fn load( { Ok(pm) => pm, Err(e) => { - if config.failure_behavior.panic_on_module_compilation_failure { + if config.panic_on_module_load_failure { panic!("Module [{filename}] failed to load: {e}") } else { error!("Module [{filename}] failed to load: {e}. Skipping module load"); From bb7ecc9ee0b8f5e3f9ca9a63238e919d06277fd8 Mon Sep 17 00:00:00 2001 From: Will Spencer Date: Tue, 17 Mar 2026 17:31:05 -0400 Subject: [PATCH 6/6] Remove stale example config --- runtime/plaid/resources/config/loading.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/runtime/plaid/resources/config/loading.toml b/runtime/plaid/resources/config/loading.toml index 31e69685..725e602d 100644 --- a/runtime/plaid/resources/config/loading.toml +++ b/runtime/plaid/resources/config/loading.toml @@ -150,7 +150,3 @@ default = "Unlimited" authorized_signers = ["{plaid-secret{public-key}}"] signatures_required = 1 panic_on_invalid_signature = true - -[loading.failure_behavior] -panic_on_module_parsing_failure = true -panic_on_module_compilation_failure = true