1use crate::{MAX_HOSTCALL_RESPONSE_BYTES, WorkerError};
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::collections::BTreeMap;
5
6type Hostcall = unsafe extern "C" fn(i32, i32, i32, i32) -> i32;
7
8#[derive(Debug, Deserialize)]
9struct HostcallResponseDiscriminator {
10 ok: bool,
11}
12
13#[derive(Debug, Deserialize)]
14#[serde(deny_unknown_fields)]
15struct HostcallFailure {
16 ok: bool,
17 code: String,
18 message: String,
19 error_origin: String,
20}
21
22fn call_host<Request, Response>(
23 request: &Request,
24 hostcall: Hostcall,
25) -> Result<Response, WorkerError>
26where
27 Request: Serialize,
28 Response: DeserializeOwned,
29{
30 let request_bytes = serde_json::to_vec(request)
31 .map_err(|err| WorkerError::hostcall(format!("encode hostcall request: {err}")))?;
32 let mut response = vec![0_u8; MAX_HOSTCALL_RESPONSE_BYTES];
33 let written = unsafe {
34 hostcall(
35 request_bytes.as_ptr() as i32,
36 request_bytes.len() as i32,
37 response.as_mut_ptr() as i32,
38 response.len() as i32,
39 )
40 };
41 if written < 0 {
42 return Err(WorkerError::hostcall(format!(
43 "hostcall failed with ABI code {written}"
44 )));
45 }
46 let written = usize::try_from(written)
47 .map_err(|_| WorkerError::hostcall("hostcall response length is invalid"))?;
48 if written > response.len() {
49 return Err(WorkerError::hostcall(
50 "hostcall response exceeded the provided buffer",
51 ));
52 }
53 response.truncate(written);
54 decode_hostcall_response(&response)
55}
56
57fn decode_hostcall_response<Response>(response: &[u8]) -> Result<Response, WorkerError>
58where
59 Response: DeserializeOwned,
60{
61 let discriminator: HostcallResponseDiscriminator =
62 serde_json::from_slice(response).map_err(|err| {
63 WorkerError::hostcall(format!("decode hostcall response discriminator: {err}"))
64 })?;
65 if discriminator.ok {
66 return serde_json::from_slice(response).map_err(|err| {
67 WorkerError::hostcall(format!("decode typed hostcall response: {err}"))
68 });
69 }
70 let failure: HostcallFailure = serde_json::from_slice(response)
71 .map_err(|err| WorkerError::hostcall(format!("decode hostcall failure: {err}")))?;
72 let code = failure.code.trim();
73 let message = failure.message.trim();
74 if failure.ok
75 || failure.error_origin != "hostcall"
76 || !stable_error_code(code)
77 || message.is_empty()
78 || message.chars().count() > 4096
79 {
80 return Err(WorkerError::hostcall(
81 "hostcall failure response violates the closed contract",
82 ));
83 }
84 Err(WorkerError::new(code, message))
85}
86
87fn stable_error_code(value: &str) -> bool {
88 !value.is_empty()
89 && value.len() <= 128
90 && value.bytes().enumerate().all(|(index, byte)| {
91 byte.is_ascii_uppercase() || byte.is_ascii_digit() || (index > 0 && byte == b'_')
92 })
93 && value.as_bytes().first().is_some_and(u8::is_ascii_uppercase)
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct Usage {
99 pub plugin_instance_id: String,
100 pub store_id: String,
101 pub usage_bytes: i64,
102 pub quota_bytes: i64,
103 pub usage_files: i64,
104 pub quota_files: i64,
105}
106
107#[derive(Serialize)]
108struct OperationRequest<'a, Request> {
109 operation: &'static str,
110 #[serde(flatten)]
111 request: &'a Request,
112}
113
114#[link(wasm_import_module = "redevplugin.storage")]
115unsafe extern "C" {
116 #[link_name = "files"]
117 fn storage_files_hostcall(
118 request_ptr: i32,
119 request_len: i32,
120 response_ptr: i32,
121 response_len: i32,
122 ) -> i32;
123 #[link_name = "kv"]
124 fn storage_kv_hostcall(
125 request_ptr: i32,
126 request_len: i32,
127 response_ptr: i32,
128 response_len: i32,
129 ) -> i32;
130 #[link_name = "sqlite"]
131 fn storage_sqlite_hostcall(
132 request_ptr: i32,
133 request_len: i32,
134 response_ptr: i32,
135 response_len: i32,
136 ) -> i32;
137}
138
139#[link(wasm_import_module = "redevplugin.network")]
140unsafe extern "C" {
141 #[link_name = "execute"]
142 fn network_execute_hostcall(
143 request_ptr: i32,
144 request_len: i32,
145 response_ptr: i32,
146 response_len: i32,
147 ) -> i32;
148}
149
150pub mod storage {
151 use super::*;
152
153 pub mod files {
154 use super::*;
155
156 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157 #[serde(deny_unknown_fields)]
158 pub struct ReadRequest {
159 pub store_id: String,
160 pub path: String,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub max_bytes: Option<u64>,
163 }
164
165 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166 #[serde(deny_unknown_fields)]
167 pub struct ReadResponse {
168 pub ok: bool,
169 pub path: String,
170 pub data_base64: String,
171 pub size_bytes: i64,
172 pub usage: Usage,
173 }
174
175 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176 #[serde(deny_unknown_fields)]
177 pub struct WriteRequest {
178 pub store_id: String,
179 pub path: String,
180 pub data_base64: String,
181 }
182
183 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184 #[serde(deny_unknown_fields)]
185 pub struct WriteResponse {
186 pub ok: bool,
187 pub path: String,
188 pub size_bytes: i64,
189 pub usage: Usage,
190 }
191
192 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193 #[serde(deny_unknown_fields)]
194 pub struct DeleteRequest {
195 pub store_id: String,
196 pub path: String,
197 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
198 pub recursive: bool,
199 }
200
201 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202 #[serde(deny_unknown_fields)]
203 pub struct DeleteResponse {
204 pub ok: bool,
205 pub path: String,
206 }
207
208 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209 #[serde(deny_unknown_fields)]
210 pub struct ListRequest {
211 pub store_id: String,
212 #[serde(default, skip_serializing_if = "String::is_empty")]
213 pub path: String,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub max_entries: Option<u32>,
216 }
217
218 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219 #[serde(deny_unknown_fields)]
220 pub struct Entry {
221 pub path: String,
222 pub dir: bool,
223 #[serde(default)]
224 pub size_bytes: i64,
225 pub updated_at: String,
226 }
227
228 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229 #[serde(deny_unknown_fields)]
230 pub struct ListResponse {
231 pub ok: bool,
232 pub path: String,
233 pub entries: Vec<Entry>,
234 pub usage: Usage,
235 }
236
237 pub fn read(request: ReadRequest) -> Result<ReadResponse, WorkerError> {
238 call_host(
239 &OperationRequest {
240 operation: "read",
241 request: &request,
242 },
243 storage_files_hostcall,
244 )
245 }
246
247 pub fn write(request: WriteRequest) -> Result<WriteResponse, WorkerError> {
248 call_host(
249 &OperationRequest {
250 operation: "write",
251 request: &request,
252 },
253 storage_files_hostcall,
254 )
255 }
256
257 pub fn delete(request: DeleteRequest) -> Result<DeleteResponse, WorkerError> {
258 call_host(
259 &OperationRequest {
260 operation: "delete",
261 request: &request,
262 },
263 storage_files_hostcall,
264 )
265 }
266
267 pub fn list(request: ListRequest) -> Result<ListResponse, WorkerError> {
268 call_host(
269 &OperationRequest {
270 operation: "list",
271 request: &request,
272 },
273 storage_files_hostcall,
274 )
275 }
276 }
277
278 pub mod kv {
279 use super::*;
280
281 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282 #[serde(deny_unknown_fields)]
283 pub struct GetRequest {
284 pub store_id: String,
285 pub key: String,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub max_bytes: Option<u64>,
288 }
289
290 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291 #[serde(deny_unknown_fields)]
292 pub struct GetResponse {
293 pub ok: bool,
294 pub key: String,
295 pub value_base64: String,
296 pub size_bytes: i64,
297 pub usage: Usage,
298 }
299
300 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301 #[serde(deny_unknown_fields)]
302 pub struct PutRequest {
303 pub store_id: String,
304 pub key: String,
305 pub value_base64: String,
306 }
307
308 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309 #[serde(deny_unknown_fields)]
310 pub struct PutResponse {
311 pub ok: bool,
312 pub key: String,
313 pub size_bytes: i64,
314 pub usage: Usage,
315 }
316
317 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318 #[serde(deny_unknown_fields)]
319 pub struct DeleteRequest {
320 pub store_id: String,
321 pub key: String,
322 }
323
324 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325 #[serde(deny_unknown_fields)]
326 pub struct DeleteResponse {
327 pub ok: bool,
328 pub key: String,
329 }
330
331 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332 #[serde(deny_unknown_fields)]
333 pub struct ListRequest {
334 pub store_id: String,
335 #[serde(default, skip_serializing_if = "String::is_empty")]
336 pub prefix: String,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub max_entries: Option<u32>,
339 }
340
341 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342 #[serde(deny_unknown_fields)]
343 pub struct Entry {
344 pub key: String,
345 pub size_bytes: i64,
346 pub updated_at: String,
347 }
348
349 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350 #[serde(deny_unknown_fields)]
351 pub struct ListResponse {
352 pub ok: bool,
353 #[serde(default)]
354 pub prefix: String,
355 pub entries: Vec<Entry>,
356 pub usage: Usage,
357 }
358
359 pub fn get(request: GetRequest) -> Result<GetResponse, WorkerError> {
360 call_host(
361 &OperationRequest {
362 operation: "get",
363 request: &request,
364 },
365 storage_kv_hostcall,
366 )
367 }
368
369 pub fn put(request: PutRequest) -> Result<PutResponse, WorkerError> {
370 call_host(
371 &OperationRequest {
372 operation: "put",
373 request: &request,
374 },
375 storage_kv_hostcall,
376 )
377 }
378
379 pub fn delete(request: DeleteRequest) -> Result<DeleteResponse, WorkerError> {
380 call_host(
381 &OperationRequest {
382 operation: "delete",
383 request: &request,
384 },
385 storage_kv_hostcall,
386 )
387 }
388
389 pub fn list(request: ListRequest) -> Result<ListResponse, WorkerError> {
390 call_host(
391 &OperationRequest {
392 operation: "list",
393 request: &request,
394 },
395 storage_kv_hostcall,
396 )
397 }
398 }
399
400 pub mod sqlite {
401 use super::*;
402
403 #[derive(Debug, Clone, PartialEq)]
404 pub enum Value {
405 Null,
406 Integer(i64),
407 Float(f64),
408 Text(String),
409 BlobBase64(String),
410 }
411
412 #[derive(Serialize, Deserialize)]
413 #[serde(deny_unknown_fields)]
414 struct ValueWire {
415 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
416 null: bool,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 int: Option<i64>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 float: Option<f64>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 text: Option<String>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 blob_base64: Option<String>,
425 }
426
427 impl Serialize for Value {
428 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
429 where
430 S: Serializer,
431 {
432 let wire = match self {
433 Self::Null => ValueWire {
434 null: true,
435 int: None,
436 float: None,
437 text: None,
438 blob_base64: None,
439 },
440 Self::Integer(value) => ValueWire {
441 null: false,
442 int: Some(*value),
443 float: None,
444 text: None,
445 blob_base64: None,
446 },
447 Self::Float(value) => ValueWire {
448 null: false,
449 int: None,
450 float: Some(*value),
451 text: None,
452 blob_base64: None,
453 },
454 Self::Text(value) => ValueWire {
455 null: false,
456 int: None,
457 float: None,
458 text: Some(value.clone()),
459 blob_base64: None,
460 },
461 Self::BlobBase64(value) => ValueWire {
462 null: false,
463 int: None,
464 float: None,
465 text: None,
466 blob_base64: Some(value.clone()),
467 },
468 };
469 wire.serialize(serializer)
470 }
471 }
472
473 impl<'de> Deserialize<'de> for Value {
474 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
475 where
476 D: Deserializer<'de>,
477 {
478 let wire = ValueWire::deserialize(deserializer)?;
479 let variants = usize::from(wire.null)
480 + usize::from(wire.int.is_some())
481 + usize::from(wire.float.is_some())
482 + usize::from(wire.text.is_some())
483 + usize::from(wire.blob_base64.is_some());
484 if variants != 1 {
485 return Err(serde::de::Error::custom(
486 "SQLite value must contain exactly one typed field",
487 ));
488 }
489 if wire.null {
490 return Ok(Self::Null);
491 }
492 if let Some(value) = wire.int {
493 return Ok(Self::Integer(value));
494 }
495 if let Some(value) = wire.float {
496 return Ok(Self::Float(value));
497 }
498 if let Some(value) = wire.text {
499 return Ok(Self::Text(value));
500 }
501 Ok(Self::BlobBase64(wire.blob_base64.unwrap_or_default()))
502 }
503 }
504
505 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
506 #[serde(deny_unknown_fields)]
507 pub struct ExecRequest {
508 pub store_id: String,
509 #[serde(default, skip_serializing_if = "String::is_empty")]
510 pub database: String,
511 pub sql: String,
512 #[serde(default, skip_serializing_if = "Vec::is_empty")]
513 pub args: Vec<Value>,
514 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub timeout_ms: Option<u64>,
516 }
517
518 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519 #[serde(deny_unknown_fields)]
520 pub struct ExecResponse {
521 pub ok: bool,
522 pub database: String,
523 pub rows_affected: i64,
524 #[serde(default)]
525 pub last_insert_id: i64,
526 pub usage: Usage,
527 }
528
529 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
530 #[serde(deny_unknown_fields)]
531 pub struct QueryRequest {
532 pub store_id: String,
533 #[serde(default, skip_serializing_if = "String::is_empty")]
534 pub database: String,
535 pub sql: String,
536 #[serde(default, skip_serializing_if = "Vec::is_empty")]
537 pub args: Vec<Value>,
538 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub max_rows: Option<u32>,
540 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub max_response_bytes: Option<u64>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
543 pub timeout_ms: Option<u64>,
544 }
545
546 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
547 #[serde(deny_unknown_fields)]
548 pub struct QueryResponse {
549 pub ok: bool,
550 pub database: String,
551 pub columns: Vec<String>,
552 pub rows: Vec<Vec<Value>>,
553 pub usage: Usage,
554 }
555
556 pub fn exec(request: ExecRequest) -> Result<ExecResponse, WorkerError> {
557 call_host(
558 &OperationRequest {
559 operation: "exec",
560 request: &request,
561 },
562 storage_sqlite_hostcall,
563 )
564 }
565
566 pub fn query(request: QueryRequest) -> Result<QueryResponse, WorkerError> {
567 call_host(
568 &OperationRequest {
569 operation: "query",
570 request: &request,
571 },
572 storage_sqlite_hostcall,
573 )
574 }
575 }
576}
577
578pub mod network {
579 use super::*;
580
581 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
582 #[serde(rename_all = "snake_case")]
583 pub enum Transport {
584 Http,
585 Websocket,
586 Tcp,
587 Udp,
588 }
589
590 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
591 #[serde(rename_all = "snake_case")]
592 pub enum Operation {
593 Http,
594 HttpStream,
595 WebsocketRoundTrip,
596 TcpRoundTrip,
597 UdpRoundTrip,
598 }
599
600 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601 #[serde(deny_unknown_fields)]
602 pub struct ExecuteRequest {
603 pub connector_id: String,
604 pub transport: Transport,
605 pub destination: String,
606 pub operation: Operation,
607 #[serde(default, skip_serializing_if = "String::is_empty")]
608 pub method: String,
609 #[serde(default, skip_serializing_if = "String::is_empty")]
610 pub path: String,
611 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
612 pub query: BTreeMap<String, Vec<String>>,
613 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
614 pub headers: BTreeMap<String, Vec<String>>,
615 #[serde(default, skip_serializing_if = "String::is_empty")]
616 pub message_type: String,
617 #[serde(default, skip_serializing_if = "String::is_empty")]
618 pub body_base64: String,
619 #[serde(default, skip_serializing_if = "String::is_empty")]
620 pub payload_base64: String,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub ttl_ms: Option<u64>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub max_request_bytes: Option<u64>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub max_response_bytes: Option<u64>,
627 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub max_chunk_bytes: Option<u64>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub max_buffered_bytes: Option<u64>,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub timeout_ms: Option<u64>,
633 #[serde(default, skip_serializing_if = "String::is_empty")]
634 pub content_type: String,
635 }
636
637 impl ExecuteRequest {
638 pub fn http_get(
639 connector_id: impl Into<String>,
640 destination: impl Into<String>,
641 path: impl Into<String>,
642 ) -> Self {
643 Self {
644 connector_id: connector_id.into(),
645 transport: Transport::Http,
646 destination: destination.into(),
647 operation: Operation::Http,
648 method: "GET".to_string(),
649 path: path.into(),
650 query: BTreeMap::new(),
651 headers: BTreeMap::new(),
652 message_type: String::new(),
653 body_base64: String::new(),
654 payload_base64: String::new(),
655 ttl_ms: None,
656 max_request_bytes: None,
657 max_response_bytes: None,
658 max_chunk_bytes: None,
659 max_buffered_bytes: None,
660 timeout_ms: None,
661 content_type: String::new(),
662 }
663 }
664 }
665
666 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
667 #[serde(deny_unknown_fields)]
668 pub struct Destination {
669 pub transport: Transport,
670 #[serde(default)]
671 pub scheme: String,
672 pub host: String,
673 pub port: u16,
674 }
675
676 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
677 #[serde(deny_unknown_fields)]
678 pub struct ExecuteResponse {
679 pub ok: bool,
680 #[serde(default)]
681 pub transport: Option<Transport>,
682 #[serde(default)]
683 pub destination: Option<Destination>,
684 #[serde(default)]
685 pub status_code: Option<u16>,
686 #[serde(default)]
687 pub headers: BTreeMap<String, Vec<String>>,
688 #[serde(default)]
689 pub message_type: String,
690 #[serde(default)]
691 pub body_base64: String,
692 #[serde(default)]
693 pub payload_base64: String,
694 #[serde(default)]
695 pub stream_id: String,
696 #[serde(default)]
697 pub bytes_read: i64,
698 #[serde(default)]
699 pub chunk_count: u32,
700 #[serde(default)]
701 pub grant_id: String,
702 #[serde(default)]
703 pub connector_id: String,
704 #[serde(default)]
705 pub runtime_generation_id: String,
706 }
707
708 pub fn execute(request: ExecuteRequest) -> Result<ExecuteResponse, WorkerError> {
709 call_host(&request, network_execute_hostcall)
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716 use serde_json::json;
717
718 #[test]
719 fn sqlite_values_are_exactly_typed() {
720 let values = vec![
721 storage::sqlite::Value::Null,
722 storage::sqlite::Value::Integer(7),
723 storage::sqlite::Value::Float(1.5),
724 storage::sqlite::Value::Text("memo".to_string()),
725 storage::sqlite::Value::BlobBase64("AAE=".to_string()),
726 ];
727 let encoded = serde_json::to_value(&values).unwrap();
728 let decoded: Vec<storage::sqlite::Value> = serde_json::from_value(encoded).unwrap();
729 assert_eq!(decoded, values);
730 assert!(
731 serde_json::from_value::<storage::sqlite::Value>(json!({"int": 1, "text": "x"}))
732 .is_err()
733 );
734 assert!(
735 serde_json::from_value::<storage::sqlite::Value>(
736 json!({"text": "x", "token": "secret"})
737 )
738 .is_err()
739 );
740 }
741
742 #[test]
743 fn network_request_cannot_encode_host_owned_invocation_fields() {
744 let mut request = network::ExecuteRequest::http_get(
745 "forecast",
746 "https://api.example.com",
747 "/v1/forecast",
748 );
749 request
750 .query
751 .insert("latitude".to_string(), vec!["52.52".to_string()]);
752 let encoded = serde_json::to_value(request).unwrap();
753 assert_eq!(encoded["operation"], "http");
754 for forbidden in [
755 "stream_id",
756 "surface_instance_id",
757 "owner_session_hash",
758 "owner_user_hash",
759 "owner_env_hash",
760 "session_channel_id_hash",
761 "bridge_channel_id",
762 ] {
763 assert!(encoded.get(forbidden).is_none(), "unexpected {forbidden}");
764 }
765 }
766
767 #[test]
768 fn typed_success_responses_reject_unknown_fields() {
769 let response = json!({
770 "ok": true,
771 "database": "notes.sqlite",
772 "columns": ["title"],
773 "rows": [[{"text": "Launch"}]],
774 "usage": {
775 "plugin_instance_id": "plugini_1",
776 "store_id": "notes",
777 "usage_bytes": 10,
778 "quota_bytes": 100,
779 "usage_files": 1,
780 "quota_files": 4
781 },
782 "handle_grant_token": "secret"
783 });
784 assert!(serde_json::from_value::<storage::sqlite::QueryResponse>(response).is_err());
785 }
786
787 #[test]
788 fn operation_specific_storage_responses_reject_cross_operation_fields() {
789 let usage = json!({
790 "plugin_instance_id": "plugini_1",
791 "store_id": "workspace",
792 "usage_bytes": 10,
793 "quota_bytes": 100,
794 "usage_files": 1,
795 "quota_files": 4
796 });
797 let cases = [
798 serde_json::to_vec(&json!({
799 "ok": true,
800 "path": "notes/a.txt",
801 "data_base64": "YQ==",
802 "size_bytes": 1,
803 "entries": [],
804 "usage": usage.clone()
805 }))
806 .unwrap(),
807 serde_json::to_vec(&json!({
808 "ok": true,
809 "path": "notes/a.txt",
810 "size_bytes": 1,
811 "data_base64": "YQ==",
812 "usage": usage.clone()
813 }))
814 .unwrap(),
815 serde_json::to_vec(&json!({
816 "ok": true,
817 "path": "notes/a.txt",
818 "usage": usage.clone()
819 }))
820 .unwrap(),
821 serde_json::to_vec(&json!({
822 "ok": true,
823 "path": "notes",
824 "entries": [],
825 "data_base64": "YQ==",
826 "usage": usage.clone()
827 }))
828 .unwrap(),
829 ];
830 assert!(decode_hostcall_response::<storage::files::ReadResponse>(&cases[0]).is_err());
831 assert!(decode_hostcall_response::<storage::files::WriteResponse>(&cases[1]).is_err());
832 assert!(decode_hostcall_response::<storage::files::DeleteResponse>(&cases[2]).is_err());
833 assert!(decode_hostcall_response::<storage::files::ListResponse>(&cases[3]).is_err());
834
835 let kv_get = serde_json::to_vec(&json!({
836 "ok": true,
837 "key": "theme",
838 "value_base64": "ZGFyaw==",
839 "size_bytes": 4,
840 "entries": [],
841 "usage": usage.clone()
842 }))
843 .unwrap();
844 let kv_put = serde_json::to_vec(&json!({
845 "ok": true,
846 "key": "theme",
847 "size_bytes": 4,
848 "value_base64": "ZGFyaw==",
849 "usage": usage.clone()
850 }))
851 .unwrap();
852 let kv_delete = serde_json::to_vec(&json!({
853 "ok": true,
854 "key": "theme",
855 "usage": usage.clone()
856 }))
857 .unwrap();
858 let kv_list = serde_json::to_vec(&json!({
859 "ok": true,
860 "prefix": "settings/",
861 "entries": [],
862 "value_base64": "ZGFyaw==",
863 "usage": usage.clone()
864 }))
865 .unwrap();
866 assert!(decode_hostcall_response::<storage::kv::GetResponse>(&kv_get).is_err());
867 assert!(decode_hostcall_response::<storage::kv::PutResponse>(&kv_put).is_err());
868 assert!(decode_hostcall_response::<storage::kv::DeleteResponse>(&kv_delete).is_err());
869 assert!(decode_hostcall_response::<storage::kv::ListResponse>(&kv_list).is_err());
870
871 let sqlite_exec = serde_json::to_vec(&json!({
872 "ok": true,
873 "database": "notes.sqlite",
874 "rows_affected": 1,
875 "columns": [],
876 "rows": [],
877 "usage": usage.clone()
878 }))
879 .unwrap();
880 let sqlite_query = serde_json::to_vec(&json!({
881 "ok": true,
882 "database": "notes.sqlite",
883 "columns": [],
884 "rows": [],
885 "rows_affected": 1,
886 "usage": usage
887 }))
888 .unwrap();
889 assert!(decode_hostcall_response::<storage::sqlite::ExecResponse>(&sqlite_exec).is_err());
890 assert!(decode_hostcall_response::<storage::sqlite::QueryResponse>(&sqlite_query).is_err());
891 }
892
893 #[test]
894 fn hostcall_failures_require_the_closed_failure_contract() {
895 let failure = br#"{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"blocked","error_origin":"hostcall"}"#;
896 let error = decode_hostcall_response::<network::ExecuteResponse>(failure)
897 .expect_err("closed hostcall failure");
898 assert_eq!(error.code, "NETWORK_TARGET_DENIED");
899 assert_eq!(error.message, "blocked");
900
901 for invalid in [
902 br#"{"ok":false,"message":"blocked","error_origin":"hostcall"}"#.as_slice(),
903 br#"{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"blocked","error_origin":"runtime"}"#.as_slice(),
904 br#"{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"blocked","error_origin":"hostcall","future":true}"#.as_slice(),
905 br#"{"ok":false,"code":"NETWORK_TARGET_DENIED","code":"NETWORK_CONNECTOR_DENIED","message":"blocked","error_origin":"hostcall"}"#.as_slice(),
906 br#"{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"blocked","error_origin":"hostcall"} {}"#.as_slice(),
907 ] {
908 assert!(
909 decode_hostcall_response::<network::ExecuteResponse>(invalid).is_err(),
910 "accepted invalid hostcall failure: {}",
911 String::from_utf8_lossy(invalid)
912 );
913 }
914 }
915}