···182182183183/// Host function: get a secret value by name
184184/// Returns a packed i64: (ptr << 32) | len, or 0 on error
185185+///
186186+/// The response is JSON-encoded as `{"ok": "<value>"}` so plugins can
187187+/// deserialize it with the same `Response<String>` envelope they use for
188188+/// other host calls.
185189async fn host_get_secret_impl(
186190 caller: &mut wasmtime::Caller<'_, PluginState>,
187191 name_ptr: i32,
···197201 None => return 0,
198202 };
199203200200- write_guest_response(caller, value.as_bytes()).await
204204+ let response_bytes = serde_json::to_vec(&serde_json::json!({"ok": value})).unwrap_or_default();
205205+ write_guest_response(caller, &response_bytes).await
201206}
202207203208// ============================================================================
tests/fixtures/steam.wasm
This is a binary file and will not be displayed.
+73
tests/plugin_executor.rs
···228228229229 assert!(matches!(result, Err(ExecutionError::PluginNotFound(_))));
230230}
231231+232232+/// Test that host_get_secret returns a JSON envelope that plugins can parse.
233233+/// Uses the real Steam plugin WASM which calls get_secret("API_KEY") in get_profile.
234234+/// This test verifies the fix for the JSON envelope mismatch where the host
235235+/// returned raw bytes but the plugin expected {"ok": "value"}.
236236+#[tokio::test]
237237+async fn test_steam_get_secret_json_envelope() {
238238+ let wasm_path = "tests/fixtures/steam.wasm";
239239+ if !std::path::Path::new(wasm_path).exists() {
240240+ eprintln!("Skipping test: {} not found", wasm_path);
241241+ return;
242242+ }
243243+244244+ let (executor, registry) = create_test_executor().await;
245245+ let wasm_bytes = std::fs::read(wasm_path).expect("Failed to read steam.wasm");
246246+247247+ let plugin = LoadedPlugin {
248248+ info: PluginInfo {
249249+ id: "steam".into(),
250250+ name: "Steam".into(),
251251+ version: "1.1.0".into(),
252252+ api_version: "1".into(),
253253+ icon_url: None,
254254+ required_secrets: vec!["API_KEY".into()],
255255+ auth_type: "openid".into(),
256256+ config_schema: None,
257257+ },
258258+ source: PluginSource::File {
259259+ path: wasm_path.into(),
260260+ },
261261+ wasm_bytes,
262262+ manifest: None,
263263+ };
264264+265265+ registry.register(plugin).await;
266266+267267+ let mut secrets = Secrets::new();
268268+ secrets.insert("API_KEY".to_string(), "test-fake-key".to_string());
269269+270270+ let mut instance = executor
271271+ .instantiate(
272272+ "steam",
273273+ "user:did:plc:test",
274274+ secrets,
275275+ serde_json::Value::Null,
276276+ )
277277+ .await
278278+ .expect("Failed to instantiate Steam plugin");
279279+280280+ // get_profile calls host_get_secret("API_KEY") internally.
281281+ // It will then try to call the Steam API with the fake key, which will fail
282282+ // with an HTTP error — but the important thing is it does NOT fail with
283283+ // MISSING_SECRET, which would mean host_get_secret's JSON envelope is broken.
284284+ let result = instance
285285+ .call_get_profile("76561198024344834", &serde_json::Value::Null)
286286+ .await;
287287+288288+ match result {
289289+ Ok(_) => {} // Unlikely with a fake key, but fine
290290+ Err(ExecutionError::PluginError { code, .. }) => {
291291+ // HTTP_ERROR = Steam API rejected our fake key (expected)
292292+ // INVALID_RESPONSE = Steam returned unexpected format (also fine)
293293+ // MISSING_SECRET = host_get_secret envelope is broken (BAD!)
294294+ assert_ne!(
295295+ code, "MISSING_SECRET",
296296+ "host_get_secret JSON envelope is broken — plugin couldn't parse the secret"
297297+ );
298298+ }
299299+ Err(e) => {
300300+ panic!("Unexpected error type: {:?}", e);
301301+ }
302302+ }
303303+}