feat(plugins): annotation to allow panic

This commit is contained in:
Edouard Vanbelle
2026-07-03 02:07:53 +02:00
parent 92d1a10d45
commit 0a6368e1bf
9 changed files with 62 additions and 21 deletions
+21 -3
View File
@@ -171,14 +171,32 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# Pinned to match wasm/oxicloud-plugin-hello/rust-toolchain.toml.
# Reproducibility of the committed .wasm fixtures depends on both
# sides using the same rustc — even a patch bump shifts codegen.
# Bump both together.
- uses: dtolnay/rust-toolchain@1.96.1
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: Rebuild committed wasm fixtures
run: bash scripts/build-plugin-hello.sh
- name: Fail if fixtures are stale (rebuild + commit them)
run: git diff --exit-code tests/fixtures/plugins/
# NOTE: no `git diff --exit-code` staleness check.
#
# Cross-host wasm builds (contributor aarch64-macOS vs CI
# x86_64-linux, same rustc 1.96.1, same `--remap-path-prefix`,
# same `CARGO_INCREMENTAL=0`, same profile) still produce
# byte-different .wasm — plain `cargo build` doesn't guarantee
# bit-reproducible cross-host wasm output. The proper fixes
# (containerised builds, or dropping the committed fixtures and
# rebuilding from source everywhere) are deferred; the frontend
# wasm crate (`wasm/oxicloud-hash`) will hit the same wall when
# we add a similar check for it, so we'll tackle both together.
# For now: CI rebuilds the fixtures fresh above and uses those
# for the plugin runtime tests below. The versions committed at
# HEAD are a convenience for local dev without the wasm32
# toolchain — they may drift from what CI produces, which is
# fine as long as the runtime tests pass.
- name: Run plugin runtime tests
# Quote: the trailing `::` confuses GitHub's YAML parser (mapping
# values not allowed) and aborts the whole workflow at load time.
+13
View File
@@ -23,6 +23,19 @@ OUT=tests/fixtures/plugins
export CARGO_TARGET_DIR="$PWD/$CRATE/target"
ARTIFACT="$CRATE/target/wasm32-unknown-unknown/release/oxicloud_plugin_hello.wasm"
# Reproducibility: the CI plugins job runs `git diff --exit-code` against
# the committed fixtures, so any environment drift (absolute paths in
# debug info, incremental caches, host-specific codegen) breaks the check.
# * `--remap-path-prefix` strips the source directory from any residual
# path strings (panic message file paths mostly).
# * `CARGO_INCREMENTAL=0` forces a from-scratch compilation — incremental
# artifacts are not bit-reproducible across cache states.
# The Rust version itself is pinned via
# `wasm/oxicloud-plugin-hello/rust-toolchain.toml`; keep it in sync with
# the CI action tag in `.github/workflows/ci.yml` (`plugins` job).
export CARGO_INCREMENTAL=0
export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$PWD/$CRATE=."
# Needs the wasm32-unknown-unknown target's std. In the devenv this comes from
# `languages.rust.targets` in devenv.nix; otherwise run
# `rustup target add wasm32-unknown-unknown`. cargo emits a clear "can't find
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+28 -18
View File
@@ -37,8 +37,12 @@ pub fn abi_version() -> FnResult<u32> {
}
/// Handler for the `file.uploaded` event.
///
/// `#[plugin_fn]` rewrites the fn signature, so an outer `#[allow]` doesn't
/// reach the inner scope where `input` is bound — hence the `_` prefix on the
/// parameter. The well-behaved tail rebinds it as `input` locally.
#[plugin_fn]
pub fn on_file_uploaded(input: String) -> FnResult<String> {
pub fn on_file_uploaded(_input: String) -> FnResult<String> {
// --- misbehaving variants (compiled in only under their feature) ---------
#[cfg(feature = "panic")]
panic!("intentional panic: exercises host failure isolation");
@@ -53,27 +57,33 @@ pub fn on_file_uploaded(input: String) -> FnResult<String> {
}
}
#[cfg(feature = "net")]
// The well-behaved tail is unreachable under the diverging variants above;
// gate it so the compiler doesn't flag input/tail as unused/dead.
#[cfg(not(any(feature = "panic", feature = "sleep")))]
{
// Attempt an outbound HTTP call. The host grants no `allowed_hosts`, so
// Extism denies this before any socket is opened (offline-deterministic)
// and the error propagates out of the handler.
let req = HttpRequest::new("https://example.com/");
let _ = http::request::<()>(&req, None)?;
}
let input = _input;
// --- well-behaved path ---------------------------------------------------
let ev: serde_json::Value = serde_json::from_str(&input)?;
let path = ev["payload"]["path"].as_str().unwrap_or("<unknown>");
let size = ev["payload"]["size"].as_u64().unwrap_or(0);
#[cfg(feature = "net")]
{
// Attempt an outbound HTTP call. The host grants no `allowed_hosts`,
// so Extism denies this before any socket is opened
// (offline-deterministic) and the error propagates out.
let req = HttpRequest::new("https://example.com/");
let _ = http::request::<()>(&req, None)?;
}
unsafe {
log(
"info".to_string(),
format!("hello plugin saw upload: {path} ({size} bytes)"),
)?;
let ev: serde_json::Value = serde_json::from_str(&input)?;
let path = ev["payload"]["path"].as_str().unwrap_or("<unknown>");
let size = ev["payload"]["size"].as_u64().unwrap_or(0);
unsafe {
log(
"info".to_string(),
format!("hello plugin saw upload: {path} ({size} bytes)"),
)?;
}
Ok(serde_json::json!({ "ok": true }).to_string())
}
Ok(serde_json::json!({ "ok": true }).to_string())
}
/// Handler for the `user.login` event. Dropped by the `omit_login` variant so