1use base64::Engine as _;
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use serde::de::DeserializeOwned;
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::{HashMap, HashSet};
7use std::sync::OnceLock;
8
9mod contract_set_gen;
10
11pub use contract_set_gen::CONTRACT_SET_SHA256;
12
13pub const RUST_IPC_VERSION: &str = "rust-ipc-v6";
14pub const WASM_ABI_VERSION: &str = "redevplugin-wasm-worker-v2";
15pub const RUNTIME_LEASE_SIGNATURE_SCHEMA_VERSION: &str = "redevplugin.runtime_execution_lease.v1";
16
17#[cfg(test)]
18fn contract_fixture(id: redevplugin_contracts::ContractId) -> &'static str {
19 std::str::from_utf8(redevplugin_contracts::get(id).bytes())
20 .expect("generated contracts are valid UTF-8")
21}
22pub const RUNTIME_LEASE_TOKEN_KIND: &str = "runtime_execution_lease";
23pub const RUNTIME_LEASE_SIGNATURE_ALGORITHM: &str = "ed25519";
24pub const WORKER_INVOCATION_TARGET_SCHEMA_VERSION: &str = "redevplugin.worker_invocation_target.v1";
25pub const MAX_RUNTIME_LEASE_MEMORY_BYTES: u64 = 256 * 1024 * 1024;
26pub const MAX_JSON_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;
27pub const MIN_RUNTIME_WORKER_COUNT: usize = 1;
28pub const MAX_RUNTIME_WORKER_COUNT: usize = 64;
29pub const MIN_RUNTIME_QUEUE_CAPACITY: usize = 1;
30pub const MAX_RUNTIME_QUEUE_CAPACITY: usize = 64;
31pub const MIN_RUNTIME_PER_PLUGIN_CONCURRENCY: usize = 1;
32pub const MAX_RUNTIME_PER_PLUGIN_CONCURRENCY: usize = 64;
33pub const MIN_RUNTIME_MODULE_CACHE_ENTRIES: usize = 1;
34pub const MAX_RUNTIME_MODULE_CACHE_ENTRIES: usize = 1024;
35pub const MIN_RUNTIME_MODULE_CACHE_SOURCE_BYTES: usize = 1;
36pub const MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES: usize = 128 * 1024 * 1024;
37pub const FRAME_TYPE_HELLO: &str = "hello";
38pub const FRAME_TYPE_HELLO_ACK: &str = "hello_ack";
39pub const FRAME_TYPE_HEARTBEAT: &str = "heartbeat";
40pub const FRAME_TYPE_INVOKE_WORKER: &str = "invoke_worker";
41pub const FRAME_TYPE_INVOKE_WORKER_RESULT: &str = "invoke_worker_result";
42pub const FRAME_TYPE_CANCEL_INVOKE: &str = "cancel_invoke";
43pub const FRAME_TYPE_CANCEL_INVOKE_ACK: &str = "cancel_invoke_ack";
44pub const FRAME_TYPE_COMPILE_FLIGHT_REGISTER: &str = "compile_flight_register";
45pub const FRAME_TYPE_COMPILE_FLIGHT_COMPLETE: &str = "compile_flight_complete";
46pub const FRAME_TYPE_OPEN_HANDLE: &str = "open_handle";
47pub const FRAME_TYPE_VALIDATE_HANDLE_GRANT: &str = "validate_handle_grant";
48pub const FRAME_TYPE_STORAGE_FILE: &str = "storage_file";
49pub const FRAME_TYPE_STORAGE_KV: &str = "storage_kv";
50pub const FRAME_TYPE_STORAGE_SQLITE: &str = "storage_sqlite";
51pub const FRAME_TYPE_NETWORK_GRANT: &str = "network_grant";
52pub const FRAME_TYPE_NETWORK_EXECUTE: &str = "network_execute";
53pub const FRAME_TYPE_REVOKE_EPOCH: &str = "revoke_epoch";
54pub const FRAME_TYPE_REVOKE_EPOCH_ACK: &str = "revoke_epoch_ack";
55pub const FRAME_TYPE_SESSION_REVOKE: &str = "session_revoke";
56pub const FRAME_TYPE_SESSION_REVOKE_ACK: &str = "session_revoke_ack";
57pub const ERR_ARTIFACT_HANDLE_FAILED: &str = "ARTIFACT_HANDLE_FAILED";
58pub const ERR_HANDLE_GRANT_VALIDATION_FAILED: &str = "HANDLE_GRANT_VALIDATION_FAILED";
59pub const ERR_STORAGE_FILE_FAILED: &str = "STORAGE_FILE_FAILED";
60pub const ERR_STORAGE_KV_FAILED: &str = "STORAGE_KV_FAILED";
61pub const ERR_STORAGE_SQLITE_FAILED: &str = "STORAGE_SQLITE_FAILED";
62pub const ERR_NETWORK_GRANT_FAILED: &str = "NETWORK_GRANT_FAILED";
63pub const ERR_NETWORK_EXECUTE_FAILED: &str = "NETWORK_EXECUTE_FAILED";
64pub const ERR_NETWORK_STREAM_STORE_UNAVAILABLE: &str = "NETWORK_STREAM_STORE_UNAVAILABLE";
65pub const ERR_NETWORK_STREAM_FAILED: &str = "NETWORK_STREAM_FAILED";
66pub const ERR_NETWORK_STREAM_BACKPRESSURE: &str = "NETWORK_STREAM_BACKPRESSURE";
67pub const ERR_NETWORK_STREAM_INVALID: &str = "NETWORK_STREAM_INVALID";
68pub const ERR_NETWORK_STREAM_NOT_FOUND: &str = "NETWORK_STREAM_NOT_FOUND";
69pub const ERR_NETWORK_STREAM_CLOSED: &str = "NETWORK_STREAM_CLOSED";
70pub const ERR_WORKER_INVOCATION_INVALID: &str = "WORKER_INVOCATION_INVALID";
71pub const ERR_RUNTIME_CAPABILITY_REVOKED: &str = "RUNTIME_CAPABILITY_REVOKED";
72pub const ERR_RUNTIME_CONTROL_CHANNEL_STALE: &str = "RUNTIME_CONTROL_CHANNEL_STALE";
73pub const ERR_RUNTIME_LEASE_INVALID: &str = "RUNTIME_LEASE_INVALID";
74pub const ERR_RUNTIME_LEASE_SIGNATURE_INVALID: &str = "RUNTIME_LEASE_SIGNATURE_INVALID";
75pub const ERR_LEASE_REPLAYED: &str = "PLUGIN_LEASE_REPLAYED";
76pub const ERR_WASM_WORKER_INVALID: &str = "WASM_WORKER_INVALID";
77pub const ERR_WASM_WORKER_FAILED: &str = "WASM_WORKER_FAILED";
78pub const ERR_WASM_HOSTCALL_FAILED: &str = "WASM_HOSTCALL_FAILED";
79pub const ERR_RUNTIME_CAPACITY_EXCEEDED: &str = "RUNTIME_CAPACITY_EXCEEDED";
80pub const ERR_RUNTIME_INVOCATION_CANCELED: &str = "RUNTIME_INVOCATION_CANCELED";
81pub const ERR_SESSION_REVOKED: &str = "PLUGIN_SESSION_REVOKED";
82pub const ERR_SESSION_REVOKE_SEQUENCE_STALE: &str = "SESSION_REVOKE_SEQUENCE_STALE";
83pub const ERR_SESSION_REVOKE_DRAIN_TIMEOUT: &str = "SESSION_REVOKE_DRAIN_TIMEOUT";
84pub const ERR_UNSUPPORTED_FRAME: &str = "UNSUPPORTED_FRAME";
85pub const ERROR_ORIGIN_RUNTIME: &str = "runtime";
86pub const ERROR_ORIGIN_HOSTCALL: &str = "hostcall";
87pub const ERROR_ORIGIN_PLUGIN: &str = "plugin";
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90#[non_exhaustive]
91pub enum IpcError {
92 DecodeFailed { context: &'static str },
93 EncodeFailed { context: &'static str },
94 MissingField { field: &'static str },
95 InvalidField { field: &'static str },
96 ProtocolViolation { message: &'static str },
97 CapacityOverflow { capacity: &'static str },
98 RemoteFailure { code: String },
99 InvalidResponseResultJson,
100 EmptyResponseErrorCode,
101 EmptyResponseErrorMessage,
102}
103
104pub type IpcResult<T> = Result<T, IpcError>;
105
106impl std::fmt::Display for IpcError {
107 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Self::DecodeFailed { context } => write!(formatter, "failed to decode {context}"),
110 Self::EncodeFailed { context } => write!(formatter, "failed to encode {context}"),
111 Self::MissingField { field } => write!(formatter, "missing {field}"),
112 Self::InvalidField { field } => write!(formatter, "invalid {field}"),
113 Self::ProtocolViolation { message } => formatter.write_str(message),
114 Self::CapacityOverflow { capacity } => write!(formatter, "{capacity} overflows usize"),
115 Self::RemoteFailure { code } => {
116 write!(formatter, "hostcall response failed with code {code}")
117 }
118 Self::InvalidResponseResultJson => {
119 formatter.write_str("runtime response result must be valid JSON")
120 }
121 Self::EmptyResponseErrorCode => {
122 formatter.write_str("runtime response code is required")
123 }
124 Self::EmptyResponseErrorMessage => {
125 formatter.write_str("runtime response message is required")
126 }
127 }
128 }
129}
130
131impl std::error::Error for IpcError {}
132
133fn decode_failed(context: &'static str) -> IpcError {
134 IpcError::DecodeFailed { context }
135}
136
137fn encode_failed(context: &'static str) -> IpcError {
138 IpcError::EncodeFailed { context }
139}
140
141fn missing_field(field: &'static str) -> IpcError {
142 IpcError::MissingField { field }
143}
144
145fn invalid_field(field: &'static str) -> IpcError {
146 IpcError::InvalidField { field }
147}
148
149fn protocol_violation(message: &'static str) -> IpcError {
150 IpcError::ProtocolViolation { message }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub enum FrameType {
155 Hello,
156 HelloAck,
157 Heartbeat,
158 InvokeWorker,
159 InvokeWorkerResult,
160 CompileFlightRegister,
161 CompileFlightComplete,
162 OpenHandle,
163 ValidateHandleGrant,
164 StorageFile,
165 StorageKV,
166 StorageSQLite,
167 NetworkGrant,
168 NetworkExecute,
169 RevokeEpoch,
170 RevokeEpochAck,
171 SessionRevoke,
172 SessionRevokeAck,
173 Diagnostic,
174}
175
176#[cfg(test)]
177mod property_gates {
178 use super::*;
179 use proptest::prelude::*;
180
181 proptest! {
182 #[test]
183 fn ipc_frame_parser_is_total(input in any::<String>()) {
184 let parsed = std::panic::catch_unwind(|| {
185 let _ = decode_runtime_input_frame(&input);
186 let _ = parse_frame_identity(&input);
187 let _ = parse_hello_frame(&input);
188 let _ = validate_hello_frame(&input);
189 let _ = parse_worker_invocation(&input);
190 let _ = parse_worker_invocation_context(&input);
191 let _ = parse_worker_invocation_identity(&input);
192 let _ = parse_worker_lease_replay_key(&input);
193 let _ = parse_worker_response_v2(&input);
194 let _ = parse_heartbeat_request(&input);
195 let _ = parse_revoke_epoch_request(&input);
196 let _ = parse_session_revoke_request(&input);
197 let _ = parse_cancel_invoke(&input);
198 let _ = parse_runtime_lease_public_keys(&input);
199 let _ = bind_parent_request_id(&input, "parent_request");
200 });
201 prop_assert!(parsed.is_ok());
202 }
203
204 #[test]
205 fn response_frame_builders_are_total(
206 frame_type in any::<String>(),
207 request_id in any::<String>(),
208 runtime_generation_id in any::<String>(),
209 result_json in any::<String>(),
210 code in any::<String>(),
211 message in any::<String>(),
212 ) {
213 let success = std::panic::catch_unwind(|| {
214 success_response_frame(
215 &frame_type,
216 &request_id,
217 &runtime_generation_id,
218 &result_json,
219 )
220 });
221 prop_assert!(success.is_ok());
222 let error = std::panic::catch_unwind(|| ResponseError::runtime(&code, &message));
223 prop_assert!(error.is_ok());
224 if let Ok(Ok(error)) = error {
225 let frame = std::panic::catch_unwind(|| {
226 error_response_frame(
227 &frame_type,
228 &request_id,
229 &runtime_generation_id,
230 error,
231 )
232 });
233 prop_assert!(frame.is_ok());
234 }
235 }
236
237 #[test]
238 fn session_revoke_ack_builder_is_total(
239 request_id in any::<String>(),
240 runtime_generation_id in any::<String>(),
241 sequence in any::<u64>(),
242 queued_invocations in any::<u64>(),
243 running_invocations in any::<u64>(),
244 storage_hostcalls in any::<u64>(),
245 active_network_requests in any::<u64>(),
246 sockets in any::<u64>(),
247 network_streams in any::<u64>(),
248 ) {
249 let built = std::panic::catch_unwind(|| {
250 session_revoke_ack_frame(
251 &request_id,
252 &runtime_generation_id,
253 sequence,
254 SessionRevokeState::Complete,
255 SessionRevokeAckCounts {
256 queued_invocations,
257 running_invocations,
258 storage_hostcalls,
259 active_network_requests,
260 sockets,
261 network_streams,
262 },
263 )
264 });
265 prop_assert!(built.is_ok());
266 }
267
268 #[test]
269 fn network_frame_builders_are_total(
270 request_id in any::<String>(),
271 runtime_generation_id in any::<String>(),
272 scope_kind in any::<String>(),
273 owner_env_hash in any::<String>(),
274 owner_user_hash in any::<String>(),
275 query_json in any::<String>(),
276 headers_json in any::<String>(),
277 ) {
278 let resource_scope = NetworkResourceScope {
279 kind: scope_kind,
280 owner_env_hash,
281 owner_user_hash,
282 };
283 let grant = NetworkGrantRequest {
284 plugin_instance_id: "plugini_1".to_string(),
285 active_fingerprint: "sha256:active".to_string(),
286 resource_scope: resource_scope.clone(),
287 runtime_instance_id: "runtime_1".to_string(),
288 runtime_generation_id: runtime_generation_id.clone(),
289 runtime_shard_id: "runtime_shard_1".to_string(),
290 policy_revision: 1,
291 management_revision: 1,
292 revoke_epoch: 1,
293 connector_id: "api".to_string(),
294 transport: "http".to_string(),
295 destination: "https://api.example.com".to_string(),
296 ttl_ms: 1,
297 };
298 let grant_result = std::panic::catch_unwind(|| {
299 network_grant_frame(&request_id, &runtime_generation_id, &grant)
300 });
301 prop_assert!(grant_result.is_ok());
302
303 let execute = NetworkExecuteRequest {
304 plugin_id: "com.example.worker".to_string(),
305 plugin_instance_id: "plugini_1".to_string(),
306 active_fingerprint: "sha256:active".to_string(),
307 resource_scope,
308 runtime_instance_id: "runtime_1".to_string(),
309 runtime_generation_id: runtime_generation_id.clone(),
310 runtime_shard_id: "runtime_shard_1".to_string(),
311 policy_revision: 1,
312 management_revision: 1,
313 revoke_epoch: 1,
314 connector_id: "api".to_string(),
315 transport: "http".to_string(),
316 destination: "https://api.example.com".to_string(),
317 ttl_ms: 1,
318 operation: "http".to_string(),
319 method: "GET".to_string(),
320 path: "/".to_string(),
321 query_json,
322 headers_json,
323 message_type: String::new(),
324 body_base64: String::new(),
325 payload_base64: String::new(),
326 max_request_bytes: 1,
327 max_response_bytes: 1,
328 max_chunk_bytes: 1,
329 max_buffered_bytes: 1,
330 timeout_ms: 1,
331 stream_id: String::new(),
332 stream_method: String::new(),
333 stream_effect: String::new(),
334 stream_execution: String::new(),
335 surface_instance_id: String::new(),
336 owner_session_hash: String::new(),
337 owner_user_hash: String::new(),
338 owner_env_hash: String::new(),
339 session_channel_id_hash: String::new(),
340 bridge_channel_id: String::new(),
341 content_type: String::new(),
342 };
343 let execute_result = std::panic::catch_unwind(|| {
344 network_execute_frame(&request_id, &runtime_generation_id, &execute)
345 });
346 prop_assert!(execute_result.is_ok());
347 }
348
349 #[test]
350 fn runtime_limits_keep_derived_capacities_bounded(
351 worker_count in MIN_RUNTIME_WORKER_COUNT..=MAX_RUNTIME_WORKER_COUNT,
352 queue_capacity in MIN_RUNTIME_QUEUE_CAPACITY..=MAX_RUNTIME_QUEUE_CAPACITY,
353 per_plugin_concurrency in MIN_RUNTIME_PER_PLUGIN_CONCURRENCY..=MAX_RUNTIME_PER_PLUGIN_CONCURRENCY,
354 module_cache_entries in MIN_RUNTIME_MODULE_CACHE_ENTRIES..=MAX_RUNTIME_MODULE_CACHE_ENTRIES,
355 module_cache_source_bytes in MIN_RUNTIME_MODULE_CACHE_SOURCE_BYTES..=MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES,
356 ) {
357 let limits = RuntimeLimits {
358 worker_count,
359 queue_capacity,
360 per_plugin_concurrency,
361 module_cache_entries,
362 module_cache_source_bytes,
363 };
364 match limits.validate() {
365 Ok(validated) => {
366 prop_assert!(per_plugin_concurrency <= worker_count);
367 prop_assert_eq!(validated.hostcall_active_route_capacity(), worker_count);
368 prop_assert_eq!(
369 validated.hostcall_canceled_route_capacity().unwrap(),
370 worker_count + queue_capacity,
371 );
372 prop_assert_eq!(validated.compile_flight_route_capacity(), worker_count);
373 }
374 Err(_) => prop_assert!(per_plugin_concurrency > worker_count),
375 }
376 }
377
378 #[test]
379 fn lease_signature_payload_is_stable_for_valid_fields(
380 lease_id in "[a-z][a-z0-9_]{0,24}",
381 token_id in "[a-z][a-z0-9_]{0,24}",
382 nonce in prop::collection::vec(any::<u8>(), 16..=32),
383 method in "worker\\.[a-z][a-z0-9_]{0,16}",
384 ) {
385 let nonce = nonce.into_iter().map(|byte| format!("{byte:02x}")).collect::<String>();
386 let fixture: serde_json::Value = serde_json::from_str(include_str!(
387 "../testdata/runtime-lease-signature-v1.json"
388 ))
389 .unwrap();
390 let mut lease = fixture.get("lease").cloned().unwrap();
391 lease["lease_id"] = serde_json::Value::String(lease_id);
392 lease["token_id"] = serde_json::Value::String(token_id);
393 lease["lease_nonce"] = serde_json::Value::String(nonce);
394 lease["method"] = serde_json::Value::String(method.clone());
395 let typed: WorkerLeasePayload = serde_json::from_value(lease).unwrap();
396 let first = runtime_lease_signature_payload_json(&typed, method.as_str()).unwrap();
397 let second = runtime_lease_signature_payload_json(&typed, method.as_str()).unwrap();
398 prop_assert_eq!(&first, &second);
399 let parsed: serde_json::Value = serde_json::from_str(&first).unwrap();
400 prop_assert!(parsed.is_object());
401 prop_assert!(parsed.get("signature").is_none());
402 }
403 }
404}
405
406#[derive(Deserialize)]
407#[serde(deny_unknown_fields)]
408struct RawIPCFrame {
409 ipc_version: String,
410 frame_type: String,
411 request_id: String,
412 parent_request_id: Option<String>,
413 runtime_generation_id: Option<String>,
414 payload: Box<serde_json::value::RawValue>,
415}
416
417#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
418#[serde(deny_unknown_fields)]
419pub struct RuntimeLimits {
420 pub worker_count: usize,
421 pub queue_capacity: usize,
422 pub per_plugin_concurrency: usize,
423 pub module_cache_entries: usize,
424 pub module_cache_source_bytes: usize,
425}
426
427#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
428#[serde(deny_unknown_fields)]
429pub struct ProcessContainmentEvidence {
430 pub schema_version: String,
431 pub profile: String,
432 pub seccomp_policy_sha256: String,
433 pub no_new_privs: bool,
434 pub seccomp_tsync: bool,
435 pub process_creation_denied: bool,
436 pub reexec_denied: bool,
437 pub active: bool,
438}
439
440impl ProcessContainmentEvidence {
441 pub fn validate(&self) -> IpcResult<()> {
442 if self.schema_version != "redevplugin.process_containment.v1"
443 || self.profile != "linux-runtime-v1"
444 || self.seccomp_policy_sha256.len() != 64
445 || !self
446 .seccomp_policy_sha256
447 .bytes()
448 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
449 || !self.no_new_privs
450 || !self.seccomp_tsync
451 || !self.process_creation_denied
452 || !self.reexec_denied
453 || !self.active
454 {
455 return Err(invalid_field("process_containment"));
456 }
457 Ok(())
458 }
459}
460
461impl RuntimeLimits {
462 pub fn validate(self) -> IpcResult<Self> {
463 if self.worker_count < MIN_RUNTIME_WORKER_COUNT
464 || self.queue_capacity < MIN_RUNTIME_QUEUE_CAPACITY
465 || self.per_plugin_concurrency < MIN_RUNTIME_PER_PLUGIN_CONCURRENCY
466 || self.module_cache_entries < MIN_RUNTIME_MODULE_CACHE_ENTRIES
467 || self.module_cache_source_bytes < MIN_RUNTIME_MODULE_CACHE_SOURCE_BYTES
468 {
469 return Err(protocol_violation(
470 "runtime limits are below platform minimums",
471 ));
472 }
473 if self.worker_count > MAX_RUNTIME_WORKER_COUNT
474 || self.queue_capacity > MAX_RUNTIME_QUEUE_CAPACITY
475 || self.per_plugin_concurrency > MAX_RUNTIME_PER_PLUGIN_CONCURRENCY
476 || self.module_cache_entries > MAX_RUNTIME_MODULE_CACHE_ENTRIES
477 || self.module_cache_source_bytes > MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES
478 {
479 return Err(protocol_violation(
480 "runtime limits exceed platform maximums",
481 ));
482 }
483 if self.per_plugin_concurrency > self.worker_count {
484 return Err(protocol_violation(
485 "runtime per_plugin_concurrency exceeds worker_count",
486 ));
487 }
488 self.hostcall_canceled_route_capacity()?;
489 Ok(self)
490 }
491
492 pub fn hostcall_active_route_capacity(self) -> usize {
493 self.worker_count
494 }
495
496 pub fn hostcall_canceled_route_capacity(self) -> IpcResult<usize> {
497 self.worker_count
498 .checked_add(self.queue_capacity)
499 .ok_or(IpcError::CapacityOverflow {
500 capacity: "runtime hostcall canceled route capacity",
501 })
502 }
503
504 pub fn compile_flight_route_capacity(self) -> usize {
505 self.worker_count
506 }
507}
508
509#[derive(Deserialize)]
510#[serde(deny_unknown_fields)]
511struct HelloPayload {
512 target: String,
513 host_process_id: u64,
514 host_ipc_version: String,
515 host_wasm_abi: String,
516 contract_set_sha256: String,
517 started_unix_nano: u64,
518 channel_nonce: String,
519 runtime_lease_public_keys: Vec<RuntimeLeasePublicKeyPayload>,
520 limits: RuntimeLimits,
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub enum RuntimeTarget {
525 LinuxAmd64,
526 LinuxArm64,
527}
528
529impl RuntimeTarget {
530 pub fn parse(value: &str) -> IpcResult<Self> {
531 match value {
532 "linux/amd64" => Ok(Self::LinuxAmd64),
533 "linux/arm64" => Ok(Self::LinuxArm64),
534 _ => Err(protocol_violation("unsupported runtime target")),
535 }
536 }
537
538 pub fn as_str(&self) -> &str {
539 match self {
540 Self::LinuxAmd64 => "linux/amd64",
541 Self::LinuxArm64 => "linux/arm64",
542 }
543 }
544}
545
546#[derive(Deserialize)]
547#[serde(deny_unknown_fields)]
548struct RuntimeLeasePublicKeyPayload {
549 algorithm: String,
550 key_id: String,
551 public_key_base64: String,
552}
553
554#[derive(Deserialize, Serialize)]
555#[serde(deny_unknown_fields)]
556struct WorkerFramePayload {
557 lease: WorkerLeasePayload,
558 method: String,
559 invocation: WorkerInvocationPayload,
560}
561
562#[derive(Clone, Deserialize, Serialize)]
563#[serde(deny_unknown_fields)]
564#[allow(dead_code)]
565struct WorkerLeasePayload {
566 lease_id: Option<String>,
567 token_id: Option<String>,
568 lease_nonce: Option<String>,
569 plugin_id: Option<String>,
570 plugin_version: Option<String>,
571 active_fingerprint: Option<String>,
572 surface_instance_id: Option<String>,
573 owner_session_hash: Option<String>,
574 owner_user_hash: Option<String>,
575 owner_env_hash: Option<String>,
576 session_channel_id_hash: Option<String>,
577 bridge_channel_id: Option<String>,
578 runtime_generation_id: Option<String>,
579 plugin_instance_id: Option<String>,
580 method: Option<String>,
581 effect: Option<String>,
582 execution: Option<String>,
583 operation_id: Option<String>,
584 stream_id: Option<String>,
585 audit_correlation_id: Option<String>,
586 target_descriptor_hashes: Option<Vec<String>>,
587 limits: Option<WorkerLeaseLimitsPayload>,
588 policy_revision: Option<u64>,
589 management_revision: Option<u64>,
590 revoke_epoch: Option<u64>,
591 runtime_shard_id: Option<String>,
592 runtime_instance_id: Option<String>,
593 ipc_channel_id: Option<String>,
594 connection_nonce: Option<String>,
595 key_id: Option<String>,
596 signature: Option<String>,
597 issued_at_unix_ms: Option<i64>,
598 expires_at_unix_ms: Option<i64>,
599}
600
601#[derive(Clone, Deserialize, Serialize)]
602#[serde(deny_unknown_fields)]
603#[allow(dead_code)]
604struct WorkerLeaseLimitsPayload {
605 timeout_ms: Option<i64>,
606 memory_bytes: Option<u64>,
607 max_payload_bytes: Option<i64>,
608 max_stream_bytes_per_sec: Option<i64>,
609}
610
611#[derive(Clone, Deserialize, Serialize)]
612#[serde(deny_unknown_fields)]
613#[allow(dead_code)]
614struct WorkerInvocationPayload {
615 plugin_id: Option<String>,
616 plugin_instance_id: Option<String>,
617 active_fingerprint: Option<String>,
618 runtime_instance_id: Option<String>,
619 runtime_generation_id: Option<String>,
620 package_hash: Option<String>,
621 worker_id: Option<String>,
622 worker_mode: Option<String>,
623 worker_scope: Option<String>,
624 artifact: Option<String>,
625 artifact_sha256: Option<String>,
626 abi: Option<String>,
627 method: Option<String>,
628 effect: Option<String>,
629 execution: Option<String>,
630 surface_instance_id: Option<String>,
631 owner_session_hash: Option<String>,
632 owner_user_hash: Option<String>,
633 owner_env_hash: Option<String>,
634 session_channel_id_hash: Option<String>,
635 bridge_channel_id: Option<String>,
636 operation_id: Option<String>,
637 stream_id: Option<String>,
638 audit_correlation_id: Option<String>,
639 policy_revision: Option<u64>,
640 management_revision: Option<u64>,
641 revoke_epoch: Option<u64>,
642 params_sha256: Option<String>,
643 params: Option<serde_json::Map<String, serde_json::Value>>,
644 storage_handle_grants: Option<HashMap<String, String>>,
645 broker_access: Option<WorkerBrokerAccessPayload>,
646 broker_access_sha256: Option<String>,
647}
648
649#[derive(Clone, Deserialize, Serialize)]
650#[serde(deny_unknown_fields)]
651struct WorkerBrokerAccessPayload {
652 #[serde(default, skip_serializing_if = "Vec::is_empty")]
653 storage: Vec<WorkerStorageBrokerAccessPayload>,
654 #[serde(default, skip_serializing_if = "Vec::is_empty")]
655 network: Vec<WorkerNetworkBrokerAccessPayload>,
656}
657
658#[derive(Clone, Deserialize, Serialize)]
659#[serde(deny_unknown_fields)]
660struct WorkerStorageBrokerAccessPayload {
661 store_id: String,
662 scope: String,
663 operations: Vec<String>,
664}
665
666#[derive(Clone, Deserialize, Serialize)]
667#[serde(deny_unknown_fields)]
668struct WorkerNetworkBrokerAccessPayload {
669 connector_id: String,
670 transport: String,
671 scope: String,
672 operations: Vec<String>,
673 #[serde(default, skip_serializing_if = "Vec::is_empty")]
674 http_methods: Vec<String>,
675}
676
677struct ClosedWorkerFrame {
678 request_id: String,
679 runtime_generation_id: String,
680 method: String,
681 lease: WorkerLeasePayload,
682 invocation: WorkerInvocationPayload,
683}
684
685#[derive(Debug, Clone, PartialEq, Eq)]
686pub struct FrameIdentity {
687 pub frame_type: String,
688 pub request_id: String,
689 pub parent_request_id: Option<String>,
690 pub runtime_generation_id: String,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq)]
694pub struct HelloFrame {
695 pub request_id: String,
696 pub runtime_generation_id: String,
697 pub target: RuntimeTarget,
698 pub contract_set_sha256: String,
699 pub channel_nonce: String,
700 pub runtime_lease_public_keys: Vec<RuntimeLeasePublicKey>,
701 pub limits: RuntimeLimits,
702}
703
704pub struct ParsedWorkerInvocation {
705 request_id: String,
706 runtime_generation_id: String,
707 method: String,
708 lease: WorkerLeasePayload,
709 invocation: WorkerInvocationPayload,
710 params_json: Option<String>,
711 broker_access_json: Option<String>,
712 context: OnceLock<IpcResult<WorkerInvocationContext>>,
713 identity: OnceLock<IpcResult<WorkerInvocationIdentity>>,
714 target_hash: OnceLock<IpcResult<String>>,
715}
716
717pub struct WorkerInvocationInput {
718 pub identity: FrameIdentity,
719 pub invocation: IpcResult<ParsedWorkerInvocation>,
720}
721
722pub struct CancelInvocationInput {
723 pub identity: FrameIdentity,
724 pub invocation_request_id: String,
725}
726
727pub struct RuntimeHostcallResponseInput {
728 pub identity: FrameIdentity,
729 pub raw_frame: String,
730}
731
732pub enum RuntimeInputFrame {
733 InvokeWorker(Box<WorkerInvocationInput>),
734 CancelInvoke(CancelInvocationInput),
735 HostcallResponse(RuntimeHostcallResponseInput),
736 Unsupported(FrameIdentity),
737}
738
739#[derive(Debug, Clone, PartialEq, Eq)]
740pub struct WorkerInvocationContext {
741 pub plugin_id: String,
742 pub plugin_instance_id: String,
743 pub active_fingerprint: String,
744 pub runtime_instance_id: String,
745 pub runtime_generation_id: String,
746 pub runtime_shard_id: String,
747 pub method: String,
748 pub effect: String,
749 pub execution: String,
750 pub surface_instance_id: String,
751 pub owner_session_hash: String,
752 pub owner_user_hash: String,
753 pub owner_env_hash: String,
754 pub session_channel_id_hash: String,
755 pub bridge_channel_id: String,
756 pub operation_id: String,
757 pub stream_id: String,
758 pub policy_revision: u64,
759 pub management_revision: u64,
760 pub revoke_epoch: u64,
761 pub storage_handle_grants: HashMap<String, String>,
762 pub broker_access_json: String,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq)]
766pub struct HeartbeatRequest {
767 pub sent_unix_nano: u64,
768 pub max_staleness_ms: u64,
769}
770
771#[derive(Debug, Clone, PartialEq, Eq)]
772pub struct RevokeEpochRequest {
773 pub resource_scope: NetworkResourceScope,
774 pub plugin_instance_id: String,
775 pub revoke_epoch: u64,
776}
777
778#[derive(Debug, Clone, PartialEq, Eq, Hash)]
779pub struct SessionScope {
780 pub owner_session_hash: String,
781 pub owner_user_hash: String,
782 pub owner_env_hash: String,
783 pub session_channel_id_hash: String,
784}
785
786impl SessionScope {
787 pub fn new(
788 owner_session_hash: impl Into<String>,
789 owner_user_hash: impl Into<String>,
790 owner_env_hash: impl Into<String>,
791 session_channel_id_hash: impl Into<String>,
792 ) -> IpcResult<Self> {
793 let scope = Self {
794 owner_session_hash: owner_session_hash.into(),
795 owner_user_hash: owner_user_hash.into(),
796 owner_env_hash: owner_env_hash.into(),
797 session_channel_id_hash: session_channel_id_hash.into(),
798 };
799 scope.validate()?;
800 Ok(scope)
801 }
802
803 fn validate(&self) -> IpcResult<()> {
804 for (value, field) in [
805 (&self.owner_session_hash, "owner_session_hash"),
806 (&self.owner_user_hash, "owner_user_hash"),
807 (&self.owner_env_hash, "owner_env_hash"),
808 (&self.session_channel_id_hash, "session_channel_id_hash"),
809 ] {
810 if value.is_empty() || value.trim() != value {
811 return Err(invalid_field(field));
812 }
813 }
814 Ok(())
815 }
816}
817
818#[derive(Debug, Clone, PartialEq, Eq)]
819pub struct SessionRevokeRequest {
820 pub request_id: String,
821 pub runtime_generation_id: String,
822 pub session_revoke_sequence: u64,
823 pub owner_session_hash: String,
824 pub owner_user_hash: String,
825 pub owner_env_hash: String,
826 pub session_channel_id_hash: String,
827}
828
829impl SessionRevokeRequest {
830 pub fn session_scope(&self) -> SessionScope {
831 SessionScope {
832 owner_session_hash: self.owner_session_hash.clone(),
833 owner_user_hash: self.owner_user_hash.clone(),
834 owner_env_hash: self.owner_env_hash.clone(),
835 session_channel_id_hash: self.session_channel_id_hash.clone(),
836 }
837 }
838}
839
840#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
841#[serde(rename_all = "snake_case")]
842pub enum SessionRevokeState {
843 Complete,
844}
845
846#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
847#[serde(deny_unknown_fields)]
848pub struct SessionRevokeAckCounts {
849 pub queued_invocations: u64,
850 pub running_invocations: u64,
851 pub storage_hostcalls: u64,
852 pub active_network_requests: u64,
853 pub sockets: u64,
854 pub network_streams: u64,
855}
856
857#[derive(Deserialize)]
858#[serde(deny_unknown_fields)]
859struct HeartbeatRequestPayload {
860 sent_unix_nano: u64,
861 max_staleness_ms: u64,
862}
863
864#[derive(Deserialize)]
865#[serde(deny_unknown_fields)]
866struct RevokeEpochRequestPayload {
867 resource_scope: NetworkResourceScope,
868 plugin_instance_id: String,
869 revoke_epoch: u64,
870}
871
872#[derive(Deserialize)]
873#[serde(deny_unknown_fields)]
874struct SessionRevokeRequestPayload {
875 session_revoke_sequence: u64,
876 owner_session_hash: String,
877 owner_user_hash: String,
878 owner_env_hash: String,
879 session_channel_id_hash: String,
880}
881
882fn parse_raw_frame(input: &str) -> IpcResult<RawIPCFrame> {
883 serde_json::from_str(input).map_err(|_| decode_failed("IPC frame"))
884}
885
886fn parse_hello_payload(frame: &RawIPCFrame) -> IpcResult<HelloPayload> {
887 serde_json::from_str(frame.payload.get()).map_err(|_| decode_failed("hello payload"))
888}
889
890fn parse_closed_worker_frame(
891 identity: &FrameIdentity,
892 payload: &serde_json::value::RawValue,
893) -> IpcResult<ClosedWorkerFrame> {
894 if identity.parent_request_id.is_some() {
895 return Err(protocol_violation(
896 "invoke_worker must not have parent_request_id",
897 ));
898 }
899 let payload: WorkerFramePayload =
900 serde_json::from_str(payload.get()).map_err(|_| decode_failed("worker frame payload"))?;
901 if payload.method.trim().is_empty() {
902 return Err(invalid_field("worker frame method"));
903 }
904 if payload
905 .invocation
906 .method
907 .as_deref()
908 .is_some_and(|method| method.trim() != payload.method.trim())
909 {
910 return Err(protocol_violation(
911 "worker invocation method does not match the frame envelope",
912 ));
913 }
914 Ok(ClosedWorkerFrame {
915 request_id: identity.request_id.clone(),
916 runtime_generation_id: identity.runtime_generation_id.clone(),
917 method: payload.method,
918 lease: payload.lease,
919 invocation: payload.invocation,
920 })
921}
922
923fn parsed_worker_invocation(
924 identity: &FrameIdentity,
925 payload: &serde_json::value::RawValue,
926) -> IpcResult<ParsedWorkerInvocation> {
927 let parsed = parse_closed_worker_frame(identity, payload)?;
928 let params_json = parsed
929 .invocation
930 .params
931 .as_ref()
932 .map(encode_worker_canonical_json)
933 .transpose()
934 .map_err(|_| encode_failed("parsed worker params"))?;
935 let broker_access_json = parsed
936 .invocation
937 .broker_access
938 .as_ref()
939 .map(encode_worker_canonical_json)
940 .transpose()
941 .map_err(|_| encode_failed("parsed worker broker access"))?;
942 Ok(ParsedWorkerInvocation {
943 request_id: parsed.request_id,
944 runtime_generation_id: parsed.runtime_generation_id,
945 method: parsed.method,
946 lease: parsed.lease,
947 invocation: parsed.invocation,
948 params_json,
949 broker_access_json,
950 context: OnceLock::new(),
951 identity: OnceLock::new(),
952 target_hash: OnceLock::new(),
953 })
954}
955
956pub fn parse_worker_invocation(input: &str) -> IpcResult<ParsedWorkerInvocation> {
957 match decode_runtime_input_frame(input)? {
958 RuntimeInputFrame::InvokeWorker(worker) => worker.invocation,
959 _ => Err(protocol_violation("expected invoke_worker frame")),
960 }
961}
962
963fn encode_worker_canonical_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
964 let encoded = serde_json::to_string(value)?;
965 if !encoded.contains(['\u{2028}', '\u{2029}']) {
966 return Ok(encoded);
967 }
968 let mut canonical = String::with_capacity(encoded.len());
969 for character in encoded.chars() {
970 match character {
971 '\u{2028}' => canonical.push_str("\\u2028"),
972 '\u{2029}' => canonical.push_str("\\u2029"),
973 _ => canonical.push(character),
974 }
975 }
976 Ok(canonical)
977}
978
979fn required_string(value: &Option<String>, field: &'static str) -> IpcResult<String> {
980 value
981 .as_deref()
982 .map(str::trim)
983 .filter(|value| !value.is_empty())
984 .map(str::to_string)
985 .ok_or_else(|| missing_field(field))
986}
987
988impl ParsedWorkerInvocation {
989 pub fn request_id(&self) -> &str {
990 &self.request_id
991 }
992
993 pub fn runtime_generation_id(&self) -> &str {
994 &self.runtime_generation_id
995 }
996
997 pub fn plugin_instance_id(&self) -> IpcResult<&str> {
998 self.invocation
999 .plugin_instance_id
1000 .as_deref()
1001 .map(str::trim)
1002 .filter(|value| !value.is_empty())
1003 .ok_or_else(|| missing_field("plugin_instance_id"))
1004 }
1005
1006 pub fn session_scope(&self) -> IpcResult<Option<SessionScope>> {
1010 let invocation = &self.invocation;
1011 let session_present = invocation
1012 .owner_session_hash
1013 .as_deref()
1014 .is_some_and(|value| !value.is_empty())
1015 || invocation
1016 .session_channel_id_hash
1017 .as_deref()
1018 .is_some_and(|value| !value.is_empty());
1019 if !session_present {
1020 return Ok(None);
1021 }
1022 SessionScope::new(
1023 required_string(&invocation.owner_session_hash, "owner_session_hash")?,
1024 required_string(&invocation.owner_user_hash, "owner_user_hash")?,
1025 required_string(&invocation.owner_env_hash, "owner_env_hash")?,
1026 required_string(
1027 &invocation.session_channel_id_hash,
1028 "session_channel_id_hash",
1029 )?,
1030 )
1031 .map(Some)
1032 }
1033
1034 pub fn context(&self) -> IpcResult<WorkerInvocationContext> {
1035 self.context.get_or_init(|| self.build_context()).clone()
1036 }
1037
1038 fn build_context(&self) -> IpcResult<WorkerInvocationContext> {
1039 let invocation = &self.invocation;
1040 Ok(WorkerInvocationContext {
1041 plugin_id: required_string(&invocation.plugin_id, "plugin_id")?,
1042 plugin_instance_id: required_string(
1043 &invocation.plugin_instance_id,
1044 "plugin_instance_id",
1045 )?,
1046 active_fingerprint: required_string(
1047 &invocation.active_fingerprint,
1048 "active_fingerprint",
1049 )?,
1050 runtime_instance_id: required_string(
1051 &invocation.runtime_instance_id,
1052 "runtime_instance_id",
1053 )?,
1054 runtime_generation_id: required_string(
1055 &invocation.runtime_generation_id,
1056 "runtime_generation_id",
1057 )?,
1058 runtime_shard_id: required_string(&self.lease.runtime_shard_id, "runtime_shard_id")?,
1059 method: required_string(&invocation.method, "method")?,
1060 effect: invocation.effect.clone().unwrap_or_default(),
1061 execution: invocation.execution.clone().unwrap_or_default(),
1062 surface_instance_id: invocation.surface_instance_id.clone().unwrap_or_default(),
1063 owner_session_hash: invocation.owner_session_hash.clone().unwrap_or_default(),
1064 owner_user_hash: invocation.owner_user_hash.clone().unwrap_or_default(),
1065 owner_env_hash: invocation.owner_env_hash.clone().unwrap_or_default(),
1066 session_channel_id_hash: invocation
1067 .session_channel_id_hash
1068 .clone()
1069 .unwrap_or_default(),
1070 bridge_channel_id: invocation.bridge_channel_id.clone().unwrap_or_default(),
1071 operation_id: invocation.operation_id.clone().unwrap_or_default(),
1072 stream_id: invocation.stream_id.clone().unwrap_or_default(),
1073 policy_revision: required_safe_u64(self.lease.policy_revision, "policy_revision")?,
1074 management_revision: required_safe_u64(
1075 self.lease.management_revision,
1076 "management_revision",
1077 )?,
1078 revoke_epoch: required_positive_u64(self.lease.revoke_epoch, "revoke_epoch")?,
1079 storage_handle_grants: invocation.storage_handle_grants.clone().unwrap_or_default(),
1080 broker_access_json: self
1081 .broker_access_json
1082 .clone()
1083 .unwrap_or_else(|| "{}".to_string()),
1084 })
1085 }
1086
1087 pub fn identity(&self) -> IpcResult<WorkerInvocationIdentity> {
1088 self.identity.get_or_init(|| self.build_identity()).clone()
1089 }
1090
1091 pub fn validate_worker_contract(&self) -> IpcResult<()> {
1092 if self.invocation.abi.as_deref() != Some(WASM_ABI_VERSION) {
1093 return Err(protocol_violation("worker invocation ABI is unsupported"));
1094 }
1095 if self.invocation.worker_mode.as_deref() != Some("job") {
1096 return Err(protocol_violation("worker invocation mode is unsupported"));
1097 }
1098 required_string(&self.invocation.worker_scope, "worker_scope")?;
1099 Ok(())
1100 }
1101
1102 fn build_identity(&self) -> IpcResult<WorkerInvocationIdentity> {
1103 let invocation = &self.invocation;
1104 let package_hash = invocation
1105 .package_hash
1106 .clone()
1107 .ok_or_else(|| missing_field("package_hash"))?;
1108 if !is_sha256_ref(&package_hash) {
1109 return Err(invalid_field("package_hash"));
1110 }
1111 let artifact = invocation
1112 .artifact
1113 .clone()
1114 .ok_or_else(|| missing_field("artifact"))?;
1115 if !is_worker_artifact_path(&artifact) {
1116 return Err(invalid_field("artifact"));
1117 }
1118 let artifact_sha256 = invocation
1119 .artifact_sha256
1120 .clone()
1121 .ok_or_else(|| missing_field("artifact_sha256"))?;
1122 if !is_sha256_ref(&artifact_sha256) {
1123 return Err(invalid_field("artifact_sha256"));
1124 }
1125 let worker_id = invocation
1126 .worker_id
1127 .clone()
1128 .ok_or_else(|| missing_field("worker_id"))?;
1129 if worker_id.trim().is_empty() {
1130 return Err(invalid_field("worker_id"));
1131 }
1132 let method = invocation
1133 .method
1134 .clone()
1135 .ok_or_else(|| missing_field("method"))?;
1136 if method.trim().is_empty() {
1137 return Err(invalid_field("method"));
1138 }
1139 Ok(WorkerInvocationIdentity {
1140 package_hash,
1141 artifact,
1142 artifact_sha256,
1143 worker_id,
1144 method,
1145 })
1146 }
1147
1148 pub fn worker_request_json_v2(&self) -> IpcResult<String> {
1149 let method = required_string(&self.invocation.method, "worker invocation method")?;
1150 let params = self
1151 .params_json
1152 .as_ref()
1153 .ok_or_else(|| missing_field("worker invocation params"))?;
1154 Ok(format!(
1155 "{{\"schema_version\":\"redevplugin.worker_request.v2\",\"method\":\"{}\",\"params\":{}}}",
1156 escape_json_string(&method),
1157 params
1158 ))
1159 }
1160
1161 pub fn memory_limit_bytes(&self) -> IpcResult<usize> {
1162 let memory_bytes = self
1163 .lease
1164 .limits
1165 .as_ref()
1166 .and_then(|limits| limits.memory_bytes)
1167 .filter(|value| *value > 0)
1168 .ok_or_else(|| invalid_field("runtime lease memory_bytes limit"))?;
1169 if memory_bytes > MAX_RUNTIME_LEASE_MEMORY_BYTES {
1170 return Err(protocol_violation(
1171 "runtime lease memory_bytes limit exceeds platform maximum",
1172 ));
1173 }
1174 usize::try_from(memory_bytes)
1175 .map_err(|_| protocol_violation("runtime lease memory_bytes limit exceeds runtime"))
1176 }
1177
1178 pub fn replay_key(&self) -> IpcResult<WorkerLeaseReplayKey> {
1179 let lease_id = self
1180 .lease
1181 .lease_id
1182 .clone()
1183 .ok_or_else(|| missing_field("lease_id"))?;
1184 if lease_id.trim().is_empty() {
1185 return Err(invalid_field("lease_id"));
1186 }
1187 let lease_nonce = self
1188 .lease
1189 .lease_nonce
1190 .clone()
1191 .ok_or_else(|| missing_field("lease_nonce"))?;
1192 if lease_nonce.trim().is_empty() {
1193 return Err(invalid_field("lease_nonce"));
1194 }
1195 let expires_at_unix_ms = self
1196 .lease
1197 .expires_at_unix_ms
1198 .filter(|value| *value > 0)
1199 .ok_or_else(|| invalid_field("expires_at_unix_ms"))?;
1200 Ok(WorkerLeaseReplayKey {
1201 lease_id,
1202 lease_nonce,
1203 expires_at_unix_ms,
1204 })
1205 }
1206
1207 pub fn storage_handle_grant(&self, store_id: &str) -> IpcResult<String> {
1208 let grants = self
1209 .invocation
1210 .storage_handle_grants
1211 .as_ref()
1212 .ok_or_else(|| missing_field("worker invocation storage_handle_grants"))?;
1213 grants
1214 .get(store_id)
1215 .map(String::as_str)
1216 .filter(|value| !value.trim().is_empty())
1217 .map(str::to_string)
1218 .ok_or_else(|| invalid_field("worker invocation storage grant"))
1219 }
1220
1221 pub fn validate_storage_broker_access(&self, store_id: &str, operation: &str) -> IpcResult<()> {
1222 let effect = required_string(&self.invocation.effect, "effect")?;
1223 if effect == "read" && !matches!(operation, "read" | "list" | "get" | "query") {
1224 return Err(protocol_violation(
1225 "worker method with read effect cannot perform the storage mutation",
1226 ));
1227 }
1228 if !self
1229 .invocation
1230 .broker_access
1231 .as_ref()
1232 .is_some_and(|access| {
1233 access.storage.iter().any(|entry| {
1234 entry.store_id == store_id
1235 && entry.operations.iter().any(|value| value == operation)
1236 })
1237 })
1238 {
1239 return Err(protocol_violation(
1240 "worker method is not allowed to perform the storage operation",
1241 ));
1242 }
1243 Ok(())
1244 }
1245
1246 pub fn storage_broker_scope(&self, store_id: &str) -> IpcResult<String> {
1247 let scope = self
1248 .invocation
1249 .broker_access
1250 .as_ref()
1251 .and_then(|access| {
1252 access
1253 .storage
1254 .iter()
1255 .find(|entry| entry.store_id == store_id)
1256 })
1257 .map(|entry| entry.scope.as_str())
1258 .filter(|scope| matches!(*scope, "user" | "environment"))
1259 .ok_or_else(|| invalid_field("worker invocation storage scope"))?;
1260 Ok(scope.to_string())
1261 }
1262
1263 pub fn validate_network_broker_access(
1264 &self,
1265 connector_id: &str,
1266 transport: &str,
1267 operation: &str,
1268 http_method: &str,
1269 ) -> IpcResult<()> {
1270 let allowed = self
1271 .invocation
1272 .broker_access
1273 .as_ref()
1274 .is_some_and(|access| {
1275 access.network.iter().any(|entry| {
1276 entry.connector_id == connector_id
1277 && entry.transport == transport
1278 && entry.operations.iter().any(|value| value == operation)
1279 && (transport != "http"
1280 || entry.http_methods.iter().any(|value| value == http_method))
1281 })
1282 });
1283 if !allowed {
1284 return Err(protocol_violation(
1285 "worker method is not allowed to perform the network operation",
1286 ));
1287 }
1288 Ok(())
1289 }
1290
1291 pub fn network_broker_scope(&self, connector_id: &str, transport: &str) -> IpcResult<String> {
1292 let scope = self
1293 .invocation
1294 .broker_access
1295 .as_ref()
1296 .and_then(|access| {
1297 access.network.iter().find(|entry| {
1298 entry.connector_id == connector_id && entry.transport == transport
1299 })
1300 })
1301 .map(|entry| entry.scope.trim())
1302 .filter(|scope| matches!(*scope, "user" | "environment"))
1303 .ok_or_else(|| invalid_field("worker invocation network scope"))?;
1304 Ok(scope.to_string())
1305 }
1306}
1307
1308pub fn parse_worker_invocation_context(input: &str) -> IpcResult<WorkerInvocationContext> {
1309 parse_worker_invocation(input)?.context()
1310}
1311
1312pub fn parse_heartbeat_request(input: &str) -> IpcResult<HeartbeatRequest> {
1313 let frame = parse_raw_frame(input)?;
1314 if frame.frame_type != FRAME_TYPE_HEARTBEAT {
1315 return Err(protocol_violation("expected heartbeat frame"));
1316 }
1317 let payload: HeartbeatRequestPayload = serde_json::from_str(frame.payload.get())
1318 .map_err(|_| decode_failed("heartbeat payload"))?;
1319 Ok(HeartbeatRequest {
1320 sent_unix_nano: payload.sent_unix_nano,
1321 max_staleness_ms: payload.max_staleness_ms,
1322 })
1323}
1324
1325pub fn parse_revoke_epoch_request(input: &str) -> IpcResult<RevokeEpochRequest> {
1326 let frame = parse_raw_frame(input)?;
1327 if frame.frame_type != FRAME_TYPE_REVOKE_EPOCH {
1328 return Err(protocol_violation("expected revoke_epoch frame"));
1329 }
1330 let payload: RevokeEpochRequestPayload = serde_json::from_str(frame.payload.get())
1331 .map_err(|_| decode_failed("revoke_epoch payload"))?;
1332 if payload.plugin_instance_id.trim().is_empty() {
1333 return Err(invalid_field("plugin_instance_id"));
1334 }
1335 if !payload.resource_scope.valid() || payload.resource_scope.kind != "environment" {
1336 return Err(invalid_field("revoke resource_scope"));
1337 }
1338 validate_revoke_epoch(payload.revoke_epoch)?;
1339 Ok(RevokeEpochRequest {
1340 resource_scope: payload.resource_scope,
1341 plugin_instance_id: payload.plugin_instance_id,
1342 revoke_epoch: payload.revoke_epoch,
1343 })
1344}
1345
1346pub fn parse_session_revoke_request(input: &str) -> IpcResult<SessionRevokeRequest> {
1347 let frame = parse_raw_frame(input)?;
1348 let identity = validated_frame_identity(&frame)?;
1349 if identity.frame_type != FRAME_TYPE_SESSION_REVOKE {
1350 return Err(protocol_violation("expected session_revoke frame"));
1351 }
1352 if identity.parent_request_id.is_some() {
1353 return Err(protocol_violation(
1354 "session_revoke must not have parent_request_id",
1355 ));
1356 }
1357 let payload: SessionRevokeRequestPayload = serde_json::from_str(frame.payload.get())
1358 .map_err(|_| decode_failed("session_revoke payload"))?;
1359 if payload.session_revoke_sequence == 0
1360 || payload.session_revoke_sequence > MAX_JSON_SAFE_INTEGER
1361 {
1362 return Err(invalid_field("session_revoke_sequence"));
1363 }
1364 let scope = SessionScope::new(
1365 payload.owner_session_hash,
1366 payload.owner_user_hash,
1367 payload.owner_env_hash,
1368 payload.session_channel_id_hash,
1369 )?;
1370 Ok(SessionRevokeRequest {
1371 request_id: identity.request_id,
1372 runtime_generation_id: identity.runtime_generation_id,
1373 session_revoke_sequence: payload.session_revoke_sequence,
1374 owner_session_hash: scope.owner_session_hash,
1375 owner_user_hash: scope.owner_user_hash,
1376 owner_env_hash: scope.owner_env_hash,
1377 session_channel_id_hash: scope.session_channel_id_hash,
1378 })
1379}
1380
1381pub fn escape_json_string(input: &str) -> String {
1382 let mut out = String::with_capacity(input.len());
1383 for ch in input.chars() {
1384 match ch {
1385 '"' => out.push_str("\\\""),
1386 '\\' => out.push_str("\\\\"),
1387 '\n' => out.push_str("\\n"),
1388 '\r' => out.push_str("\\r"),
1389 '\t' => out.push_str("\\t"),
1390 c if c.is_control() => out.push_str(&format!("\\u{:04x}", c as u32)),
1391 other => out.push(other),
1392 }
1393 }
1394 out
1395}
1396
1397pub fn bind_parent_request_id(frame: &str, parent_request_id: &str) -> IpcResult<String> {
1398 if parent_request_id.trim().is_empty() {
1399 return Err(invalid_field("parent_request_id"));
1400 }
1401 let mut value: serde_json::Value =
1402 serde_json::from_str(frame).map_err(|_| decode_failed("outbound IPC frame"))?;
1403 let object = value
1404 .as_object_mut()
1405 .ok_or_else(|| protocol_violation("outbound IPC frame must be an object"))?;
1406 object.insert(
1407 "parent_request_id".to_string(),
1408 serde_json::Value::String(parent_request_id.to_string()),
1409 );
1410 serde_json::to_string(&value).map_err(|_| encode_failed("outbound IPC frame"))
1411}
1412
1413#[derive(Debug, Clone, PartialEq, Eq)]
1414pub struct RuntimeLeasePublicKey {
1415 pub key_id: String,
1416 pub public_key: [u8; 32],
1417}
1418
1419fn parse_runtime_lease_public_key_payloads(
1420 keys: Vec<RuntimeLeasePublicKeyPayload>,
1421) -> IpcResult<Vec<RuntimeLeasePublicKey>> {
1422 let mut seen = HashSet::new();
1423 let mut parsed = Vec::with_capacity(keys.len());
1424 if keys.is_empty() {
1425 return Err(invalid_field("runtime_lease_public_keys"));
1426 }
1427 for key in keys {
1428 let key_id = key.key_id.trim().to_string();
1429 if key_id.is_empty() {
1430 return Err(invalid_field("runtime lease public key key_id"));
1431 }
1432 if !seen.insert(key_id.clone()) {
1433 return Err(protocol_violation(
1434 "runtime lease public key key_id is duplicated",
1435 ));
1436 }
1437 if key.algorithm != RUNTIME_LEASE_SIGNATURE_ALGORITHM {
1438 return Err(protocol_violation(
1439 "runtime lease public key algorithm is unsupported",
1440 ));
1441 }
1442 let decoded = base64::engine::general_purpose::STANDARD
1443 .decode(key.public_key_base64.as_bytes())
1444 .map_err(|_| invalid_field("runtime lease public key base64"))?;
1445 let public_key: [u8; 32] = decoded
1446 .try_into()
1447 .map_err(|_| invalid_field("runtime lease public key length"))?;
1448 parsed.push(RuntimeLeasePublicKey { key_id, public_key });
1449 }
1450 Ok(parsed)
1451}
1452
1453pub fn parse_runtime_lease_public_keys(input: &str) -> IpcResult<Vec<RuntimeLeasePublicKey>> {
1454 let frame = parse_raw_frame(input)?;
1455 let payload = parse_hello_payload(&frame)?;
1456 parse_runtime_lease_public_key_payloads(payload.runtime_lease_public_keys)
1457}
1458
1459pub fn verify_worker_runtime_lease_signature(
1460 input: &str,
1461 public_keys: &[RuntimeLeasePublicKey],
1462) -> IpcResult<()> {
1463 parse_worker_invocation(input)?.verify_runtime_lease_signature(public_keys)
1464}
1465
1466impl ParsedWorkerInvocation {
1467 pub fn verify_runtime_lease_signature(
1468 &self,
1469 public_keys: &[RuntimeLeasePublicKey],
1470 ) -> IpcResult<()> {
1471 if public_keys.is_empty() {
1472 return Err(missing_field("runtime lease public keys"));
1473 }
1474 let key_id = required_string(&self.lease.key_id, "key_id")?;
1475 let public_key = public_keys
1476 .iter()
1477 .find(|key| key.key_id == key_id)
1478 .ok_or_else(|| invalid_field("runtime lease signing key"))?;
1479 let verifying_key = VerifyingKey::from_bytes(&public_key.public_key)
1480 .map_err(|_| invalid_field("runtime lease public key"))?;
1481 let payload = runtime_lease_signature_payload_json(&self.lease, &self.method)?;
1482 let signature =
1483 decode_runtime_lease_signature(&required_string(&self.lease.signature, "signature")?)?;
1484 verifying_key
1485 .verify(payload.as_bytes(), &signature)
1486 .map_err(|_| invalid_field("runtime lease signature"))
1487 }
1488}
1489
1490pub fn validate_worker_runtime_lease(input: &str, now_unix_ms: i64) -> IpcResult<()> {
1491 parse_worker_invocation(input)?.validate_runtime_lease(now_unix_ms)
1492}
1493
1494impl ParsedWorkerInvocation {
1495 pub fn validate_runtime_lease(&self, now_unix_ms: i64) -> IpcResult<()> {
1496 let lease = &self.lease;
1497 let invocation = &self.invocation;
1498 let expires_at_unix_ms = positive_i64(lease.expires_at_unix_ms, "expires_at_unix_ms")?;
1499 if expires_at_unix_ms <= now_unix_ms {
1500 return Err(protocol_violation("runtime execution lease is expired"));
1501 }
1502 validate_runtime_lease_string_binding(&lease.method, &invocation.method, "method", true)?;
1503 if required_string(&lease.method, "method")? != self.method {
1504 return Err(protocol_violation(
1505 "runtime lease method does not match the invocation envelope",
1506 ));
1507 }
1508 for (lease_value, invocation_value, field) in [
1509 (&lease.plugin_id, &invocation.plugin_id, "plugin_id"),
1510 (
1511 &lease.plugin_instance_id,
1512 &invocation.plugin_instance_id,
1513 "plugin_instance_id",
1514 ),
1515 (
1516 &lease.active_fingerprint,
1517 &invocation.active_fingerprint,
1518 "active_fingerprint",
1519 ),
1520 (
1521 &lease.runtime_instance_id,
1522 &invocation.runtime_instance_id,
1523 "runtime_instance_id",
1524 ),
1525 (
1526 &lease.runtime_generation_id,
1527 &invocation.runtime_generation_id,
1528 "runtime_generation_id",
1529 ),
1530 (&lease.effect, &invocation.effect, "effect"),
1531 (&lease.execution, &invocation.execution, "execution"),
1532 (
1533 &lease.audit_correlation_id,
1534 &invocation.audit_correlation_id,
1535 "audit_correlation_id",
1536 ),
1537 ] {
1538 validate_runtime_lease_string_binding(lease_value, invocation_value, field, true)?;
1539 }
1540 for (lease_value, invocation_value, field) in [
1541 (
1542 &lease.surface_instance_id,
1543 &invocation.surface_instance_id,
1544 "surface_instance_id",
1545 ),
1546 (
1547 &lease.owner_session_hash,
1548 &invocation.owner_session_hash,
1549 "owner_session_hash",
1550 ),
1551 (
1552 &lease.owner_user_hash,
1553 &invocation.owner_user_hash,
1554 "owner_user_hash",
1555 ),
1556 (
1557 &lease.owner_env_hash,
1558 &invocation.owner_env_hash,
1559 "owner_env_hash",
1560 ),
1561 (
1562 &lease.session_channel_id_hash,
1563 &invocation.session_channel_id_hash,
1564 "session_channel_id_hash",
1565 ),
1566 (
1567 &lease.bridge_channel_id,
1568 &invocation.bridge_channel_id,
1569 "bridge_channel_id",
1570 ),
1571 (
1572 &lease.operation_id,
1573 &invocation.operation_id,
1574 "operation_id",
1575 ),
1576 (&lease.stream_id, &invocation.stream_id, "stream_id"),
1577 ] {
1578 validate_runtime_lease_string_binding(lease_value, invocation_value, field, false)?;
1579 }
1580 if required_string(&lease.runtime_generation_id, "runtime_generation_id")?
1581 != self.runtime_generation_id
1582 {
1583 return Err(protocol_violation(
1584 "runtime lease runtime_generation_id does not match the invocation frame",
1585 ));
1586 }
1587 validate_runtime_execution_handles(
1588 &lease.execution,
1589 &lease.operation_id,
1590 &lease.stream_id,
1591 )?;
1592 validate_runtime_execution_handles(
1593 &invocation.execution,
1594 &invocation.operation_id,
1595 &invocation.stream_id,
1596 )?;
1597 let invocation_target_hash = self.target_hash()?;
1598 let target_hashes = lease
1599 .target_descriptor_hashes
1600 .as_ref()
1601 .ok_or_else(|| missing_field("runtime lease target_descriptor_hashes"))?;
1602 if target_hashes
1603 .iter()
1604 .filter(|value| value.as_str() == invocation_target_hash.as_str())
1605 .count()
1606 != 1
1607 {
1608 return Err(protocol_violation(
1609 "runtime lease does not bind the worker invocation target",
1610 ));
1611 }
1612 Ok(())
1613 }
1614}
1615
1616pub fn worker_invocation_target_hash(input: &str) -> IpcResult<String> {
1617 parse_worker_invocation(input)?.target_hash()
1618}
1619
1620impl ParsedWorkerInvocation {
1621 pub fn target_hash(&self) -> IpcResult<String> {
1622 self.target_hash
1623 .get_or_init(|| self.build_target_hash())
1624 .clone()
1625 }
1626
1627 fn build_target_hash(&self) -> IpcResult<String> {
1628 let invocation = &self.invocation;
1629 let params = self
1630 .params_json
1631 .as_ref()
1632 .ok_or_else(|| missing_field("worker invocation params"))?;
1633 let broker_access = self
1634 .broker_access_json
1635 .as_ref()
1636 .ok_or_else(|| missing_field("worker invocation broker_access"))?;
1637 let params_hash = format!(
1638 "sha256:{}",
1639 lowercase_hex(&Sha256::digest(params.as_bytes()))
1640 );
1641 if required_string(&invocation.params_sha256, "params_sha256")? != params_hash {
1642 return Err(protocol_violation(
1643 "worker invocation params_sha256 does not match params",
1644 ));
1645 }
1646 let broker_access_hash = format!(
1647 "sha256:{}",
1648 lowercase_hex(&Sha256::digest(broker_access.as_bytes()))
1649 );
1650 if self.invocation.broker_access_sha256.as_deref() != Some(broker_access_hash.as_str()) {
1651 return Err(protocol_violation(
1652 "worker invocation broker_access_sha256 does not match broker_access",
1653 ));
1654 }
1655 let fields = [
1656 WORKER_INVOCATION_TARGET_SCHEMA_VERSION.to_string(),
1657 required_string(&invocation.plugin_id, "plugin_id")?,
1658 required_string(&invocation.plugin_instance_id, "plugin_instance_id")?,
1659 required_string(&invocation.active_fingerprint, "active_fingerprint")?,
1660 required_string(&invocation.runtime_instance_id, "runtime_instance_id")?,
1661 required_string(&invocation.runtime_generation_id, "runtime_generation_id")?,
1662 required_string(&invocation.package_hash, "package_hash")?,
1663 required_string(&invocation.worker_id, "worker_id")?,
1664 required_string(&invocation.worker_mode, "worker_mode")?,
1665 required_string(&invocation.worker_scope, "worker_scope")?,
1666 required_string(&invocation.artifact, "artifact")?,
1667 required_string(&invocation.artifact_sha256, "artifact_sha256")?,
1668 required_string(&invocation.abi, "abi")?,
1669 required_string(&invocation.method, "method")?,
1670 required_string(&invocation.effect, "effect")?,
1671 required_string(&invocation.execution, "execution")?,
1672 optional_string(&invocation.surface_instance_id),
1673 optional_string(&invocation.owner_session_hash),
1674 optional_string(&invocation.owner_user_hash),
1675 optional_string(&invocation.owner_env_hash),
1676 optional_string(&invocation.session_channel_id_hash),
1677 optional_string(&invocation.bridge_channel_id),
1678 optional_string(&invocation.operation_id),
1679 optional_string(&invocation.stream_id),
1680 required_string(&invocation.audit_correlation_id, "audit_correlation_id")?,
1681 params_hash,
1682 broker_access_hash,
1683 ];
1684 let mut canonical = Vec::new();
1685 for field in fields {
1686 let length = u32::try_from(field.len()).map_err(|_| {
1687 protocol_violation("worker invocation target field exceeds uint32 length")
1688 })?;
1689 canonical.extend_from_slice(&length.to_be_bytes());
1690 canonical.extend_from_slice(field.as_bytes());
1691 }
1692 Ok(format!(
1693 "invocation:sha256:{}",
1694 lowercase_hex(&Sha256::digest(canonical))
1695 ))
1696 }
1697}
1698
1699fn lowercase_hex(bytes: &[u8]) -> String {
1700 const HEX: &[u8; 16] = b"0123456789abcdef";
1701 let mut encoded = String::with_capacity(bytes.len() * 2);
1702 for byte in bytes {
1703 encoded.push(HEX[(byte >> 4) as usize] as char);
1704 encoded.push(HEX[(byte & 0x0f) as usize] as char);
1705 }
1706 encoded
1707}
1708
1709fn validate_runtime_lease_string_binding(
1710 lease: &Option<String>,
1711 invocation: &Option<String>,
1712 field: &'static str,
1713 required: bool,
1714) -> IpcResult<()> {
1715 let lease_value = optional_string_ref(lease);
1716 let invocation_value = optional_string_ref(invocation);
1717 if required && (lease_value.is_none() || invocation_value.is_none()) {
1718 return Err(IpcError::MissingField { field });
1719 }
1720 if lease_value != invocation_value {
1721 return Err(IpcError::InvalidField { field });
1722 }
1723 Ok(())
1724}
1725
1726fn validate_runtime_execution_handles(
1727 execution: &Option<String>,
1728 operation_id: &Option<String>,
1729 stream_id: &Option<String>,
1730) -> IpcResult<()> {
1731 let execution = required_string(execution, "execution")?;
1732 let operation_id = optional_string_ref(operation_id).unwrap_or_default();
1733 let stream_id = optional_string_ref(stream_id).unwrap_or_default();
1734 match execution.as_str() {
1735 "sync" if operation_id.is_empty() && stream_id.is_empty() => Ok(()),
1736 "operation" if !operation_id.is_empty() && stream_id.is_empty() => Ok(()),
1737 "subscription" if !operation_id.is_empty() && !stream_id.is_empty() => Ok(()),
1738 _ => Err(invalid_field("runtime lease execution handles")),
1739 }
1740}
1741
1742fn decode_runtime_lease_signature(input: &str) -> IpcResult<Signature> {
1743 let raw = input.trim();
1744 let prefix = format!("{RUNTIME_LEASE_SIGNATURE_ALGORITHM}:");
1745 let encoded = raw
1746 .strip_prefix(prefix.as_str())
1747 .ok_or_else(|| protocol_violation("runtime lease signature algorithm is unsupported"))?;
1748 let decoded = base64::engine::general_purpose::STANDARD
1749 .decode(encoded.as_bytes())
1750 .map_err(|_| invalid_field("runtime lease signature base64"))?;
1751 Signature::from_slice(&decoded).map_err(|_| invalid_field("runtime lease signature length"))
1752}
1753
1754fn runtime_lease_signature_payload_json(
1755 lease: &WorkerLeasePayload,
1756 method: &str,
1757) -> IpcResult<String> {
1758 if let Some(lease_method) = optional_string_ref(&lease.method) {
1759 if lease_method != method.trim() {
1760 return Err(protocol_violation("runtime lease method mismatch"));
1761 }
1762 }
1763 let lease_id = required_string(&lease.lease_id, "lease_id")?;
1764 let token_id = required_string(&lease.token_id, "token_id")?;
1765 let expires_at_unix_ms = positive_i64(lease.expires_at_unix_ms, "expires_at_unix_ms")?;
1766 let issued_at_unix_ms = positive_i64(lease.issued_at_unix_ms, "issued_at_unix_ms")?;
1767 let mut out = String::new();
1768 out.push('{');
1769 append_json_string_field(
1770 &mut out,
1771 "schema_version",
1772 RUNTIME_LEASE_SIGNATURE_SCHEMA_VERSION,
1773 false,
1774 );
1775 append_json_string_field(&mut out, "token_kind", RUNTIME_LEASE_TOKEN_KIND, true);
1776 append_json_string_field(&mut out, "lease_id", &lease_id, true);
1777 append_json_string_field(&mut out, "token_id", &token_id, true);
1778 let lease_nonce = required_string(&lease.lease_nonce, "lease_nonce")?;
1779 if lease_nonce.len() < 16 {
1780 return Err(invalid_field("runtime lease lease_nonce"));
1781 }
1782 append_json_string_field(&mut out, "lease_nonce", &lease_nonce, true);
1783 append_json_string_field(
1784 &mut out,
1785 "plugin_instance_id",
1786 &required_string(&lease.plugin_instance_id, "plugin_instance_id")?,
1787 true,
1788 );
1789 append_json_string_field(
1790 &mut out,
1791 "plugin_id",
1792 &required_string(&lease.plugin_id, "plugin_id")?,
1793 true,
1794 );
1795 append_json_string_field(
1796 &mut out,
1797 "plugin_version",
1798 &required_string(&lease.plugin_version, "plugin_version")?,
1799 true,
1800 );
1801 append_json_string_field(
1802 &mut out,
1803 "active_fingerprint",
1804 &required_string(&lease.active_fingerprint, "active_fingerprint")?,
1805 true,
1806 );
1807 append_json_i64_field(&mut out, "issued_at_unix_ms", issued_at_unix_ms);
1808 append_json_string_field(&mut out, "method", method.trim(), true);
1809 let effect = required_string(&lease.effect, "effect")?;
1810 if !matches!(
1811 effect.as_str(),
1812 "read" | "write" | "execute" | "delete" | "admin"
1813 ) {
1814 return Err(invalid_field("runtime lease effect"));
1815 }
1816 append_json_string_field(&mut out, "effect", &effect, true);
1817 append_json_string_field(
1818 &mut out,
1819 "execution",
1820 &required_string(&lease.execution, "execution")?,
1821 true,
1822 );
1823 validate_runtime_execution_handles(&lease.execution, &lease.operation_id, &lease.stream_id)?;
1824 let operation_id = optional_string(&lease.operation_id);
1825 let stream_id = optional_string(&lease.stream_id);
1826 append_json_optional_string_field(&mut out, "operation_id", Some(&operation_id));
1827 append_json_optional_string_field(&mut out, "stream_id", Some(&stream_id));
1828 append_json_string_field(
1829 &mut out,
1830 "audit_correlation_id",
1831 &required_string(&lease.audit_correlation_id, "audit_correlation_id")?,
1832 true,
1833 );
1834 append_json_optional_string_field(
1835 &mut out,
1836 "surface_instance_id",
1837 optional_string_ref(&lease.surface_instance_id),
1838 );
1839 append_json_optional_string_field(
1840 &mut out,
1841 "owner_session_hash",
1842 optional_string_ref(&lease.owner_session_hash),
1843 );
1844 append_json_optional_string_field(
1845 &mut out,
1846 "owner_user_hash",
1847 optional_string_ref(&lease.owner_user_hash),
1848 );
1849 append_json_string_field(
1850 &mut out,
1851 "owner_env_hash",
1852 &required_string(&lease.owner_env_hash, "owner_env_hash")?,
1853 true,
1854 );
1855 append_json_optional_string_field(
1856 &mut out,
1857 "session_channel_id_hash",
1858 optional_string_ref(&lease.session_channel_id_hash),
1859 );
1860 append_json_optional_string_field(
1861 &mut out,
1862 "bridge_channel_id",
1863 optional_string_ref(&lease.bridge_channel_id),
1864 );
1865 let target_hashes = lease
1866 .target_descriptor_hashes
1867 .as_ref()
1868 .filter(|hashes| !hashes.is_empty())
1869 .ok_or_else(|| missing_field("runtime lease target_descriptor_hashes"))?;
1870 let mut seen_target_hashes = HashSet::new();
1871 out.push_str(",\"target_descriptor_hashes\":[");
1872 for (index, hash) in target_hashes.iter().enumerate() {
1873 let hash = hash.trim();
1874 if hash.is_empty() {
1875 return Err(invalid_field("target_descriptor_hashes item"));
1876 }
1877 if !seen_target_hashes.insert(hash) {
1878 return Err(protocol_violation(
1879 "target_descriptor_hashes item is duplicated",
1880 ));
1881 }
1882 if index > 0 {
1883 out.push(',');
1884 }
1885 out.push('"');
1886 out.push_str(&escape_json_string(hash));
1887 out.push('"');
1888 }
1889 out.push(']');
1890 append_runtime_lease_limits_field(
1891 &mut out,
1892 lease
1893 .limits
1894 .as_ref()
1895 .ok_or_else(|| missing_field("runtime lease limits"))?,
1896 )?;
1897 append_json_u64_field(
1898 &mut out,
1899 "policy_revision",
1900 required_safe_u64(lease.policy_revision, "policy_revision")?,
1901 );
1902 append_json_u64_field(
1903 &mut out,
1904 "management_revision",
1905 required_safe_u64(lease.management_revision, "management_revision")?,
1906 );
1907 append_json_u64_field(
1908 &mut out,
1909 "revoke_epoch",
1910 required_positive_u64(lease.revoke_epoch, "revoke_epoch")?,
1911 );
1912 append_json_i64_field(&mut out, "expires_at_unix_ms", expires_at_unix_ms);
1913 append_json_string_field(
1914 &mut out,
1915 "runtime_shard_id",
1916 &required_string(&lease.runtime_shard_id, "runtime_shard_id")?,
1917 true,
1918 );
1919 append_json_string_field(
1920 &mut out,
1921 "runtime_instance_id",
1922 &required_string(&lease.runtime_instance_id, "runtime_instance_id")?,
1923 true,
1924 );
1925 append_json_string_field(
1926 &mut out,
1927 "runtime_generation_id",
1928 &required_string(&lease.runtime_generation_id, "runtime_generation_id")?,
1929 true,
1930 );
1931 append_json_string_field(
1932 &mut out,
1933 "ipc_channel_id",
1934 &required_string(&lease.ipc_channel_id, "ipc_channel_id")?,
1935 true,
1936 );
1937 let connection_nonce = required_string(&lease.connection_nonce, "connection_nonce")?;
1938 if connection_nonce.len() < 16 {
1939 return Err(invalid_field("runtime lease connection_nonce"));
1940 }
1941 append_json_string_field(&mut out, "connection_nonce", &connection_nonce, true);
1942 append_json_string_field(
1943 &mut out,
1944 "key_id",
1945 &required_string(&lease.key_id, "key_id")?,
1946 true,
1947 );
1948 out.push('}');
1949 Ok(out)
1950}
1951
1952fn optional_string_ref(value: &Option<String>) -> Option<&str> {
1953 value
1954 .as_deref()
1955 .map(str::trim)
1956 .filter(|value| !value.is_empty())
1957}
1958
1959fn optional_string(value: &Option<String>) -> String {
1960 optional_string_ref(value).unwrap_or_default().to_string()
1961}
1962
1963fn positive_i64(value: Option<i64>, field: &'static str) -> IpcResult<i64> {
1964 value.ok_or_else(|| missing_field(field)).and_then(|value| {
1965 if value > 0 && value as u64 <= MAX_JSON_SAFE_INTEGER {
1966 Ok(value)
1967 } else {
1968 Err(invalid_field(field))
1969 }
1970 })
1971}
1972
1973fn nonnegative_i64(value: Option<i64>, field: &'static str) -> IpcResult<i64> {
1974 value.ok_or_else(|| missing_field(field)).and_then(|value| {
1975 if value >= 0 && value as u64 <= MAX_JSON_SAFE_INTEGER {
1976 Ok(value)
1977 } else {
1978 Err(invalid_field(field))
1979 }
1980 })
1981}
1982
1983fn required_u64(value: Option<u64>, field: &'static str) -> IpcResult<u64> {
1984 value.ok_or_else(|| missing_field(field))
1985}
1986
1987fn required_safe_u64(value: Option<u64>, field: &'static str) -> IpcResult<u64> {
1988 validate_safe_u64(required_u64(value, field)?, field)
1989}
1990
1991fn required_positive_u64(value: Option<u64>, field: &'static str) -> IpcResult<u64> {
1992 validate_positive_u64(required_u64(value, field)?, field)
1993}
1994
1995fn validate_positive_u64(value: u64, field: &'static str) -> IpcResult<u64> {
1996 if value == 0 {
1997 return Err(invalid_field(field));
1998 }
1999 validate_safe_u64(value, field)
2000}
2001
2002fn validate_safe_u64(value: u64, field: &'static str) -> IpcResult<u64> {
2003 if value > MAX_JSON_SAFE_INTEGER {
2004 return Err(invalid_field(field));
2005 }
2006 Ok(value)
2007}
2008
2009fn validate_revoke_epoch(revoke_epoch: u64) -> IpcResult<()> {
2010 validate_positive_u64(revoke_epoch, "revoke_epoch").map(|_| ())
2011}
2012
2013fn validate_revision_binding(
2014 policy_revision: u64,
2015 management_revision: u64,
2016 revoke_epoch: u64,
2017) -> IpcResult<()> {
2018 validate_safe_u64(policy_revision, "policy_revision")?;
2019 validate_safe_u64(management_revision, "management_revision")?;
2020 validate_revoke_epoch(revoke_epoch)
2021}
2022
2023fn append_json_string_field(out: &mut String, key: &str, value: &str, comma: bool) {
2024 if comma {
2025 out.push(',');
2026 }
2027 out.push('"');
2028 out.push_str(key);
2029 out.push_str("\":\"");
2030 out.push_str(&escape_json_string(value));
2031 out.push('"');
2032}
2033
2034fn append_json_optional_string_field(out: &mut String, key: &str, value: Option<&str>) {
2035 let Some(value) = value else {
2036 return;
2037 };
2038 let value = value.trim();
2039 if value.is_empty() {
2040 return;
2041 }
2042 append_json_string_field(out, key, value, true);
2043}
2044
2045fn append_json_u64_field(out: &mut String, key: &str, value: u64) {
2046 out.push_str(",\"");
2047 out.push_str(key);
2048 out.push_str("\":");
2049 out.push_str(value.to_string().as_str());
2050}
2051
2052fn append_json_i64_field(out: &mut String, key: &str, value: i64) {
2053 out.push_str(",\"");
2054 out.push_str(key);
2055 out.push_str("\":");
2056 out.push_str(value.to_string().as_str());
2057}
2058
2059fn append_runtime_lease_limits_field(
2060 out: &mut String,
2061 limits: &WorkerLeaseLimitsPayload,
2062) -> IpcResult<()> {
2063 let timeout_ms = nonnegative_i64(limits.timeout_ms, "timeout_ms")?;
2064 let memory_bytes = limits
2065 .memory_bytes
2066 .filter(|value| *value > 0)
2067 .ok_or_else(|| invalid_field("memory_bytes"))?;
2068 if memory_bytes > MAX_RUNTIME_LEASE_MEMORY_BYTES {
2069 return Err(protocol_violation(
2070 "memory_bytes exceeds runtime lease limit",
2071 ));
2072 }
2073 let max_payload_bytes = nonnegative_i64(limits.max_payload_bytes, "max_payload_bytes")?;
2074 let max_stream_bytes_per_sec =
2075 nonnegative_i64(limits.max_stream_bytes_per_sec, "max_stream_bytes_per_sec")?;
2076 out.push_str(&format!(
2077 ",\"limits\":{{\"timeout_ms\":{timeout_ms},\"memory_bytes\":{memory_bytes},\"max_payload_bytes\":{max_payload_bytes},\"max_stream_bytes_per_sec\":{max_stream_bytes_per_sec}}}"
2078 ));
2079 Ok(())
2080}
2081
2082#[derive(Debug, Clone, Copy)]
2083pub struct HelloAckFrameRequest<'a> {
2084 pub request_id: &'a str,
2085 pub runtime_generation_id: &'a str,
2086 pub channel_nonce: &'a str,
2087 pub runtime_version: &'a str,
2088 pub actual_target: &'a RuntimeTarget,
2089 pub wasm_abi_version: &'a str,
2090 pub limits: RuntimeLimits,
2091 pub process_containment: &'a ProcessContainmentEvidence,
2092}
2093
2094pub fn hello_ack_frame(request: HelloAckFrameRequest<'_>) -> IpcResult<String> {
2095 let limits = request.limits.validate()?;
2096 let limits = serde_json::to_string(&limits).map_err(|_| encode_failed("runtime limits"))?;
2097 request.process_containment.validate()?;
2098 let process_containment = serde_json::to_string(request.process_containment)
2099 .map_err(|_| encode_failed("process containment evidence"))?;
2100 Ok(format!(
2101 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"runtime_version\":\"{}\",\"actual_target\":\"{}\",\"rust_ipc_version\":\"{}\",\"wasm_abi_version\":\"{}\",\"contract_set_sha256\":\"{}\",\"channel_nonce\":\"{}\",\"limits\":{},\"process_containment\":{}}}}}",
2102 RUST_IPC_VERSION,
2103 FRAME_TYPE_HELLO_ACK,
2104 escape_json_string(request.request_id),
2105 escape_json_string(request.runtime_generation_id),
2106 escape_json_string(request.runtime_version),
2107 escape_json_string(request.actual_target.as_str()),
2108 RUST_IPC_VERSION,
2109 escape_json_string(request.wasm_abi_version),
2110 CONTRACT_SET_SHA256,
2111 escape_json_string(request.channel_nonce),
2112 limits,
2113 process_containment
2114 ))
2115}
2116
2117pub fn success_response_frame(
2118 frame_type: &str,
2119 request_id: &str,
2120 runtime_generation_id: &str,
2121 result_json: &str,
2122) -> IpcResult<String> {
2123 serde_json::from_str::<serde_json::Value>(result_json)
2124 .map_err(|_| IpcError::InvalidResponseResultJson)?;
2125 let payload = format!("{{\"ok\":true,\"result\":{result_json}}}");
2126 Ok(render_response_frame(
2127 frame_type,
2128 request_id,
2129 runtime_generation_id,
2130 &payload,
2131 ))
2132}
2133
2134pub fn session_revoke_ack_frame(
2135 request_id: &str,
2136 runtime_generation_id: &str,
2137 session_revoke_sequence: u64,
2138 state: SessionRevokeState,
2139 counts: SessionRevokeAckCounts,
2140) -> IpcResult<String> {
2141 if request_id.is_empty() || request_id.trim() != request_id {
2142 return Err(invalid_field("request_id"));
2143 }
2144 if runtime_generation_id.is_empty() || runtime_generation_id.trim() != runtime_generation_id {
2145 return Err(invalid_field("runtime_generation_id"));
2146 }
2147 if session_revoke_sequence == 0 || session_revoke_sequence > MAX_JSON_SAFE_INTEGER {
2148 return Err(invalid_field("session_revoke_sequence"));
2149 }
2150 for (count, field) in [
2151 (counts.queued_invocations, "queued_invocations"),
2152 (counts.running_invocations, "running_invocations"),
2153 (counts.storage_hostcalls, "storage_hostcalls"),
2154 (counts.active_network_requests, "active_network_requests"),
2155 (counts.sockets, "sockets"),
2156 (counts.network_streams, "network_streams"),
2157 ] {
2158 if count > MAX_JSON_SAFE_INTEGER {
2159 return Err(invalid_field(field));
2160 }
2161 }
2162 let result = serde_json::json!({
2163 "session_revoke_sequence": session_revoke_sequence,
2164 "state": state,
2165 "counts": counts,
2166 });
2167 success_response_frame(
2168 FRAME_TYPE_SESSION_REVOKE_ACK,
2169 request_id,
2170 runtime_generation_id,
2171 &result.to_string(),
2172 )
2173}
2174
2175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2176pub struct ResponseError<'a> {
2177 code: &'a str,
2178 message: &'a str,
2179 origin: &'static str,
2180}
2181
2182impl<'a> ResponseError<'a> {
2183 pub fn runtime(code: &'a str, message: &'a str) -> IpcResult<Self> {
2184 Self::new(code, message, ERROR_ORIGIN_RUNTIME)
2185 }
2186
2187 pub fn hostcall(code: &'a str, message: &'a str) -> IpcResult<Self> {
2188 Self::new(code, message, ERROR_ORIGIN_HOSTCALL)
2189 }
2190
2191 pub fn plugin(code: &'a str, message: &'a str) -> IpcResult<Self> {
2192 Self::new(code, message, ERROR_ORIGIN_PLUGIN)
2193 }
2194
2195 fn new(code: &'a str, message: &'a str, origin: &'static str) -> IpcResult<Self> {
2196 if code.trim().is_empty() {
2197 return Err(IpcError::EmptyResponseErrorCode);
2198 }
2199 if message.trim().is_empty() {
2200 return Err(IpcError::EmptyResponseErrorMessage);
2201 }
2202 Ok(Self {
2203 code,
2204 message,
2205 origin,
2206 })
2207 }
2208}
2209
2210pub fn error_response_frame(
2211 frame_type: &str,
2212 request_id: &str,
2213 runtime_generation_id: &str,
2214 error: ResponseError<'_>,
2215) -> IpcResult<String> {
2216 let payload = render_error_payload(error);
2217 Ok(render_response_frame(
2218 frame_type,
2219 request_id,
2220 runtime_generation_id,
2221 &payload,
2222 ))
2223}
2224
2225fn render_error_payload(error: ResponseError<'_>) -> String {
2226 format!(
2227 "{{\"ok\":false,\"code\":\"{}\",\"message\":\"{}\",\"error_origin\":\"{}\"}}",
2228 escape_json_string(error.code),
2229 escape_json_string(error.message),
2230 error.origin,
2231 )
2232}
2233
2234fn render_response_frame(
2235 frame_type: &str,
2236 request_id: &str,
2237 runtime_generation_id: &str,
2238 payload: &str,
2239) -> String {
2240 format!(
2241 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{}}}",
2242 RUST_IPC_VERSION,
2243 escape_json_string(frame_type),
2244 escape_json_string(request_id),
2245 escape_json_string(runtime_generation_id),
2246 payload,
2247 )
2248}
2249
2250pub fn revoke_epoch_ack_result_json(
2251 resource_scope: &NetworkResourceScope,
2252 plugin_instance_id: &str,
2253 revoke_epoch: u64,
2254 closed_socket_count: u64,
2255 closed_stream_count: u64,
2256 closed_storage_handle_count: u64,
2257) -> IpcResult<String> {
2258 if !resource_scope.valid() || resource_scope.kind != "environment" {
2259 return Err(invalid_field("revoke resource scope"));
2260 }
2261 validate_revoke_epoch(revoke_epoch)?;
2262 let resource_scope = serde_json::to_string(resource_scope)
2263 .map_err(|_| encode_failed("revoke resource scope"))?;
2264 Ok(format!(
2265 "{{\"resource_scope\":{},\"plugin_instance_id\":\"{}\",\"revoke_epoch\":{},\"closed_socket_count\":{},\"closed_stream_count\":{},\"closed_storage_handle_count\":{}}}",
2266 resource_scope,
2267 escape_json_string(plugin_instance_id),
2268 revoke_epoch,
2269 closed_socket_count,
2270 closed_stream_count,
2271 closed_storage_handle_count
2272 ))
2273}
2274
2275pub fn heartbeat_ack_result_json(
2276 runtime_generation_id: &str,
2277 runtime_unix_nano: u64,
2278 max_staleness_ms: u64,
2279 host_sent_unix_nano: u64,
2280 status: RuntimeHeartbeatStatus,
2281) -> IpcResult<String> {
2282 let limits = status.limits.validate()?;
2283 Ok(serde_json::json!({
2284 "runtime_generation_id": runtime_generation_id,
2285 "runtime_unix_nano": runtime_unix_nano,
2286 "max_staleness_ms": max_staleness_ms,
2287 "host_sent_unix_nano": host_sent_unix_nano,
2288 "active_invocations": status.active_invocations,
2289 "queued_invocations": status.queued_invocations,
2290 "limits": limits,
2291 "module_cache": status.module_cache,
2292 })
2293 .to_string())
2294}
2295
2296#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
2297pub struct ModuleCacheMetrics {
2298 pub hits: u64,
2299 pub misses: u64,
2300 pub compiles: u64,
2301 pub entries: usize,
2302 pub source_bytes: usize,
2303}
2304
2305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2306pub struct RuntimeHeartbeatStatus {
2307 pub active_invocations: usize,
2308 pub queued_invocations: usize,
2309 pub limits: RuntimeLimits,
2310 pub module_cache: ModuleCacheMetrics,
2311}
2312
2313#[derive(Deserialize)]
2314#[serde(deny_unknown_fields)]
2315struct CancelInvokePayload {
2316 invocation_request_id: String,
2317}
2318
2319pub fn parse_cancel_invoke(input: &str) -> IpcResult<String> {
2320 let frame = parse_raw_frame(input)?;
2321 if frame.ipc_version != RUST_IPC_VERSION {
2322 return Err(protocol_violation("unsupported ipc_version"));
2323 }
2324 if frame.frame_type != FRAME_TYPE_CANCEL_INVOKE {
2325 return Err(protocol_violation("expected cancel_invoke frame"));
2326 }
2327 let payload: CancelInvokePayload = serde_json::from_str(frame.payload.get())
2328 .map_err(|_| decode_failed("cancel_invoke payload"))?;
2329 let request_id = payload.invocation_request_id.trim();
2330 if request_id.is_empty() {
2331 return Err(invalid_field("invocation_request_id"));
2332 }
2333 Ok(request_id.to_string())
2334}
2335
2336pub fn cancel_invoke_ack_frame(
2337 request_id: &str,
2338 runtime_generation_id: &str,
2339 invocation_request_id: &str,
2340 disposition: &str,
2341) -> IpcResult<String> {
2342 let result = serde_json::json!({
2343 "invocation_request_id": invocation_request_id,
2344 "disposition": disposition,
2345 });
2346 success_response_frame(
2347 FRAME_TYPE_CANCEL_INVOKE_ACK,
2348 request_id,
2349 runtime_generation_id,
2350 &result.to_string(),
2351 )
2352}
2353
2354enum HostcallResponsePayload<T> {
2355 Success(T),
2356 Failure(HostcallFailureResponsePayload),
2357}
2358
2359#[derive(Deserialize)]
2360struct BooleanResponseDiscriminator {
2361 ok: bool,
2362}
2363
2364#[derive(Deserialize)]
2365#[serde(deny_unknown_fields)]
2366struct HostcallFailureResponsePayload {
2367 ok: bool,
2368 code: String,
2369 message: String,
2370 error_origin: String,
2371}
2372
2373#[derive(Deserialize)]
2374#[serde(deny_unknown_fields)]
2375struct OpenHandleSuccessResponsePayload {
2376 ok: bool,
2377 package_hash: String,
2378 artifact: String,
2379 sha256: String,
2380 content_base64: String,
2381}
2382
2383#[derive(Deserialize)]
2384#[serde(deny_unknown_fields)]
2385#[allow(dead_code)]
2386struct HandleGrantSuccessResponsePayload {
2387 ok: bool,
2388 handle_grant_id: String,
2389 handle_id: String,
2390 method: String,
2391 runtime_generation_id: String,
2392 resource_scope: NetworkResourceScope,
2393 max_bytes_per_second: Option<u64>,
2394 max_total_bytes: Option<u64>,
2395}
2396
2397#[derive(Deserialize)]
2398#[serde(deny_unknown_fields)]
2399#[allow(dead_code)]
2400struct StorageUsageResponsePayload {
2401 plugin_instance_id: String,
2402 store_id: String,
2403 usage_bytes: u64,
2404 quota_bytes: u64,
2405 usage_files: u64,
2406 quota_files: u64,
2407}
2408
2409#[derive(Deserialize)]
2410#[serde(deny_unknown_fields)]
2411#[allow(dead_code)]
2412struct StorageFileEntryResponsePayload {
2413 path: String,
2414 dir: bool,
2415 size_bytes: Option<u64>,
2416 updated_at: String,
2417}
2418
2419#[derive(Deserialize)]
2420#[serde(deny_unknown_fields)]
2421#[allow(dead_code)]
2422struct StorageFileReadSuccessResponsePayload {
2423 ok: bool,
2424 path: String,
2425 data_base64: String,
2426 size_bytes: u64,
2427 usage: StorageUsageResponsePayload,
2428}
2429
2430#[derive(Deserialize)]
2431#[serde(deny_unknown_fields)]
2432#[allow(dead_code)]
2433struct StorageFileWriteSuccessResponsePayload {
2434 ok: bool,
2435 path: String,
2436 size_bytes: u64,
2437 usage: StorageUsageResponsePayload,
2438}
2439
2440#[derive(Deserialize)]
2441#[serde(deny_unknown_fields)]
2442#[allow(dead_code)]
2443struct StorageFileDeleteSuccessResponsePayload {
2444 ok: bool,
2445 path: String,
2446}
2447
2448#[derive(Deserialize)]
2449#[serde(deny_unknown_fields)]
2450#[allow(dead_code)]
2451struct StorageFileListSuccessResponsePayload {
2452 ok: bool,
2453 path: String,
2454 entries: Vec<StorageFileEntryResponsePayload>,
2455 usage: StorageUsageResponsePayload,
2456}
2457
2458#[derive(Deserialize)]
2459#[serde(deny_unknown_fields)]
2460#[allow(dead_code)]
2461struct StorageKVEntryResponsePayload {
2462 key: String,
2463 size_bytes: u64,
2464 updated_at: String,
2465}
2466
2467#[derive(Deserialize)]
2468#[serde(deny_unknown_fields)]
2469#[allow(dead_code)]
2470struct StorageKVGetSuccessResponsePayload {
2471 ok: bool,
2472 key: String,
2473 value_base64: String,
2474 size_bytes: u64,
2475 usage: StorageUsageResponsePayload,
2476}
2477
2478#[derive(Deserialize)]
2479#[serde(deny_unknown_fields)]
2480#[allow(dead_code)]
2481struct StorageKVPutSuccessResponsePayload {
2482 ok: bool,
2483 key: String,
2484 size_bytes: u64,
2485 usage: StorageUsageResponsePayload,
2486}
2487
2488#[derive(Deserialize)]
2489#[serde(deny_unknown_fields)]
2490#[allow(dead_code)]
2491struct StorageKVDeleteSuccessResponsePayload {
2492 ok: bool,
2493 key: String,
2494}
2495
2496#[derive(Deserialize)]
2497#[serde(deny_unknown_fields)]
2498#[allow(dead_code)]
2499struct StorageKVListSuccessResponsePayload {
2500 ok: bool,
2501 prefix: Option<String>,
2502 entries: Vec<StorageKVEntryResponsePayload>,
2503 usage: StorageUsageResponsePayload,
2504}
2505
2506#[derive(Deserialize)]
2507#[serde(deny_unknown_fields)]
2508#[allow(dead_code)]
2509struct StorageSQLiteValueResponsePayload {
2510 #[serde(rename = "null")]
2511 null_value: Option<bool>,
2512 int: Option<i64>,
2513 float: Option<f64>,
2514 text: Option<String>,
2515 blob_base64: Option<String>,
2516}
2517
2518impl StorageSQLiteValueResponsePayload {
2519 fn is_exactly_typed(&self) -> bool {
2520 let variants = usize::from(self.null_value.is_some())
2521 + usize::from(self.int.is_some())
2522 + usize::from(self.float.is_some())
2523 + usize::from(self.text.is_some())
2524 + usize::from(self.blob_base64.is_some());
2525 variants == 1 && self.null_value.unwrap_or(true)
2526 }
2527}
2528
2529#[derive(Deserialize)]
2530#[serde(deny_unknown_fields)]
2531#[allow(dead_code)]
2532struct StorageSQLiteExecSuccessResponsePayload {
2533 ok: bool,
2534 database: String,
2535 rows_affected: u64,
2536 last_insert_id: Option<u64>,
2537 usage: StorageUsageResponsePayload,
2538}
2539
2540#[derive(Deserialize)]
2541#[serde(deny_unknown_fields)]
2542#[allow(dead_code)]
2543struct StorageSQLiteQuerySuccessResponsePayload {
2544 ok: bool,
2545 database: String,
2546 columns: Vec<String>,
2547 rows: Vec<Vec<StorageSQLiteValueResponsePayload>>,
2548 usage: StorageUsageResponsePayload,
2549}
2550
2551#[derive(Deserialize)]
2552#[serde(deny_unknown_fields)]
2553#[allow(dead_code)]
2554struct NetworkDestinationResponsePayload {
2555 transport: String,
2556 scheme: Option<String>,
2557 host: String,
2558 port: u16,
2559}
2560
2561#[derive(Deserialize)]
2562#[serde(deny_unknown_fields)]
2563#[allow(dead_code)]
2564struct NetworkGrantSuccessResponsePayload {
2565 ok: bool,
2566 grant_id: String,
2567 plugin_instance_id: String,
2568 active_fingerprint: String,
2569 resource_scope: NetworkResourceScope,
2570 policy_revision: u64,
2571 management_revision: u64,
2572 revoke_epoch: u64,
2573 connector_id: String,
2574 transport: String,
2575 destination: NetworkDestinationResponsePayload,
2576 runtime_generation_id: String,
2577 target_classifier_version: String,
2578 expires_at: String,
2579}
2580
2581#[derive(Deserialize)]
2582#[serde(deny_unknown_fields)]
2583#[allow(dead_code)]
2584struct NetworkExecuteSuccessResponsePayload {
2585 ok: bool,
2586 transport: String,
2587 destination: NetworkDestinationResponsePayload,
2588 status_code: Option<u16>,
2589 headers: Option<HashMap<String, Vec<String>>>,
2590 message_type: Option<String>,
2591 body_base64: Option<String>,
2592 payload_base64: Option<String>,
2593 stream_id: Option<String>,
2594 bytes_read: Option<u64>,
2595 chunk_count: Option<u64>,
2596 grant_id: String,
2597 connector_id: String,
2598 runtime_generation_id: String,
2599}
2600
2601fn parse_hostcall_response_frame<T: DeserializeOwned>(
2602 input: &str,
2603 expected_frame_type: &'static str,
2604) -> IpcResult<(RawIPCFrame, HostcallResponsePayload<T>)> {
2605 let frame = parse_raw_frame(input)?;
2606 if frame.ipc_version != RUST_IPC_VERSION {
2607 return Err(protocol_violation("unsupported ipc_version"));
2608 }
2609 if frame.frame_type != expected_frame_type {
2610 return Err(protocol_violation(
2611 "unexpected hostcall response frame type",
2612 ));
2613 }
2614 if frame.request_id.trim().is_empty() {
2615 return Err(invalid_field("hostcall response request_id"));
2616 }
2617 let runtime_generation_id = frame
2618 .runtime_generation_id
2619 .as_deref()
2620 .filter(|value| !value.trim().is_empty())
2621 .ok_or_else(|| missing_field("hostcall response runtime_generation_id"))?;
2622 if runtime_generation_id.trim().is_empty() {
2623 return Err(invalid_field("hostcall response runtime_generation_id"));
2624 }
2625 let discriminator: BooleanResponseDiscriminator = serde_json::from_str(frame.payload.get())
2626 .map_err(|_| decode_failed("hostcall response discriminator"))?;
2627 let payload = if discriminator.ok {
2628 HostcallResponsePayload::Success(
2629 serde_json::from_str(frame.payload.get())
2630 .map_err(|_| decode_failed("hostcall success response payload"))?,
2631 )
2632 } else {
2633 HostcallResponsePayload::Failure(
2634 serde_json::from_str(frame.payload.get())
2635 .map_err(|_| decode_failed("hostcall failure response payload"))?,
2636 )
2637 };
2638 Ok((frame, payload))
2639}
2640
2641fn validate_hostcall_response_identity(
2642 frame: &RawIPCFrame,
2643 expected_request_id: &str,
2644 expected_runtime_generation_id: &str,
2645 _label: &'static str,
2646) -> IpcResult<()> {
2647 if frame.request_id != expected_request_id {
2648 return Err(protocol_violation("hostcall response request_id mismatch"));
2649 }
2650 if frame.runtime_generation_id.as_deref() != Some(expected_runtime_generation_id) {
2651 return Err(protocol_violation(
2652 "hostcall response runtime_generation_id mismatch",
2653 ));
2654 }
2655 Ok(())
2656}
2657
2658fn validated_hostcall_failure(failure: HostcallFailureResponsePayload) -> IpcResult<IpcError> {
2659 if failure.ok {
2660 return Err(protocol_violation(
2661 "hostcall failure response ok must be false",
2662 ));
2663 }
2664 if failure.error_origin != ERROR_ORIGIN_HOSTCALL {
2665 return Err(protocol_violation(
2666 "hostcall response error_origin must be hostcall",
2667 ));
2668 }
2669 let code = failure.code.trim();
2670 if !is_stable_worker_error_code(code) {
2671 return Err(invalid_field("hostcall response code"));
2672 }
2673 let message = failure.message.trim();
2674 if message.is_empty() || message.len() > 4096 {
2675 return Err(invalid_field("hostcall response message"));
2676 }
2677 Ok(IpcError::RemoteFailure {
2678 code: code.to_string(),
2679 })
2680}
2681
2682pub fn open_handle_frame(
2683 request_id: &str,
2684 runtime_generation_id: &str,
2685 identity: &WorkerInvocationIdentity,
2686) -> String {
2687 format!(
2688 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"package_hash\":\"{}\",\"artifact\":\"{}\",\"artifact_sha256\":\"{}\"}}}}",
2689 RUST_IPC_VERSION,
2690 FRAME_TYPE_OPEN_HANDLE,
2691 escape_json_string(request_id),
2692 escape_json_string(runtime_generation_id),
2693 escape_json_string(&identity.package_hash),
2694 escape_json_string(&identity.artifact),
2695 escape_json_string(&identity.artifact_sha256)
2696 )
2697}
2698
2699pub fn compile_flight_register_frame(
2700 parent_request_id: &str,
2701 runtime_generation_id: &str,
2702 identity: &WorkerInvocationIdentity,
2703) -> String {
2704 compile_flight_lifecycle_frame(
2705 FRAME_TYPE_COMPILE_FLIGHT_REGISTER,
2706 parent_request_id,
2707 runtime_generation_id,
2708 identity,
2709 )
2710}
2711
2712pub fn compile_flight_complete_frame(
2713 parent_request_id: &str,
2714 runtime_generation_id: &str,
2715 identity: &WorkerInvocationIdentity,
2716) -> String {
2717 compile_flight_lifecycle_frame(
2718 FRAME_TYPE_COMPILE_FLIGHT_COMPLETE,
2719 parent_request_id,
2720 runtime_generation_id,
2721 identity,
2722 )
2723}
2724
2725fn compile_flight_lifecycle_frame(
2726 frame_type: &str,
2727 parent_request_id: &str,
2728 runtime_generation_id: &str,
2729 identity: &WorkerInvocationIdentity,
2730) -> String {
2731 let artifact_request_id = format!("{parent_request_id}:artifact");
2732 let request_id = if frame_type == FRAME_TYPE_COMPILE_FLIGHT_REGISTER {
2733 format!("{artifact_request_id}:register")
2734 } else {
2735 format!("{artifact_request_id}:complete")
2736 };
2737 format!(
2738 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"parent_request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"artifact_request_id\":\"{}\",\"package_hash\":\"{}\",\"artifact\":\"{}\",\"artifact_sha256\":\"{}\",\"wasm_abi_version\":\"{}\"}}}}",
2739 RUST_IPC_VERSION,
2740 frame_type,
2741 escape_json_string(&request_id),
2742 escape_json_string(parent_request_id),
2743 escape_json_string(runtime_generation_id),
2744 escape_json_string(&artifact_request_id),
2745 escape_json_string(&identity.package_hash),
2746 escape_json_string(&identity.artifact),
2747 escape_json_string(&identity.artifact_sha256),
2748 WASM_ABI_VERSION,
2749 )
2750}
2751
2752pub fn validate_open_handle_response(
2753 input: &str,
2754 expected_request_id: &str,
2755 expected_parent_request_id: &str,
2756 expected_runtime_generation_id: &str,
2757 expected_identity: &WorkerInvocationIdentity,
2758) -> IpcResult<()> {
2759 parse_open_handle_success_response(
2760 input,
2761 expected_request_id,
2762 expected_parent_request_id,
2763 expected_runtime_generation_id,
2764 expected_identity,
2765 )?;
2766 Ok(())
2767}
2768
2769fn parse_open_handle_success_response(
2770 input: &str,
2771 expected_request_id: &str,
2772 expected_parent_request_id: &str,
2773 expected_runtime_generation_id: &str,
2774 expected_identity: &WorkerInvocationIdentity,
2775) -> IpcResult<OpenHandleSuccessResponsePayload> {
2776 let (frame, response) = parse_hostcall_response_frame::<OpenHandleSuccessResponsePayload>(
2777 input,
2778 FRAME_TYPE_OPEN_HANDLE,
2779 )?;
2780 validate_hostcall_response_identity(
2781 &frame,
2782 expected_request_id,
2783 expected_runtime_generation_id,
2784 "open_handle",
2785 )?;
2786 if frame.parent_request_id.as_deref() != Some(expected_parent_request_id) {
2787 return Err(protocol_violation("open_handle parent_request_id mismatch"));
2788 }
2789 let success = match response {
2790 HostcallResponsePayload::Success(success) if success.ok => success,
2791 HostcallResponsePayload::Success(_) => {
2792 return Err(protocol_violation(
2793 "open_handle success response ok must be true",
2794 ));
2795 }
2796 HostcallResponsePayload::Failure(failure) => {
2797 return Err(validated_hostcall_failure(failure)?);
2798 }
2799 };
2800 if success.package_hash != expected_identity.package_hash
2801 || success.artifact != expected_identity.artifact
2802 || success.sha256 != expected_identity.artifact_sha256
2803 {
2804 return Err(protocol_violation("open_handle artifact identity mismatch"));
2805 }
2806 if success.content_base64.trim().is_empty() {
2807 return Err(invalid_field("content_base64"));
2808 }
2809 Ok(success)
2810}
2811
2812pub fn open_handle_content_base64(
2813 input: &str,
2814 expected_request_id: &str,
2815 expected_parent_request_id: &str,
2816 expected_runtime_generation_id: &str,
2817 expected_identity: &WorkerInvocationIdentity,
2818) -> IpcResult<String> {
2819 let success = parse_open_handle_success_response(
2820 input,
2821 expected_request_id,
2822 expected_parent_request_id,
2823 expected_runtime_generation_id,
2824 expected_identity,
2825 )?;
2826 Ok(success.content_base64)
2827}
2828
2829pub fn worker_success_result_json(
2830 identity: &WorkerInvocationIdentity,
2831 wasm_byte_len: usize,
2832 storage_file_result_json: Option<&str>,
2833 storage_kv_result_json: Option<&str>,
2834 storage_sqlite_result_json: Option<&str>,
2835 network_execute_result_json: Option<&str>,
2836) -> String {
2837 worker_success_result_json_with_network_results(
2838 identity,
2839 wasm_byte_len,
2840 storage_file_result_json,
2841 storage_kv_result_json,
2842 storage_sqlite_result_json,
2843 network_execute_result_json.into_iter().collect(),
2844 )
2845}
2846
2847pub fn worker_success_result_json_with_network_results(
2848 identity: &WorkerInvocationIdentity,
2849 wasm_byte_len: usize,
2850 storage_file_result_json: Option<&str>,
2851 storage_kv_result_json: Option<&str>,
2852 storage_sqlite_result_json: Option<&str>,
2853 network_execute_result_jsons: Vec<&str>,
2854) -> String {
2855 let storage_file = storage_file_result_json
2856 .map(|result| format!(",\"storage_file\":{result}"))
2857 .unwrap_or_default();
2858 let storage_kv = storage_kv_result_json
2859 .map(|result| format!(",\"storage_kv\":{result}"))
2860 .unwrap_or_default();
2861 let storage_sqlite = storage_sqlite_result_json
2862 .map(|result| format!(",\"storage_sqlite\":{result}"))
2863 .unwrap_or_default();
2864 let stream_id = first_network_stream_id(&network_execute_result_jsons)
2865 .map(|value| format!(",\"stream_id\":\"{}\"", escape_json_string(&value)))
2866 .unwrap_or_default();
2867 let network_execute = network_success_fields(network_execute_result_jsons);
2868 format!(
2869 "{{\"data\":{{\"method\":\"{}\",\"worker_id\":\"{}\",\"backend\":\"executed wasm worker scaffold\",\"transport\":\"rust runtime ipc\",\"wasm_abi\":\"{}\",\"wasm_byte_len\":{}{}{}{}{}}}{}}}",
2870 escape_json_string(&identity.method),
2871 escape_json_string(&identity.worker_id),
2872 WASM_ABI_VERSION,
2873 wasm_byte_len,
2874 storage_file,
2875 storage_kv,
2876 storage_sqlite,
2877 network_execute,
2878 stream_id
2879 )
2880}
2881
2882fn network_success_fields(results: Vec<&str>) -> String {
2883 let mut fields = String::new();
2884 for (index, result) in results.into_iter().enumerate() {
2885 let field = if index == 0 {
2886 "network_execute".to_string()
2887 } else {
2888 format!(
2889 "network_execute_{}",
2890 network_result_transport(result)
2891 .filter(|transport| !transport.is_empty())
2892 .unwrap_or_else(|| index.to_string())
2893 )
2894 };
2895 fields.push_str(&format!(",\"{}\":{}", escape_json_string(&field), result));
2896 }
2897 fields
2898}
2899
2900#[derive(Deserialize)]
2901struct NetworkResultProjection {
2902 transport: Option<String>,
2903 stream_id: Option<String>,
2904}
2905
2906fn parse_network_result_projection(result: &str) -> Option<NetworkResultProjection> {
2907 serde_json::from_str(result).ok()
2908}
2909
2910fn network_result_transport(result: &str) -> Option<String> {
2911 parse_network_result_projection(result)
2912 .and_then(|result| result.transport)
2913 .map(|value| {
2914 value
2915 .chars()
2916 .map(|ch| {
2917 if ch.is_ascii_alphanumeric() {
2918 ch.to_ascii_lowercase()
2919 } else {
2920 '_'
2921 }
2922 })
2923 .collect::<String>()
2924 .trim_matches('_')
2925 .to_string()
2926 })
2927}
2928
2929fn first_network_stream_id(results: &[&str]) -> Option<String> {
2930 results
2931 .iter()
2932 .filter_map(|result| parse_network_result_projection(result)?.stream_id)
2933 .find(|stream_id| !stream_id.trim().is_empty())
2934}
2935
2936pub fn storage_file_payload_json(input: &str, expected_operation: &str) -> IpcResult<String> {
2937 match expected_operation {
2938 "read" => successful_hostcall_payload_json::<StorageFileReadSuccessResponsePayload, _>(
2939 input,
2940 FRAME_TYPE_STORAGE_FILE,
2941 |payload| payload.ok,
2942 ),
2943 "write" => successful_hostcall_payload_json::<StorageFileWriteSuccessResponsePayload, _>(
2944 input,
2945 FRAME_TYPE_STORAGE_FILE,
2946 |payload| payload.ok,
2947 ),
2948 "delete" => successful_hostcall_payload_json::<StorageFileDeleteSuccessResponsePayload, _>(
2949 input,
2950 FRAME_TYPE_STORAGE_FILE,
2951 |payload| payload.ok,
2952 ),
2953 "list" => successful_hostcall_payload_json::<StorageFileListSuccessResponsePayload, _>(
2954 input,
2955 FRAME_TYPE_STORAGE_FILE,
2956 |payload| payload.ok,
2957 ),
2958 _ => Err(invalid_field("storage_file response operation")),
2959 }
2960}
2961
2962pub fn storage_kv_payload_json(input: &str, expected_operation: &str) -> IpcResult<String> {
2963 match expected_operation {
2964 "get" => successful_hostcall_payload_json::<StorageKVGetSuccessResponsePayload, _>(
2965 input,
2966 FRAME_TYPE_STORAGE_KV,
2967 |payload| payload.ok,
2968 ),
2969 "put" => successful_hostcall_payload_json::<StorageKVPutSuccessResponsePayload, _>(
2970 input,
2971 FRAME_TYPE_STORAGE_KV,
2972 |payload| payload.ok,
2973 ),
2974 "delete" => successful_hostcall_payload_json::<StorageKVDeleteSuccessResponsePayload, _>(
2975 input,
2976 FRAME_TYPE_STORAGE_KV,
2977 |payload| payload.ok,
2978 ),
2979 "list" => successful_hostcall_payload_json::<StorageKVListSuccessResponsePayload, _>(
2980 input,
2981 FRAME_TYPE_STORAGE_KV,
2982 |payload| payload.ok,
2983 ),
2984 _ => Err(invalid_field("storage_kv response operation")),
2985 }
2986}
2987
2988pub fn storage_sqlite_payload_json(input: &str, expected_operation: &str) -> IpcResult<String> {
2989 match expected_operation {
2990 "exec" => successful_hostcall_payload_json::<StorageSQLiteExecSuccessResponsePayload, _>(
2991 input,
2992 FRAME_TYPE_STORAGE_SQLITE,
2993 |payload| payload.ok,
2994 ),
2995 "query" => successful_hostcall_payload_json::<StorageSQLiteQuerySuccessResponsePayload, _>(
2996 input,
2997 FRAME_TYPE_STORAGE_SQLITE,
2998 |payload| {
2999 payload.ok
3000 && payload
3001 .rows
3002 .iter()
3003 .flatten()
3004 .all(StorageSQLiteValueResponsePayload::is_exactly_typed)
3005 },
3006 ),
3007 _ => Err(invalid_field("storage_sqlite response operation")),
3008 }
3009}
3010
3011pub fn network_execute_payload_json(input: &str) -> IpcResult<String> {
3012 successful_hostcall_payload_json::<NetworkExecuteSuccessResponsePayload, _>(
3013 input,
3014 FRAME_TYPE_NETWORK_EXECUTE,
3015 |payload| payload.ok,
3016 )
3017}
3018
3019fn successful_hostcall_payload_json<T, F>(
3020 input: &str,
3021 frame_type: &'static str,
3022 is_success: F,
3023) -> IpcResult<String>
3024where
3025 T: DeserializeOwned,
3026 F: FnOnce(&T) -> bool,
3027{
3028 let (frame, response) = parse_hostcall_response_frame::<T>(input, frame_type)?;
3029 match response {
3030 HostcallResponsePayload::Success(payload) if is_success(&payload) => {
3031 Ok(frame.payload.get().to_string())
3032 }
3033 HostcallResponsePayload::Success(_) => Err(protocol_violation(
3034 "hostcall success response ok must be true",
3035 )),
3036 HostcallResponsePayload::Failure(failure) => Err(validated_hostcall_failure(failure)?),
3037 }
3038}
3039
3040fn parse_validated_hostcall_success<T, F>(
3041 input: &str,
3042 frame_type: &'static str,
3043 expected_request_id: &str,
3044 expected_runtime_generation_id: &str,
3045 is_success: F,
3046) -> IpcResult<T>
3047where
3048 T: DeserializeOwned,
3049 F: FnOnce(&T) -> bool,
3050{
3051 let (frame, response) = parse_hostcall_response_frame::<T>(input, frame_type)?;
3052 validate_hostcall_response_identity(
3053 &frame,
3054 expected_request_id,
3055 expected_runtime_generation_id,
3056 frame_type,
3057 )?;
3058 match response {
3059 HostcallResponsePayload::Success(payload) if is_success(&payload) => Ok(payload),
3060 HostcallResponsePayload::Success(_) => Err(protocol_violation(
3061 "hostcall success response ok must be true",
3062 )),
3063 HostcallResponsePayload::Failure(failure) => Err(validated_hostcall_failure(failure)?),
3064 }
3065}
3066
3067#[derive(Debug, Clone, PartialEq, Eq)]
3068pub struct HandleGrantValidationRequest {
3069 pub handle_grant_token: String,
3070 pub plugin_instance_id: String,
3071 pub active_fingerprint: String,
3072 pub runtime_instance_id: String,
3073 pub runtime_generation_id: String,
3074 pub runtime_shard_id: String,
3075 pub owner_session_hash: String,
3076 pub owner_user_hash: String,
3077 pub owner_env_hash: String,
3078 pub session_channel_id_hash: String,
3079 pub handle_id: String,
3080 pub method: String,
3081 pub resource_scope: NetworkResourceScope,
3082 pub policy_revision: u64,
3083 pub management_revision: u64,
3084 pub revoke_epoch: u64,
3085}
3086
3087pub fn validate_handle_grant_frame(
3088 request_id: &str,
3089 runtime_generation_id: &str,
3090 req: &HandleGrantValidationRequest,
3091) -> IpcResult<String> {
3092 for (value, field) in [
3093 (request_id, "request_id"),
3094 (runtime_generation_id, "runtime_generation_id"),
3095 (&req.handle_grant_token, "handle_grant_token"),
3096 (&req.plugin_instance_id, "plugin_instance_id"),
3097 (&req.active_fingerprint, "active_fingerprint"),
3098 (&req.runtime_instance_id, "runtime_instance_id"),
3099 (&req.runtime_generation_id, "runtime_generation_id"),
3100 (&req.runtime_shard_id, "runtime_shard_id"),
3101 (&req.handle_id, "handle_id"),
3102 (&req.method, "method"),
3103 ] {
3104 if value.trim().is_empty() {
3105 return Err(invalid_field(field));
3106 }
3107 }
3108 if runtime_generation_id != req.runtime_generation_id {
3109 return Err(protocol_violation(
3110 "validate_handle_grant runtime_generation_id mismatch",
3111 ));
3112 }
3113 if !req.resource_scope.valid() {
3114 return Err(invalid_field("handle grant resource scope"));
3115 }
3116 if req.owner_session_hash.trim().is_empty()
3117 || req.owner_user_hash.trim().is_empty()
3118 || req.owner_env_hash.trim().is_empty()
3119 || req.session_channel_id_hash.trim().is_empty()
3120 {
3121 return Err(invalid_field("handle grant session audience"));
3122 }
3123 if req.resource_scope.owner_env_hash != req.owner_env_hash
3124 || (req.resource_scope.kind == "user"
3125 && req.resource_scope.owner_user_hash != req.owner_user_hash)
3126 {
3127 return Err(invalid_field("handle grant resource scope"));
3128 }
3129 validate_revision_binding(
3130 req.policy_revision,
3131 req.management_revision,
3132 req.revoke_epoch,
3133 )?;
3134 let resource_scope = serde_json::to_string(&req.resource_scope)
3135 .map_err(|_| encode_failed("handle grant resource scope"))?;
3136 Ok(format!(
3137 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"handle_grant_token\":\"{}\",\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"owner_session_hash\":\"{}\",\"owner_user_hash\":\"{}\",\"owner_env_hash\":\"{}\",\"session_channel_id_hash\":\"{}\",\"handle_id\":\"{}\",\"method\":\"{}\",\"resource_scope\":{},\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{}}}}}",
3138 RUST_IPC_VERSION,
3139 FRAME_TYPE_VALIDATE_HANDLE_GRANT,
3140 escape_json_string(request_id),
3141 escape_json_string(runtime_generation_id),
3142 escape_json_string(&req.handle_grant_token),
3143 escape_json_string(&req.plugin_instance_id),
3144 escape_json_string(&req.active_fingerprint),
3145 escape_json_string(&req.runtime_instance_id),
3146 escape_json_string(&req.runtime_generation_id),
3147 escape_json_string(&req.runtime_shard_id),
3148 escape_json_string(&req.owner_session_hash),
3149 escape_json_string(&req.owner_user_hash),
3150 escape_json_string(&req.owner_env_hash),
3151 escape_json_string(&req.session_channel_id_hash),
3152 escape_json_string(&req.handle_id),
3153 escape_json_string(&req.method),
3154 resource_scope,
3155 req.policy_revision,
3156 req.management_revision,
3157 req.revoke_epoch
3158 ))
3159}
3160
3161pub fn validate_handle_grant_response(
3162 input: &str,
3163 expected_request_id: &str,
3164 expected_runtime_generation_id: &str,
3165 expected_handle_id: &str,
3166 expected_method: &str,
3167 expected_resource_scope: &NetworkResourceScope,
3168) -> IpcResult<()> {
3169 let (frame, response) = parse_hostcall_response_frame::<HandleGrantSuccessResponsePayload>(
3170 input,
3171 FRAME_TYPE_VALIDATE_HANDLE_GRANT,
3172 )?;
3173 validate_hostcall_response_identity(
3174 &frame,
3175 expected_request_id,
3176 expected_runtime_generation_id,
3177 "validate_handle_grant",
3178 )?;
3179 let success = match response {
3180 HostcallResponsePayload::Success(success) if success.ok => success,
3181 HostcallResponsePayload::Success(_) => {
3182 return Err(protocol_violation(
3183 "validate_handle_grant success response ok must be true",
3184 ));
3185 }
3186 HostcallResponsePayload::Failure(failure) => {
3187 return Err(validated_hostcall_failure(failure)?);
3188 }
3189 };
3190 if success.handle_id != expected_handle_id || success.method != expected_method {
3191 return Err(protocol_violation(
3192 "validate_handle_grant audience mismatch",
3193 ));
3194 }
3195 if success.runtime_generation_id != expected_runtime_generation_id {
3196 return Err(protocol_violation(
3197 "validate_handle_grant payload runtime_generation_id mismatch",
3198 ));
3199 }
3200 if !success.resource_scope.valid() || success.resource_scope != *expected_resource_scope {
3201 return Err(protocol_violation(
3202 "validate_handle_grant payload resource_scope mismatch",
3203 ));
3204 }
3205 Ok(())
3206}
3207
3208#[derive(Debug, Clone, PartialEq, Eq)]
3209pub struct StorageFileRequest {
3210 pub handle_grant_token: String,
3211 pub plugin_instance_id: String,
3212 pub active_fingerprint: String,
3213 pub runtime_instance_id: String,
3214 pub runtime_generation_id: String,
3215 pub runtime_shard_id: String,
3216 pub handle_id: String,
3217 pub method: String,
3218 pub resource_scope: NetworkResourceScope,
3219 pub policy_revision: u64,
3220 pub management_revision: u64,
3221 pub revoke_epoch: u64,
3222 pub operation: String,
3223 pub store_id: String,
3224 pub path: String,
3225 pub data_base64: String,
3226 pub max_bytes: u64,
3227 pub max_entries: u64,
3228 pub recursive: bool,
3229}
3230
3231pub fn storage_file_frame(
3232 request_id: &str,
3233 runtime_generation_id: &str,
3234 req: &StorageFileRequest,
3235) -> IpcResult<String> {
3236 if !req.resource_scope.valid() {
3237 return Err(invalid_field("storage file resource scope"));
3238 }
3239 validate_revision_binding(
3240 req.policy_revision,
3241 req.management_revision,
3242 req.revoke_epoch,
3243 )?;
3244 let resource_scope = serde_json::to_string(&req.resource_scope)
3245 .map_err(|_| encode_failed("storage file resource scope"))?;
3246 Ok(format!(
3247 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"handle_grant_token\":\"{}\",\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"handle_id\":\"{}\",\"method\":\"{}\",\"resource_scope\":{},\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{},\"operation\":\"{}\",\"store_id\":\"{}\",\"path\":\"{}\",\"data_base64\":\"{}\",\"max_bytes\":{},\"max_entries\":{},\"recursive\":{}}}}}",
3248 RUST_IPC_VERSION,
3249 FRAME_TYPE_STORAGE_FILE,
3250 escape_json_string(request_id),
3251 escape_json_string(runtime_generation_id),
3252 escape_json_string(&req.handle_grant_token),
3253 escape_json_string(&req.plugin_instance_id),
3254 escape_json_string(&req.active_fingerprint),
3255 escape_json_string(&req.runtime_instance_id),
3256 escape_json_string(&req.runtime_generation_id),
3257 escape_json_string(&req.runtime_shard_id),
3258 escape_json_string(&req.handle_id),
3259 escape_json_string(&req.method),
3260 resource_scope,
3261 req.policy_revision,
3262 req.management_revision,
3263 req.revoke_epoch,
3264 escape_json_string(&req.operation),
3265 escape_json_string(&req.store_id),
3266 escape_json_string(&req.path),
3267 escape_json_string(&req.data_base64),
3268 req.max_bytes,
3269 req.max_entries,
3270 if req.recursive { "true" } else { "false" }
3271 ))
3272}
3273
3274pub fn validate_storage_file_response(
3275 input: &str,
3276 expected_request_id: &str,
3277 expected_runtime_generation_id: &str,
3278 expected_operation: &str,
3279) -> IpcResult<()> {
3280 match expected_operation {
3281 "read" => parse_validated_hostcall_success::<StorageFileReadSuccessResponsePayload, _>(
3282 input,
3283 FRAME_TYPE_STORAGE_FILE,
3284 expected_request_id,
3285 expected_runtime_generation_id,
3286 |payload| payload.ok,
3287 )
3288 .map(|_| ()),
3289 "write" => parse_validated_hostcall_success::<StorageFileWriteSuccessResponsePayload, _>(
3290 input,
3291 FRAME_TYPE_STORAGE_FILE,
3292 expected_request_id,
3293 expected_runtime_generation_id,
3294 |payload| payload.ok,
3295 )
3296 .map(|_| ()),
3297 "delete" => parse_validated_hostcall_success::<StorageFileDeleteSuccessResponsePayload, _>(
3298 input,
3299 FRAME_TYPE_STORAGE_FILE,
3300 expected_request_id,
3301 expected_runtime_generation_id,
3302 |payload| payload.ok,
3303 )
3304 .map(|_| ()),
3305 "list" => parse_validated_hostcall_success::<StorageFileListSuccessResponsePayload, _>(
3306 input,
3307 FRAME_TYPE_STORAGE_FILE,
3308 expected_request_id,
3309 expected_runtime_generation_id,
3310 |payload| payload.ok,
3311 )
3312 .map(|_| ()),
3313 _ => Err(invalid_field("storage_file response operation")),
3314 }?;
3315 Ok(())
3316}
3317
3318#[derive(Debug, Clone, PartialEq, Eq)]
3319pub struct StorageKVRequest {
3320 pub handle_grant_token: String,
3321 pub plugin_instance_id: String,
3322 pub active_fingerprint: String,
3323 pub runtime_instance_id: String,
3324 pub runtime_generation_id: String,
3325 pub runtime_shard_id: String,
3326 pub handle_id: String,
3327 pub method: String,
3328 pub resource_scope: NetworkResourceScope,
3329 pub policy_revision: u64,
3330 pub management_revision: u64,
3331 pub revoke_epoch: u64,
3332 pub operation: String,
3333 pub store_id: String,
3334 pub key: String,
3335 pub value_base64: String,
3336 pub prefix: String,
3337 pub max_bytes: u64,
3338 pub max_entries: u64,
3339}
3340
3341pub fn storage_kv_frame(
3342 request_id: &str,
3343 runtime_generation_id: &str,
3344 req: &StorageKVRequest,
3345) -> IpcResult<String> {
3346 if !req.resource_scope.valid() {
3347 return Err(invalid_field("storage kv resource scope"));
3348 }
3349 validate_revision_binding(
3350 req.policy_revision,
3351 req.management_revision,
3352 req.revoke_epoch,
3353 )?;
3354 let resource_scope = serde_json::to_string(&req.resource_scope)
3355 .map_err(|_| encode_failed("storage kv resource scope"))?;
3356 Ok(format!(
3357 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"handle_grant_token\":\"{}\",\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"handle_id\":\"{}\",\"method\":\"{}\",\"resource_scope\":{},\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{},\"operation\":\"{}\",\"store_id\":\"{}\",\"key\":\"{}\",\"value_base64\":\"{}\",\"prefix\":\"{}\",\"max_bytes\":{},\"max_entries\":{}}}}}",
3358 RUST_IPC_VERSION,
3359 FRAME_TYPE_STORAGE_KV,
3360 escape_json_string(request_id),
3361 escape_json_string(runtime_generation_id),
3362 escape_json_string(&req.handle_grant_token),
3363 escape_json_string(&req.plugin_instance_id),
3364 escape_json_string(&req.active_fingerprint),
3365 escape_json_string(&req.runtime_instance_id),
3366 escape_json_string(&req.runtime_generation_id),
3367 escape_json_string(&req.runtime_shard_id),
3368 escape_json_string(&req.handle_id),
3369 escape_json_string(&req.method),
3370 resource_scope,
3371 req.policy_revision,
3372 req.management_revision,
3373 req.revoke_epoch,
3374 escape_json_string(&req.operation),
3375 escape_json_string(&req.store_id),
3376 escape_json_string(&req.key),
3377 escape_json_string(&req.value_base64),
3378 escape_json_string(&req.prefix),
3379 req.max_bytes,
3380 req.max_entries
3381 ))
3382}
3383
3384pub fn validate_storage_kv_response(
3385 input: &str,
3386 expected_request_id: &str,
3387 expected_runtime_generation_id: &str,
3388 expected_operation: &str,
3389) -> IpcResult<()> {
3390 match expected_operation {
3391 "get" => parse_validated_hostcall_success::<StorageKVGetSuccessResponsePayload, _>(
3392 input,
3393 FRAME_TYPE_STORAGE_KV,
3394 expected_request_id,
3395 expected_runtime_generation_id,
3396 |payload| payload.ok,
3397 )
3398 .map(|_| ()),
3399 "put" => parse_validated_hostcall_success::<StorageKVPutSuccessResponsePayload, _>(
3400 input,
3401 FRAME_TYPE_STORAGE_KV,
3402 expected_request_id,
3403 expected_runtime_generation_id,
3404 |payload| payload.ok,
3405 )
3406 .map(|_| ()),
3407 "delete" => parse_validated_hostcall_success::<StorageKVDeleteSuccessResponsePayload, _>(
3408 input,
3409 FRAME_TYPE_STORAGE_KV,
3410 expected_request_id,
3411 expected_runtime_generation_id,
3412 |payload| payload.ok,
3413 )
3414 .map(|_| ()),
3415 "list" => parse_validated_hostcall_success::<StorageKVListSuccessResponsePayload, _>(
3416 input,
3417 FRAME_TYPE_STORAGE_KV,
3418 expected_request_id,
3419 expected_runtime_generation_id,
3420 |payload| payload.ok,
3421 )
3422 .map(|_| ()),
3423 _ => Err(invalid_field("storage_kv response operation")),
3424 }?;
3425 Ok(())
3426}
3427
3428#[derive(Debug, Clone, PartialEq, Eq)]
3429pub struct StorageSQLiteRequest {
3430 pub handle_grant_token: String,
3431 pub plugin_instance_id: String,
3432 pub active_fingerprint: String,
3433 pub runtime_instance_id: String,
3434 pub runtime_generation_id: String,
3435 pub runtime_shard_id: String,
3436 pub handle_id: String,
3437 pub method: String,
3438 pub resource_scope: NetworkResourceScope,
3439 pub policy_revision: u64,
3440 pub management_revision: u64,
3441 pub revoke_epoch: u64,
3442 pub operation: String,
3443 pub store_id: String,
3444 pub database: String,
3445 pub sql: String,
3446 pub args_json: String,
3447 pub max_rows: u64,
3448 pub max_response_bytes: u64,
3449 pub timeout_ms: u64,
3450}
3451
3452pub fn storage_sqlite_frame(
3453 request_id: &str,
3454 runtime_generation_id: &str,
3455 req: &StorageSQLiteRequest,
3456) -> IpcResult<String> {
3457 if !req.resource_scope.valid() {
3458 return Err(invalid_field("storage sqlite resource scope"));
3459 }
3460 validate_revision_binding(
3461 req.policy_revision,
3462 req.management_revision,
3463 req.revoke_epoch,
3464 )?;
3465 let resource_scope = serde_json::to_string(&req.resource_scope)
3466 .map_err(|_| encode_failed("storage sqlite resource scope"))?;
3467 let args_json = if req.args_json.trim().is_empty() {
3468 "[]"
3469 } else {
3470 req.args_json.trim()
3471 };
3472 Ok(format!(
3473 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"handle_grant_token\":\"{}\",\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"handle_id\":\"{}\",\"method\":\"{}\",\"resource_scope\":{},\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{},\"operation\":\"{}\",\"store_id\":\"{}\",\"database\":\"{}\",\"sql\":\"{}\",\"args\":{},\"max_rows\":{},\"max_response_bytes\":{},\"timeout_ms\":{}}}}}",
3474 RUST_IPC_VERSION,
3475 FRAME_TYPE_STORAGE_SQLITE,
3476 escape_json_string(request_id),
3477 escape_json_string(runtime_generation_id),
3478 escape_json_string(&req.handle_grant_token),
3479 escape_json_string(&req.plugin_instance_id),
3480 escape_json_string(&req.active_fingerprint),
3481 escape_json_string(&req.runtime_instance_id),
3482 escape_json_string(&req.runtime_generation_id),
3483 escape_json_string(&req.runtime_shard_id),
3484 escape_json_string(&req.handle_id),
3485 escape_json_string(&req.method),
3486 resource_scope,
3487 req.policy_revision,
3488 req.management_revision,
3489 req.revoke_epoch,
3490 escape_json_string(&req.operation),
3491 escape_json_string(&req.store_id),
3492 escape_json_string(&req.database),
3493 escape_json_string(&req.sql),
3494 args_json,
3495 req.max_rows,
3496 req.max_response_bytes,
3497 req.timeout_ms
3498 ))
3499}
3500
3501pub fn validate_storage_sqlite_response(
3502 input: &str,
3503 expected_request_id: &str,
3504 expected_runtime_generation_id: &str,
3505 expected_operation: &str,
3506) -> IpcResult<()> {
3507 match expected_operation {
3508 "exec" => parse_validated_hostcall_success::<StorageSQLiteExecSuccessResponsePayload, _>(
3509 input,
3510 FRAME_TYPE_STORAGE_SQLITE,
3511 expected_request_id,
3512 expected_runtime_generation_id,
3513 |payload| payload.ok,
3514 )
3515 .map(|_| ()),
3516 "query" => parse_validated_hostcall_success::<StorageSQLiteQuerySuccessResponsePayload, _>(
3517 input,
3518 FRAME_TYPE_STORAGE_SQLITE,
3519 expected_request_id,
3520 expected_runtime_generation_id,
3521 |payload| {
3522 payload.ok
3523 && payload
3524 .rows
3525 .iter()
3526 .flatten()
3527 .all(StorageSQLiteValueResponsePayload::is_exactly_typed)
3528 },
3529 )
3530 .map(|_| ()),
3531 _ => Err(invalid_field("storage_sqlite response operation")),
3532 }?;
3533 Ok(())
3534}
3535
3536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3537#[serde(deny_unknown_fields)]
3538pub struct NetworkResourceScope {
3539 pub kind: String,
3540 pub owner_env_hash: String,
3541 #[serde(default, skip_serializing_if = "String::is_empty")]
3542 pub owner_user_hash: String,
3543}
3544
3545impl NetworkResourceScope {
3546 fn valid(&self) -> bool {
3547 valid_owner_hash(&self.owner_env_hash)
3548 && match self.kind.as_str() {
3549 "user" => valid_owner_hash(&self.owner_user_hash),
3550 "environment" => self.owner_user_hash.is_empty(),
3551 _ => false,
3552 }
3553 }
3554}
3555
3556fn valid_owner_hash(value: &str) -> bool {
3557 let bytes = value.as_bytes();
3558 (1..=256).contains(&bytes.len())
3559 && bytes[0].is_ascii_alphanumeric()
3560 && bytes[1..]
3561 .iter()
3562 .all(|byte| byte.is_ascii_alphanumeric() || matches!(*byte, b'.' | b'_' | b':' | b'-'))
3563}
3564
3565#[derive(Debug, Clone, PartialEq, Eq)]
3566pub struct NetworkGrantRequest {
3567 pub plugin_instance_id: String,
3568 pub active_fingerprint: String,
3569 pub resource_scope: NetworkResourceScope,
3570 pub runtime_instance_id: String,
3571 pub runtime_generation_id: String,
3572 pub runtime_shard_id: String,
3573 pub policy_revision: u64,
3574 pub management_revision: u64,
3575 pub revoke_epoch: u64,
3576 pub connector_id: String,
3577 pub transport: String,
3578 pub destination: String,
3579 pub ttl_ms: u64,
3580}
3581
3582pub fn network_grant_frame(
3583 request_id: &str,
3584 runtime_generation_id: &str,
3585 req: &NetworkGrantRequest,
3586) -> IpcResult<String> {
3587 if !req.resource_scope.valid() {
3588 return Err(invalid_field("network resource scope"));
3589 }
3590 validate_revision_binding(
3591 req.policy_revision,
3592 req.management_revision,
3593 req.revoke_epoch,
3594 )?;
3595 let resource_scope = serde_json::to_string(&req.resource_scope)
3596 .map_err(|_| encode_failed("network resource scope"))?;
3597 Ok(format!(
3598 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"resource_scope\":{},\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{},\"connector_id\":\"{}\",\"transport\":\"{}\",\"destination\":\"{}\",\"ttl_ms\":{}}}}}",
3599 RUST_IPC_VERSION,
3600 FRAME_TYPE_NETWORK_GRANT,
3601 escape_json_string(request_id),
3602 escape_json_string(runtime_generation_id),
3603 escape_json_string(&req.plugin_instance_id),
3604 escape_json_string(&req.active_fingerprint),
3605 resource_scope,
3606 escape_json_string(&req.runtime_instance_id),
3607 escape_json_string(&req.runtime_generation_id),
3608 escape_json_string(&req.runtime_shard_id),
3609 req.policy_revision,
3610 req.management_revision,
3611 req.revoke_epoch,
3612 escape_json_string(&req.connector_id),
3613 escape_json_string(&req.transport),
3614 escape_json_string(&req.destination),
3615 req.ttl_ms
3616 ))
3617}
3618
3619pub fn validate_network_grant_response(
3620 input: &str,
3621 expected_request_id: &str,
3622 expected_runtime_generation_id: &str,
3623 expected_connector_id: &str,
3624 expected_transport: &str,
3625 expected_resource_scope: &NetworkResourceScope,
3626) -> IpcResult<()> {
3627 let success = parse_validated_hostcall_success::<NetworkGrantSuccessResponsePayload, _>(
3628 input,
3629 FRAME_TYPE_NETWORK_GRANT,
3630 expected_request_id,
3631 expected_runtime_generation_id,
3632 |payload| payload.ok,
3633 )?;
3634 let grant_suffix = success.grant_id.strip_prefix("netgrant_");
3635 if grant_suffix.is_none_or(|suffix| {
3636 suffix.len() != 32 || !suffix.bytes().all(|byte| byte.is_ascii_hexdigit())
3637 }) {
3638 return Err(invalid_field("network grant id"));
3639 }
3640 if success.connector_id != expected_connector_id || success.transport != expected_transport {
3641 return Err(protocol_violation("network_grant audience mismatch"));
3642 }
3643 if !success.resource_scope.valid() || success.resource_scope != *expected_resource_scope {
3644 return Err(protocol_violation("network_grant resource scope mismatch"));
3645 }
3646 validate_revision_binding(
3647 success.policy_revision,
3648 success.management_revision,
3649 success.revoke_epoch,
3650 )?;
3651 if success.runtime_generation_id != expected_runtime_generation_id {
3652 return Err(protocol_violation(
3653 "network_grant payload runtime_generation_id mismatch",
3654 ));
3655 }
3656 if success.target_classifier_version != "target-classifier-v2" {
3657 return Err(protocol_violation(
3658 "network_grant target classifier version mismatch",
3659 ));
3660 }
3661 Ok(())
3662}
3663
3664#[derive(Debug, Clone, PartialEq, Eq)]
3665pub struct NetworkExecuteRequest {
3666 pub plugin_id: String,
3667 pub plugin_instance_id: String,
3668 pub active_fingerprint: String,
3669 pub resource_scope: NetworkResourceScope,
3670 pub runtime_instance_id: String,
3671 pub runtime_generation_id: String,
3672 pub runtime_shard_id: String,
3673 pub policy_revision: u64,
3674 pub management_revision: u64,
3675 pub revoke_epoch: u64,
3676 pub connector_id: String,
3677 pub transport: String,
3678 pub destination: String,
3679 pub ttl_ms: u64,
3680 pub operation: String,
3681 pub method: String,
3682 pub path: String,
3683 pub query_json: String,
3684 pub headers_json: String,
3685 pub message_type: String,
3686 pub body_base64: String,
3687 pub payload_base64: String,
3688 pub max_request_bytes: u64,
3689 pub max_response_bytes: u64,
3690 pub max_chunk_bytes: u64,
3691 pub max_buffered_bytes: u64,
3692 pub timeout_ms: u64,
3693 pub stream_id: String,
3694 pub stream_method: String,
3695 pub stream_effect: String,
3696 pub stream_execution: String,
3697 pub surface_instance_id: String,
3698 pub owner_session_hash: String,
3699 pub owner_user_hash: String,
3700 pub owner_env_hash: String,
3701 pub session_channel_id_hash: String,
3702 pub bridge_channel_id: String,
3703 pub content_type: String,
3704}
3705
3706pub fn network_execute_frame(
3707 request_id: &str,
3708 runtime_generation_id: &str,
3709 req: &NetworkExecuteRequest,
3710) -> IpcResult<String> {
3711 if !req.resource_scope.valid() {
3712 return Err(invalid_field("network resource scope"));
3713 }
3714 validate_revision_binding(
3715 req.policy_revision,
3716 req.management_revision,
3717 req.revoke_epoch,
3718 )?;
3719 let query_json = if req.query_json.trim().is_empty() {
3720 "{}"
3721 } else {
3722 req.query_json.trim()
3723 };
3724 let headers_json = if req.headers_json.trim().is_empty() {
3725 "{}"
3726 } else {
3727 req.headers_json.trim()
3728 };
3729 let query: serde_json::Value =
3730 serde_json::from_str(query_json).map_err(|_| invalid_field("network execute query"))?;
3731 if !query.is_object() {
3732 return Err(invalid_field("network execute query"));
3733 }
3734 let headers: serde_json::Value =
3735 serde_json::from_str(headers_json).map_err(|_| invalid_field("network execute headers"))?;
3736 if !headers.is_object() {
3737 return Err(invalid_field("network execute headers"));
3738 }
3739 let resource_scope = serde_json::to_string(&req.resource_scope)
3740 .map_err(|_| encode_failed("network resource scope"))?;
3741 Ok(format!(
3742 "{{\"ipc_version\":\"{}\",\"frame_type\":\"{}\",\"request_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"payload\":{{\"plugin_id\":\"{}\",\"plugin_instance_id\":\"{}\",\"active_fingerprint\":\"{}\",\"resource_scope\":{},\"runtime_instance_id\":\"{}\",\"runtime_generation_id\":\"{}\",\"runtime_shard_id\":\"{}\",\"policy_revision\":{},\"management_revision\":{},\"revoke_epoch\":{},\"connector_id\":\"{}\",\"transport\":\"{}\",\"destination\":\"{}\",\"ttl_ms\":{},\"operation\":\"{}\",\"method\":\"{}\",\"path\":\"{}\",\"query\":{},\"headers\":{},\"message_type\":\"{}\",\"body_base64\":\"{}\",\"payload_base64\":\"{}\",\"max_request_bytes\":{},\"max_response_bytes\":{},\"max_chunk_bytes\":{},\"max_buffered_bytes\":{},\"timeout_ms\":{},\"stream_id\":\"{}\",\"stream_method\":\"{}\",\"stream_effect\":\"{}\",\"stream_execution\":\"{}\",\"surface_instance_id\":\"{}\",\"owner_session_hash\":\"{}\",\"owner_user_hash\":\"{}\",\"owner_env_hash\":\"{}\",\"session_channel_id_hash\":\"{}\",\"bridge_channel_id\":\"{}\",\"content_type\":\"{}\"}}}}",
3743 RUST_IPC_VERSION,
3744 FRAME_TYPE_NETWORK_EXECUTE,
3745 escape_json_string(request_id),
3746 escape_json_string(runtime_generation_id),
3747 escape_json_string(&req.plugin_id),
3748 escape_json_string(&req.plugin_instance_id),
3749 escape_json_string(&req.active_fingerprint),
3750 resource_scope,
3751 escape_json_string(&req.runtime_instance_id),
3752 escape_json_string(&req.runtime_generation_id),
3753 escape_json_string(&req.runtime_shard_id),
3754 req.policy_revision,
3755 req.management_revision,
3756 req.revoke_epoch,
3757 escape_json_string(&req.connector_id),
3758 escape_json_string(&req.transport),
3759 escape_json_string(&req.destination),
3760 req.ttl_ms,
3761 escape_json_string(&req.operation),
3762 escape_json_string(&req.method),
3763 escape_json_string(&req.path),
3764 query_json,
3765 headers_json,
3766 escape_json_string(&req.message_type),
3767 escape_json_string(&req.body_base64),
3768 escape_json_string(&req.payload_base64),
3769 req.max_request_bytes,
3770 req.max_response_bytes,
3771 req.max_chunk_bytes,
3772 req.max_buffered_bytes,
3773 req.timeout_ms,
3774 escape_json_string(&req.stream_id),
3775 escape_json_string(&req.stream_method),
3776 escape_json_string(&req.stream_effect),
3777 escape_json_string(&req.stream_execution),
3778 escape_json_string(&req.surface_instance_id),
3779 escape_json_string(&req.owner_session_hash),
3780 escape_json_string(&req.owner_user_hash),
3781 escape_json_string(&req.owner_env_hash),
3782 escape_json_string(&req.session_channel_id_hash),
3783 escape_json_string(&req.bridge_channel_id),
3784 escape_json_string(&req.content_type)
3785 ))
3786}
3787
3788pub fn validate_network_execute_response(
3789 input: &str,
3790 expected_request_id: &str,
3791 expected_runtime_generation_id: &str,
3792 expected_connector_id: &str,
3793 expected_transport: &str,
3794) -> IpcResult<()> {
3795 let success = parse_validated_hostcall_success::<NetworkExecuteSuccessResponsePayload, _>(
3796 input,
3797 FRAME_TYPE_NETWORK_EXECUTE,
3798 expected_request_id,
3799 expected_runtime_generation_id,
3800 |payload| payload.ok,
3801 )?;
3802 if success.connector_id != expected_connector_id || success.transport != expected_transport {
3803 return Err(protocol_violation("network_execute audience mismatch"));
3804 }
3805 if success.runtime_generation_id != expected_runtime_generation_id {
3806 return Err(protocol_violation(
3807 "network_execute payload runtime_generation_id mismatch",
3808 ));
3809 }
3810 Ok(())
3811}
3812
3813pub fn validate_hello_frame(input: &str) -> IpcResult<(String, String, String)> {
3814 let parsed = parse_hello_frame(input)?;
3815 Ok((
3816 parsed.request_id,
3817 parsed.runtime_generation_id,
3818 parsed.channel_nonce,
3819 ))
3820}
3821
3822pub fn parse_hello_frame(input: &str) -> IpcResult<HelloFrame> {
3823 let frame: RawIPCFrame = serde_json::from_str(input).map_err(|err| {
3824 if err.to_string().contains("missing field `request_id`") {
3825 missing_field("request_id")
3826 } else if err
3827 .to_string()
3828 .contains("missing field `runtime_generation_id`")
3829 {
3830 missing_field("runtime_generation_id")
3831 } else {
3832 decode_failed("hello frame")
3833 }
3834 })?;
3835 if frame.ipc_version != RUST_IPC_VERSION {
3836 return Err(protocol_violation("unsupported ipc_version"));
3837 }
3838 if frame.frame_type != FRAME_TYPE_HELLO {
3839 return Err(protocol_violation("expected hello frame"));
3840 }
3841 if frame.request_id.trim().is_empty() {
3842 return Err(invalid_field("request_id"));
3843 }
3844 let runtime_generation_id = frame
3845 .runtime_generation_id
3846 .as_deref()
3847 .ok_or_else(|| missing_field("runtime_generation_id"))?;
3848 if runtime_generation_id.trim().is_empty() {
3849 return Err(invalid_field("runtime_generation_id"));
3850 }
3851 let payload: HelloPayload = serde_json::from_str(frame.payload.get()).map_err(|err| {
3852 if err.to_string().contains("missing field `channel_nonce`") {
3853 missing_field("channel_nonce")
3854 } else {
3855 decode_failed("hello payload")
3856 }
3857 })?;
3858 let target =
3859 RuntimeTarget::parse(&payload.target).map_err(|_| invalid_field("hello target"))?;
3860 if payload.host_process_id == 0 || payload.started_unix_nano == 0 {
3861 return Err(invalid_field("hello process metadata"));
3862 }
3863 if payload.host_ipc_version != RUST_IPC_VERSION {
3864 return Err(protocol_violation("unsupported host_ipc_version"));
3865 }
3866 if payload.host_wasm_abi != WASM_ABI_VERSION {
3867 return Err(protocol_violation("unsupported host_wasm_abi"));
3868 }
3869 if payload.contract_set_sha256 != CONTRACT_SET_SHA256 {
3870 return Err(protocol_violation("contract_set_sha256 mismatch"));
3871 }
3872 if payload.channel_nonce.trim().is_empty() {
3873 return Err(invalid_field("channel_nonce"));
3874 }
3875 let limits = payload.limits.validate()?;
3876 let runtime_lease_public_keys =
3877 parse_runtime_lease_public_key_payloads(payload.runtime_lease_public_keys)?;
3878 Ok(HelloFrame {
3879 request_id: frame.request_id,
3880 runtime_generation_id: runtime_generation_id.to_string(),
3881 target,
3882 contract_set_sha256: payload.contract_set_sha256,
3883 channel_nonce: payload.channel_nonce,
3884 runtime_lease_public_keys,
3885 limits,
3886 })
3887}
3888
3889pub fn parse_frame_identity(input: &str) -> IpcResult<FrameIdentity> {
3890 let frame: RawIPCFrame = serde_json::from_str(input).map_err(|err| {
3891 let message = err.to_string();
3892 if message.contains("missing field `ipc_version`") {
3893 missing_field("ipc_version")
3894 } else if message.contains("missing field `frame_type`") {
3895 missing_field("frame_type")
3896 } else if message.contains("missing field `request_id`") {
3897 missing_field("request_id")
3898 } else if message.contains("missing field `runtime_generation_id`") {
3899 missing_field("runtime_generation_id")
3900 } else if message.contains("missing field `payload`") {
3901 missing_field("payload")
3902 } else {
3903 decode_failed("IPC frame")
3904 }
3905 })?;
3906 validated_frame_identity(&frame)
3907}
3908
3909fn validated_frame_identity(frame: &RawIPCFrame) -> IpcResult<FrameIdentity> {
3910 if frame.ipc_version != RUST_IPC_VERSION {
3911 return Err(protocol_violation("unsupported ipc_version"));
3912 }
3913 if frame.frame_type.trim().is_empty() {
3914 return Err(invalid_field("frame_type"));
3915 }
3916 if frame.request_id.trim().is_empty() {
3917 return Err(invalid_field("request_id"));
3918 }
3919 let runtime_generation_id = frame
3920 .runtime_generation_id
3921 .as_deref()
3922 .ok_or_else(|| missing_field("runtime_generation_id"))?;
3923 if runtime_generation_id.trim().is_empty() {
3924 return Err(invalid_field("runtime_generation_id"));
3925 }
3926 if frame
3927 .parent_request_id
3928 .as_deref()
3929 .is_some_and(|value| value.trim().is_empty())
3930 {
3931 return Err(invalid_field("parent_request_id"));
3932 }
3933 Ok(FrameIdentity {
3934 frame_type: frame.frame_type.clone(),
3935 request_id: frame.request_id.clone(),
3936 parent_request_id: frame.parent_request_id.clone(),
3937 runtime_generation_id: runtime_generation_id.to_string(),
3938 })
3939}
3940
3941pub fn decode_runtime_input_frame(input: &str) -> IpcResult<RuntimeInputFrame> {
3942 let frame = parse_raw_frame(input)?;
3943 let identity = validated_frame_identity(&frame)?;
3944 match identity.frame_type.as_str() {
3945 FRAME_TYPE_INVOKE_WORKER => {
3946 let invocation = parsed_worker_invocation(&identity, frame.payload.as_ref());
3947 Ok(RuntimeInputFrame::InvokeWorker(Box::new(
3948 WorkerInvocationInput {
3949 identity,
3950 invocation,
3951 },
3952 )))
3953 }
3954 FRAME_TYPE_CANCEL_INVOKE => {
3955 if identity.parent_request_id.is_some() {
3956 return Err(protocol_violation(
3957 "cancel_invoke must not have parent_request_id",
3958 ));
3959 }
3960 let payload: CancelInvokePayload = serde_json::from_str(frame.payload.get())
3961 .map_err(|_| decode_failed("cancel_invoke payload"))?;
3962 if payload.invocation_request_id.trim().is_empty() {
3963 return Err(invalid_field("cancel invocation_request_id"));
3964 }
3965 Ok(RuntimeInputFrame::CancelInvoke(CancelInvocationInput {
3966 identity,
3967 invocation_request_id: payload.invocation_request_id,
3968 }))
3969 }
3970 FRAME_TYPE_OPEN_HANDLE
3971 | FRAME_TYPE_VALIDATE_HANDLE_GRANT
3972 | FRAME_TYPE_STORAGE_FILE
3973 | FRAME_TYPE_STORAGE_KV
3974 | FRAME_TYPE_STORAGE_SQLITE
3975 | FRAME_TYPE_NETWORK_GRANT
3976 | FRAME_TYPE_NETWORK_EXECUTE => {
3977 if identity.parent_request_id.is_none() {
3978 return Err(missing_field("runtime hostcall response parent_request_id"));
3979 }
3980 Ok(RuntimeInputFrame::HostcallResponse(
3981 RuntimeHostcallResponseInput {
3982 identity,
3983 raw_frame: input.to_string(),
3984 },
3985 ))
3986 }
3987 _ => Ok(RuntimeInputFrame::Unsupported(identity)),
3988 }
3989}
3990
3991#[derive(Debug, Clone, PartialEq, Eq)]
3992pub struct WorkerInvocationIdentity {
3993 pub package_hash: String,
3994 pub artifact: String,
3995 pub artifact_sha256: String,
3996 pub worker_id: String,
3997 pub method: String,
3998}
3999
4000#[derive(Debug, Clone, PartialEq, Eq)]
4001pub enum WorkerResponseV2 {
4002 Success(String),
4003 Failure { code: String, message: String },
4004}
4005
4006#[derive(Deserialize)]
4007#[serde(deny_unknown_fields)]
4008struct RawWorkerResponseV2<'a> {
4009 ok: bool,
4010 #[serde(borrow)]
4011 data: Option<&'a serde_json::value::RawValue>,
4012 error_code: Option<String>,
4013 message: Option<String>,
4014}
4015
4016pub fn worker_request_json_v2(input: &str) -> IpcResult<String> {
4017 parse_worker_invocation(input)?.worker_request_json_v2()
4018}
4019
4020pub fn runtime_lease_memory_limit_bytes(input: &str) -> IpcResult<usize> {
4021 parse_worker_invocation(input)?.memory_limit_bytes()
4022}
4023
4024pub fn worker_storage_handle_grant(input: &str, store_id: &str) -> IpcResult<String> {
4025 parse_worker_invocation(input)?.storage_handle_grant(store_id)
4026}
4027
4028pub fn validate_worker_storage_broker_access(
4029 input: &str,
4030 store_id: &str,
4031 operation: &str,
4032) -> IpcResult<()> {
4033 parse_worker_invocation(input)?.validate_storage_broker_access(store_id, operation)
4034}
4035
4036pub fn validate_worker_network_broker_access(
4037 input: &str,
4038 connector_id: &str,
4039 transport: &str,
4040 operation: &str,
4041 http_method: &str,
4042) -> IpcResult<()> {
4043 parse_worker_invocation(input)?.validate_network_broker_access(
4044 connector_id,
4045 transport,
4046 operation,
4047 http_method,
4048 )
4049}
4050
4051pub fn parse_worker_response_v2(input: &str) -> IpcResult<WorkerResponseV2> {
4052 let response: RawWorkerResponseV2<'_> =
4053 serde_json::from_str(input).map_err(|_| decode_failed("worker response"))?;
4054 if response.ok {
4055 if response.error_code.is_some() || response.message.is_some() {
4056 return Err(protocol_violation(
4057 "worker success response contains failure fields",
4058 ));
4059 }
4060 let data = response
4061 .data
4062 .ok_or_else(|| missing_field("worker success response data"))?;
4063 return Ok(WorkerResponseV2::Success(data.get().to_string()));
4064 }
4065 if response.data.is_some() {
4066 return Err(protocol_violation(
4067 "worker failure response contains success data",
4068 ));
4069 }
4070 let error_code = response
4071 .error_code
4072 .ok_or_else(|| missing_field("worker failure response error_code"))?;
4073 let message = response
4074 .message
4075 .ok_or_else(|| missing_field("worker failure response message"))?;
4076 if !is_stable_worker_error_code(&error_code) {
4077 return Err(invalid_field("worker failure response error_code"));
4078 }
4079 if message.trim().is_empty() || message.len() > 4096 {
4080 return Err(invalid_field("worker failure response message"));
4081 }
4082 Ok(WorkerResponseV2::Failure {
4083 code: error_code,
4084 message,
4085 })
4086}
4087
4088fn is_stable_worker_error_code(value: &str) -> bool {
4089 !value.is_empty()
4090 && value.len() <= 128
4091 && value.chars().enumerate().all(|(index, ch)| {
4092 ch.is_ascii_uppercase() || ch.is_ascii_digit() || (index > 0 && ch == '_')
4093 })
4094 && value
4095 .chars()
4096 .next()
4097 .is_some_and(|ch| ch.is_ascii_uppercase())
4098}
4099
4100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
4101pub struct WorkerLeaseReplayKey {
4102 pub lease_id: String,
4103 pub lease_nonce: String,
4104 pub expires_at_unix_ms: i64,
4105}
4106
4107pub fn parse_worker_lease_replay_key(input: &str) -> IpcResult<WorkerLeaseReplayKey> {
4108 parse_worker_invocation(input)?.replay_key()
4109}
4110
4111pub fn parse_worker_invocation_identity(input: &str) -> IpcResult<WorkerInvocationIdentity> {
4112 parse_worker_invocation(input)?.identity()
4113}
4114
4115pub fn worker_invocation_not_implemented_message(identity: &WorkerInvocationIdentity) -> String {
4116 format!(
4117 "runtime worker execution is not implemented for {}:{}",
4118 identity.worker_id, identity.method
4119 )
4120}
4121
4122pub fn validate_worker_artifact_bytes(
4123 identity: &WorkerInvocationIdentity,
4124 content: &[u8],
4125) -> IpcResult<()> {
4126 let actual = format!("sha256:{}", lowercase_hex(&Sha256::digest(content)));
4127 if actual != identity.artifact_sha256 {
4128 return Err(protocol_violation(
4129 "worker artifact content does not match artifact_sha256",
4130 ));
4131 }
4132 Ok(())
4133}
4134
4135fn is_sha256_ref(value: &str) -> bool {
4136 let Some(hex) = value.strip_prefix("sha256:") else {
4137 return false;
4138 };
4139 hex.len() == 64
4140 && hex
4141 .chars()
4142 .all(|ch| ch.is_ascii_hexdigit() && !ch.is_ascii_uppercase())
4143}
4144
4145fn is_worker_artifact_path(value: &str) -> bool {
4146 if !value.starts_with("workers/") || !value.ends_with(".wasm") {
4147 return false;
4148 }
4149 if value.contains('\\') || value.contains("//") {
4150 return false;
4151 }
4152 value
4153 .split('/')
4154 .all(|part| !part.is_empty() && part != "." && part != "..")
4155}
4156
4157#[cfg(test)]
4158mod tests {
4159 use super::*;
4160 use ed25519_dalek::{Signer, SigningKey};
4161 use serde_json::Value;
4162 use std::fs;
4163 use std::path::PathBuf;
4164
4165 fn runtime_limits() -> RuntimeLimits {
4166 RuntimeLimits {
4167 worker_count: 8,
4168 queue_capacity: 32,
4169 per_plugin_concurrency: 4,
4170 module_cache_entries: 64,
4171 module_cache_source_bytes: 128 * 1024 * 1024,
4172 }
4173 }
4174
4175 fn invalid_runtime_limits() -> RuntimeLimits {
4176 RuntimeLimits {
4177 worker_count: 1,
4178 per_plugin_concurrency: 2,
4179 ..runtime_limits()
4180 }
4181 }
4182
4183 fn process_containment() -> ProcessContainmentEvidence {
4184 ProcessContainmentEvidence {
4185 schema_version: "redevplugin.process_containment.v1".to_string(),
4186 profile: "linux-runtime-v1".to_string(),
4187 seccomp_policy_sha256:
4188 "6305735925c1fbacaf4950df2e535d3a11cebec8ab7aa16ce37fca3c31745543".to_string(),
4189 no_new_privs: true,
4190 seccomp_tsync: true,
4191 process_creation_denied: true,
4192 reexec_denied: true,
4193 active: true,
4194 }
4195 }
4196
4197 #[test]
4198 fn runtime_limit_constants_match_the_ipc_schema() {
4199 let schema: Value = serde_json::from_str(contract_fixture(
4200 redevplugin_contracts::ContractId::RUST_IPC_SCHEMA,
4201 ))
4202 .expect("IPC schema");
4203 let properties = schema["$defs"]["runtime_limits"]["properties"]
4204 .as_object()
4205 .expect("runtime limit properties");
4206 for (field, minimum, maximum) in [
4207 (
4208 "worker_count",
4209 MIN_RUNTIME_WORKER_COUNT,
4210 MAX_RUNTIME_WORKER_COUNT,
4211 ),
4212 (
4213 "queue_capacity",
4214 MIN_RUNTIME_QUEUE_CAPACITY,
4215 MAX_RUNTIME_QUEUE_CAPACITY,
4216 ),
4217 (
4218 "per_plugin_concurrency",
4219 MIN_RUNTIME_PER_PLUGIN_CONCURRENCY,
4220 MAX_RUNTIME_PER_PLUGIN_CONCURRENCY,
4221 ),
4222 (
4223 "module_cache_entries",
4224 MIN_RUNTIME_MODULE_CACHE_ENTRIES,
4225 MAX_RUNTIME_MODULE_CACHE_ENTRIES,
4226 ),
4227 (
4228 "module_cache_source_bytes",
4229 MIN_RUNTIME_MODULE_CACHE_SOURCE_BYTES,
4230 MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES,
4231 ),
4232 ] {
4233 let property = properties.get(field).expect("runtime limit property");
4234 assert_eq!(property["minimum"].as_u64(), Some(minimum as u64));
4235 assert_eq!(property["maximum"].as_u64(), Some(maximum as u64));
4236 }
4237 }
4238
4239 #[test]
4240 fn runtime_limits_enforce_all_platform_bounds() {
4241 RuntimeLimits {
4242 worker_count: MAX_RUNTIME_WORKER_COUNT,
4243 queue_capacity: MAX_RUNTIME_QUEUE_CAPACITY,
4244 per_plugin_concurrency: MAX_RUNTIME_PER_PLUGIN_CONCURRENCY,
4245 module_cache_entries: MAX_RUNTIME_MODULE_CACHE_ENTRIES,
4246 module_cache_source_bytes: MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES,
4247 }
4248 .validate()
4249 .expect("maximum runtime limits");
4250
4251 for invalid in [
4252 RuntimeLimits {
4253 worker_count: 0,
4254 queue_capacity: 0,
4255 per_plugin_concurrency: 0,
4256 module_cache_entries: 0,
4257 module_cache_source_bytes: 0,
4258 },
4259 RuntimeLimits {
4260 worker_count: MAX_RUNTIME_WORKER_COUNT + 1,
4261 ..runtime_limits()
4262 },
4263 RuntimeLimits {
4264 queue_capacity: MAX_RUNTIME_QUEUE_CAPACITY + 1,
4265 ..runtime_limits()
4266 },
4267 RuntimeLimits {
4268 per_plugin_concurrency: MAX_RUNTIME_PER_PLUGIN_CONCURRENCY + 1,
4269 ..runtime_limits()
4270 },
4271 invalid_runtime_limits(),
4272 RuntimeLimits {
4273 module_cache_entries: MAX_RUNTIME_MODULE_CACHE_ENTRIES + 1,
4274 ..runtime_limits()
4275 },
4276 RuntimeLimits {
4277 module_cache_source_bytes: MAX_RUNTIME_MODULE_CACHE_SOURCE_BYTES + 1,
4278 ..runtime_limits()
4279 },
4280 ] {
4281 assert!(matches!(
4282 invalid.validate(),
4283 Err(IpcError::ProtocolViolation { .. })
4284 ));
4285 }
4286 }
4287
4288 fn user_resource_scope() -> NetworkResourceScope {
4289 NetworkResourceScope {
4290 kind: "user".to_string(),
4291 owner_env_hash: "env_hash".to_string(),
4292 owner_user_hash: "user_hash".to_string(),
4293 }
4294 }
4295
4296 fn environment_resource_scope() -> NetworkResourceScope {
4297 NetworkResourceScope {
4298 kind: "environment".to_string(),
4299 owner_env_hash: "env_hash".to_string(),
4300 owner_user_hash: String::new(),
4301 }
4302 }
4303
4304 fn handle_grant_validation_request() -> HandleGrantValidationRequest {
4305 HandleGrantValidationRequest {
4306 handle_grant_token: "handle_grant.secret".to_string(),
4307 plugin_instance_id: "plugini_1".to_string(),
4308 active_fingerprint: "sha256:active".to_string(),
4309 runtime_instance_id: "runtime_1".to_string(),
4310 runtime_generation_id: "g1".to_string(),
4311 runtime_shard_id: "runtime_shard_1".to_string(),
4312 owner_session_hash: "session_hash".to_string(),
4313 owner_user_hash: "user_hash".to_string(),
4314 owner_env_hash: "env_hash".to_string(),
4315 session_channel_id_hash: "channel_hash".to_string(),
4316 handle_id: "storage:db".to_string(),
4317 method: "storage.sqlite".to_string(),
4318 resource_scope: user_resource_scope(),
4319 policy_revision: 1,
4320 management_revision: 2,
4321 revoke_epoch: 3,
4322 }
4323 }
4324
4325 #[test]
4326 fn resource_scopes_match_the_closed_owner_hash_contract() {
4327 let maximum_hash = format!("a{}", "b".repeat(255));
4328 for valid in [
4329 NetworkResourceScope {
4330 kind: "user".to_string(),
4331 owner_env_hash: maximum_hash.clone(),
4332 owner_user_hash: "user.hash:_-1".to_string(),
4333 },
4334 NetworkResourceScope {
4335 kind: "environment".to_string(),
4336 owner_env_hash: maximum_hash,
4337 owner_user_hash: String::new(),
4338 },
4339 ] {
4340 assert!(valid.valid(), "valid resource scope rejected: {valid:?}");
4341 }
4342
4343 for invalid in [
4344 NetworkResourceScope {
4345 kind: "user".to_string(),
4346 owner_env_hash: String::new(),
4347 owner_user_hash: "user_hash".to_string(),
4348 },
4349 NetworkResourceScope {
4350 kind: "user".to_string(),
4351 owner_env_hash: " env_hash".to_string(),
4352 owner_user_hash: "user_hash".to_string(),
4353 },
4354 NetworkResourceScope {
4355 kind: "user".to_string(),
4356 owner_env_hash: "env/hash".to_string(),
4357 owner_user_hash: "user_hash".to_string(),
4358 },
4359 NetworkResourceScope {
4360 kind: "user".to_string(),
4361 owner_env_hash: "env_hash".to_string(),
4362 owner_user_hash: "user_hash ".to_string(),
4363 },
4364 NetworkResourceScope {
4365 kind: "user".to_string(),
4366 owner_env_hash: "env_hash".to_string(),
4367 owner_user_hash: String::new(),
4368 },
4369 NetworkResourceScope {
4370 kind: "environment".to_string(),
4371 owner_env_hash: "env_hash".to_string(),
4372 owner_user_hash: " ".to_string(),
4373 },
4374 NetworkResourceScope {
4375 kind: "environment".to_string(),
4376 owner_env_hash: "a".repeat(257),
4377 owner_user_hash: String::new(),
4378 },
4379 NetworkResourceScope {
4380 kind: "environment".to_string(),
4381 owner_env_hash: "\u{73af}\u{5883}".to_string(),
4382 owner_user_hash: String::new(),
4383 },
4384 ] {
4385 assert!(
4386 !invalid.valid(),
4387 "invalid resource scope accepted: {invalid:?}"
4388 );
4389 }
4390 }
4391
4392 fn closed_worker_frame(lease: &str, invocation: &str) -> String {
4393 format!(
4394 r#"{{"ipc_version":"rust-ipc-v6","frame_type":"invoke_worker","request_id":"r1","runtime_generation_id":"g1","payload":{{"lease":{lease},"method":"worker.echo","invocation":{invocation}}}}}"#
4395 )
4396 }
4397
4398 fn worker_lease_from_value(value: &serde_json::Value) -> WorkerLeasePayload {
4399 serde_json::from_value(value.clone()).expect("typed worker lease")
4400 }
4401
4402 fn worker_lease_from_object(
4403 value: &serde_json::Map<String, serde_json::Value>,
4404 ) -> WorkerLeasePayload {
4405 worker_lease_from_value(&serde_json::Value::Object(value.clone()))
4406 }
4407
4408 fn hello_frame(channel_nonce: Option<&str>, public_keys: &str) -> String {
4409 let channel_nonce = channel_nonce
4410 .map(|value| format!(",\"channel_nonce\":\"{value}\""))
4411 .unwrap_or_default();
4412 format!(
4413 r#"{{"ipc_version":"rust-ipc-v6","frame_type":"hello","request_id":"r1","runtime_generation_id":"g1","payload":{{"target":"linux/amd64","host_process_id":1,"host_ipc_version":"rust-ipc-v6","host_wasm_abi":"redevplugin-wasm-worker-v2","contract_set_sha256":"{CONTRACT_SET_SHA256}","started_unix_nano":1{channel_nonce},"runtime_lease_public_keys":{public_keys},"limits":{{"worker_count":8,"queue_capacity":32,"per_plugin_concurrency":4,"module_cache_entries":64,"module_cache_source_bytes":134217728}}}}}}"#
4414 )
4415 }
4416
4417 fn hostcall_response_frame(frame_type: &str, payload: &str) -> String {
4418 format!(
4419 r#"{{"ipc_version":"rust-ipc-v6","frame_type":"{frame_type}","request_id":"r1","runtime_generation_id":"g1","payload":{payload}}}"#
4420 )
4421 }
4422
4423 fn without_payload_field(frame: &str, field: &str) -> String {
4424 let mut value: Value = serde_json::from_str(frame).expect("hostcall response frame");
4425 value["payload"]
4426 .as_object_mut()
4427 .expect("hostcall response payload")
4428 .remove(field)
4429 .unwrap_or_else(|| panic!("hostcall response payload missing {field}"));
4430 serde_json::to_string(&value).expect("hostcall response json")
4431 }
4432
4433 fn validate_test_hostcall_response<T: DeserializeOwned>(
4434 frame_type: &'static str,
4435 payload: &str,
4436 ) -> IpcResult<()> {
4437 let frame = hostcall_response_frame(frame_type, payload);
4438 let (_, response) = parse_hostcall_response_frame::<T>(&frame, frame_type)?;
4439 match response {
4440 HostcallResponsePayload::Success(_) => Ok(()),
4441 HostcallResponsePayload::Failure(failure) => {
4442 validated_hostcall_failure(failure).map(|_| ())
4443 }
4444 }
4445 }
4446
4447 fn assert_closed_hostcall_response_union<T: DeserializeOwned>(
4448 frame_type: &'static str,
4449 success_payload: &str,
4450 success_field: &str,
4451 ) {
4452 validate_test_hostcall_response::<T>(frame_type, success_payload)
4453 .unwrap_or_else(|err| panic!("valid {frame_type} success response: {err}"));
4454
4455 let success_prefix = success_payload
4456 .strip_suffix('}')
4457 .expect("success response object");
4458 let duplicate_success_field = success_payload.replacen(
4459 success_field,
4460 &format!("{success_field},{success_field}"),
4461 1,
4462 );
4463 let failure =
4464 r#"{"ok":false,"code":"HOSTCALL_FAILED","message":"failed","error_origin":"hostcall"}"#;
4465 let failure_prefix = failure.strip_suffix('}').expect("failure response object");
4466 let invalid = [
4467 format!(r#"{success_prefix},"future":true}}"#),
4468 success_payload.replacen(r#""ok":true"#, r#""ok":true,"ok":false"#, 1),
4469 success_payload.replacen(r#""ok":true"#, r#""ok":true,"OK":false"#, 1),
4470 duplicate_success_field,
4471 format!(r#"{success_prefix},"code":"HOSTCALL_FAILED"}}"#),
4472 format!(r#"{failure_prefix},{success_field}}}"#),
4473 format!(r#"{failure_prefix},"future":true}}"#),
4474 r#"{"ok":false,"code":"HOSTCALL_FAILED","message":"failed"}"#.to_string(),
4475 r#"{"ok":false,"code":"HOSTCALL_FAILED","message":"failed","error_origin":"runtime"}"#
4476 .to_string(),
4477 ];
4478 for payload in invalid {
4479 assert!(
4480 validate_test_hostcall_response::<T>(frame_type, &payload).is_err(),
4481 "{frame_type} accepted non-closed response payload {payload}"
4482 );
4483 }
4484 }
4485
4486 #[test]
4487 fn hostcall_response_unions_reject_ambiguous_or_extended_payloads() {
4488 assert_closed_hostcall_response_union::<OpenHandleSuccessResponsePayload>(
4489 FRAME_TYPE_OPEN_HANDLE,
4490 r#"{"ok":true,"package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/backend.wasm","sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","content_base64":"AGFzbQ=="}"#,
4491 r#""content_base64":"AGFzbQ==""#,
4492 );
4493 assert_closed_hostcall_response_union::<HandleGrantSuccessResponsePayload>(
4494 FRAME_TYPE_VALIDATE_HANDLE_GRANT,
4495 r#"{"ok":true,"handle_grant_id":"grant_1","handle_id":"storage:settings","method":"storage.kv","runtime_generation_id":"g1","resource_scope":{"kind":"user","owner_env_hash":"env_hash","owner_user_hash":"user_hash"}}"#,
4496 r#""handle_id":"storage:settings""#,
4497 );
4498 assert_closed_hostcall_response_union::<StorageFileDeleteSuccessResponsePayload>(
4499 FRAME_TYPE_STORAGE_FILE,
4500 r#"{"ok":true,"path":"notes/a.txt"}"#,
4501 r#""path":"notes/a.txt""#,
4502 );
4503 assert_closed_hostcall_response_union::<StorageKVDeleteSuccessResponsePayload>(
4504 FRAME_TYPE_STORAGE_KV,
4505 r#"{"ok":true,"key":"settings/theme"}"#,
4506 r#""key":"settings/theme""#,
4507 );
4508 assert_closed_hostcall_response_union::<StorageSQLiteExecSuccessResponsePayload>(
4509 FRAME_TYPE_STORAGE_SQLITE,
4510 r#"{"ok":true,"database":"plugin.sqlite","rows_affected":1,"usage":{"plugin_instance_id":"plugini_1","store_id":"db","usage_bytes":1,"quota_bytes":100,"usage_files":1,"quota_files":10}}"#,
4511 r#""database":"plugin.sqlite""#,
4512 );
4513 assert_closed_hostcall_response_union::<NetworkGrantSuccessResponsePayload>(
4514 FRAME_TYPE_NETWORK_GRANT,
4515 r#"{"ok":true,"grant_id":"netgrant_00112233445566778899aabbccddeeff","plugin_instance_id":"plugini_1","active_fingerprint":"sha256:active","resource_scope":{"kind":"user","owner_env_hash":"env_hash","owner_user_hash":"user_hash"},"policy_revision":1,"management_revision":2,"revoke_epoch":3,"connector_id":"api","transport":"http","destination":{"transport":"http","scheme":"https","host":"api.example.com","port":443},"runtime_generation_id":"g1","target_classifier_version":"target-classifier-v2","expires_at":"2026-06-30T10:00:30Z"}"#,
4516 r#""grant_id":"netgrant_00112233445566778899aabbccddeeff""#,
4517 );
4518 assert_closed_hostcall_response_union::<NetworkExecuteSuccessResponsePayload>(
4519 FRAME_TYPE_NETWORK_EXECUTE,
4520 r#"{"ok":true,"transport":"http","destination":{"transport":"http","scheme":"https","host":"api.example.com","port":443},"status_code":200,"grant_id":"netgrant_00112233445566778899aabbccddeeff","connector_id":"api","runtime_generation_id":"g1"}"#,
4521 r#""status_code":200"#,
4522 );
4523 }
4524
4525 #[test]
4526 fn validates_hello_frame() {
4527 let public_key = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
4528 let input = hello_frame(
4529 Some("nonce_1234567890"),
4530 &format!(
4531 r#"[{{"algorithm":"ed25519","key_id":"host_ephemeral_key_1","public_key_base64":"{public_key}"}}]"#
4532 ),
4533 );
4534 let (request_id, generation_id, channel_nonce) =
4535 validate_hello_frame(&input).expect("valid hello");
4536 assert_eq!(request_id, "r1");
4537 assert_eq!(generation_id, "g1");
4538 assert_eq!(channel_nonce, "nonce_1234567890");
4539 let parsed = parse_hello_frame(&input).expect("typed hello");
4540 assert_eq!(parsed.target, RuntimeTarget::LinuxAmd64);
4541 assert_eq!(parsed.limits, runtime_limits());
4542 }
4543
4544 #[test]
4545 fn runtime_target_enum_covers_only_canonical_linux_targets() {
4546 for (value, expected) in [
4547 ("linux/amd64", RuntimeTarget::LinuxAmd64),
4548 ("linux/arm64", RuntimeTarget::LinuxArm64),
4549 ] {
4550 let parsed = RuntimeTarget::parse(value).expect("canonical runtime target");
4551 assert_eq!(parsed, expected);
4552 assert_eq!(parsed.as_str(), value);
4553 }
4554 for value in [
4555 "linux/x86_64",
4556 "darwin/amd64",
4557 "darwin/arm64",
4558 "windows/amd64",
4559 "Linux/amd64",
4560 "linux-amd64",
4561 ] {
4562 assert_eq!(
4563 RuntimeTarget::parse(value).unwrap_err(),
4564 IpcError::ProtocolViolation {
4565 message: "unsupported runtime target"
4566 }
4567 );
4568 }
4569 }
4570
4571 #[test]
4572 fn rejects_noncanonical_hello_targets() {
4573 let public_key = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
4574 let valid = hello_frame(
4575 Some("nonce_1234567890"),
4576 &format!(
4577 r#"[{{"algorithm":"ed25519","key_id":"host_ephemeral_key_1","public_key_base64":"{public_key}"}}]"#
4578 ),
4579 );
4580 for invalid in [
4581 valid.replace("linux/amd64", "darwin/amd64"),
4582 valid.replace("linux/amd64", "linux/x86_64"),
4583 ] {
4584 assert_eq!(
4585 parse_hello_frame(&invalid).unwrap_err(),
4586 IpcError::InvalidField {
4587 field: "hello target"
4588 }
4589 );
4590 }
4591 }
4592
4593 #[test]
4594 fn rejects_v2_and_invalid_runtime_limits() {
4595 let public_key = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
4596 let valid = hello_frame(
4597 Some("nonce_1234567890"),
4598 &format!(
4599 r#"[{{"algorithm":"ed25519","key_id":"host_ephemeral_key_1","public_key_base64":"{public_key}"}}]"#
4600 ),
4601 );
4602 assert!(parse_hello_frame(&valid.replace("rust-ipc-v6", "rust-ipc-v2")).is_err());
4603 assert!(
4604 parse_hello_frame(&valid.replacen("\"worker_count\":8", "\"worker_count\":0", 1))
4605 .is_err()
4606 );
4607 assert!(
4608 parse_hello_frame(&valid.replacen(
4609 "\"module_cache_source_bytes\":134217728",
4610 "\"module_cache_source_bytes\":134217729",
4611 1,
4612 ))
4613 .is_err()
4614 );
4615 assert!(
4616 parse_hello_frame(&valid.replacen(
4617 "\"per_plugin_concurrency\":4",
4618 "\"per_plugin_concurrency\":9",
4619 1,
4620 ))
4621 .is_err()
4622 );
4623 }
4624
4625 #[test]
4626 fn runtime_route_capacities_are_closed_derivations_of_hello_limits() {
4627 let limits = runtime_limits().validate().unwrap();
4628 assert_eq!(limits.hostcall_active_route_capacity(), limits.worker_count);
4629 assert_eq!(
4630 limits.hostcall_canceled_route_capacity().unwrap(),
4631 limits.worker_count + limits.queue_capacity
4632 );
4633 assert_eq!(limits.compile_flight_route_capacity(), limits.worker_count);
4634 }
4635
4636 #[test]
4637 fn decodes_invalid_worker_input_once_into_a_typed_runtime_variant() {
4638 let input = r#"{"ipc_version":"rust-ipc-v6","frame_type":"invoke_worker","request_id":"invoke-invalid","runtime_generation_id":"g1","payload":{"method":"worker.echo","invocation":{}}}"#;
4639 let decoded = decode_runtime_input_frame(input).expect("outer IPC frame decodes");
4640 let RuntimeInputFrame::InvokeWorker(worker) = decoded else {
4641 panic!("invoke_worker must use the typed worker variant");
4642 };
4643 assert_eq!(worker.identity.request_id, "invoke-invalid");
4644 assert_eq!(worker.identity.runtime_generation_id, "g1");
4645 assert!(worker.invocation.is_err());
4646 }
4647
4648 #[test]
4649 fn parses_cancel_and_binds_parent_request_id() {
4650 let cancel = r#"{"ipc_version":"rust-ipc-v6","frame_type":"cancel_invoke","request_id":"cancel-1","runtime_generation_id":"g1","payload":{"invocation_request_id":"invoke-1"}}"#;
4651 assert_eq!(parse_cancel_invoke(cancel).unwrap(), "invoke-1");
4652 let ack = cancel_invoke_ack_frame("cancel-1", "g1", "invoke-1", "running")
4653 .expect("cancel acknowledgement frame");
4654 assert!(ack.contains(r#""frame_type":"cancel_invoke_ack""#));
4655 let hostcall = bind_parent_request_id(
4656 r#"{"ipc_version":"rust-ipc-v6","frame_type":"open_handle","request_id":"invoke-1:artifact","runtime_generation_id":"g1","payload":{}}"#,
4657 "invoke-1",
4658 )
4659 .unwrap();
4660 assert_eq!(
4661 parse_frame_identity(&hostcall).unwrap().parent_request_id,
4662 Some("invoke-1".to_string())
4663 );
4664 }
4665
4666 #[test]
4667 fn closed_ipc_decoding_rejects_ambiguous_or_extended_frames() {
4668 let valid = r#"{"ipc_version":"rust-ipc-v6","frame_type":"heartbeat","request_id":"outer","runtime_generation_id":"g1","payload":{"request_id":"nested"}}"#;
4669 let identity = parse_frame_identity(valid).expect("top-level frame identity");
4670 assert_eq!(identity.request_id, "outer");
4671
4672 for invalid in [
4673 format!("{valid}{{}}"),
4674 valid.replace(r#""payload""#, r#""unknown":true,"payload""#),
4675 valid.replace(
4676 r#""request_id":"outer""#,
4677 r#""request_id":"outer","request_id":"replayed""#,
4678 ),
4679 ] {
4680 assert!(parse_frame_identity(&invalid).is_err(), "{invalid}");
4681 }
4682 }
4683
4684 #[test]
4685 fn runtime_hostcall_response_requires_nonempty_parent_request_id() {
4686 let without_parent = r#"{"ipc_version":"rust-ipc-v6","frame_type":"open_handle","request_id":"r1:artifact","runtime_generation_id":"g1","payload":{"ok":false,"code":"ARTIFACT_HANDLE_FAILED","message":"unavailable","error_origin":"hostcall"}}"#;
4687 assert!(decode_runtime_input_frame(without_parent).is_err());
4688 let empty_parent = without_parent.replace(
4689 r#""request_id":"r1:artifact""#,
4690 r#""request_id":"r1:artifact","parent_request_id":"""#,
4691 );
4692 assert!(decode_runtime_input_frame(&empty_parent).is_err());
4693 }
4694
4695 #[test]
4696 fn closed_worker_decoding_rejects_unknown_duplicate_and_trailing_fields() {
4697 let valid = closed_worker_frame(
4698 r#"{"plugin_instance_id":"plugini_1","runtime_shard_id":"runtime_shard_signed","policy_revision":1,"management_revision":2,"revoke_epoch":1}"#,
4699 r#"{"plugin_id":"com.example.worker","plugin_instance_id":"plugini_1","active_fingerprint":"sha256:active","runtime_instance_id":"runtime_1","runtime_generation_id":"g1","method":"worker.echo"}"#,
4700 );
4701 let context = parse_worker_invocation_context(&valid).expect("closed worker invocation");
4702 assert_eq!(context.runtime_shard_id, "runtime_shard_signed");
4703
4704 for invalid in [
4705 format!("{valid}{{}}"),
4706 valid.replace(
4707 r#""method":"worker.echo"}}}"#,
4708 r#""method":"worker.echo","unknown":true}}}"#,
4709 ),
4710 valid.replace(
4711 r#""plugin_instance_id":"plugini_1","active_fingerprint""#,
4712 r#""plugin_instance_id":"plugini_1","plugin_instance_id":"plugini_2","active_fingerprint""#,
4713 ),
4714 valid.replace(
4715 r#""runtime_instance_id":"runtime_1""#,
4716 r#""runtime_instance_id":"runtime_1","runtime_shard_id":"runtime_shard_spoofed""#,
4717 ),
4718 valid.replace(
4719 r#""revoke_epoch":1}"#,
4720 r#""revoke_epoch":1,"unknown":true}"#,
4721 ),
4722 ] {
4723 assert!(parse_worker_invocation_context(&invalid).is_err(), "{invalid}");
4724 }
4725 }
4726
4727 #[test]
4728 fn worker_frame_initial_decode_types_params_and_broker_access() {
4729 for invocation in [
4730 r#"{"method":"worker.echo","params":[]}"#,
4731 r#"{"method":"worker.echo","broker_access":{"unknown":true}}"#,
4732 r#"{"method":"worker.echo","broker_access":{"storage":[{"store_id":"notes","scope":"user","operations":["read"],"unknown":true}]}}"#,
4733 ] {
4734 let error = match parse_worker_invocation(&closed_worker_frame("{}", invocation)) {
4735 Ok(_) => panic!("typed invocation fields must fail during initial frame decode"),
4736 Err(error) => error,
4737 };
4738 assert_eq!(
4739 error,
4740 IpcError::DecodeFailed {
4741 context: "worker frame payload"
4742 }
4743 );
4744 }
4745
4746 let invocation = r#"{"method":"worker.echo","params":{"title":"Launch notes","body":"<script>&\u2028"},"broker_access":{"storage":[{"store_id":"notes","scope":"user","operations":["read"]}]}}"#;
4747 serde_json::from_str::<WorkerInvocationPayload>(invocation)
4748 .expect("direct typed worker invocation payload");
4749 let parsed = parse_worker_invocation(&closed_worker_frame("{}", invocation))
4750 .expect("typed worker invocation");
4751 assert_eq!(
4752 parsed.params_json.as_deref(),
4753 Some(r#"{"body":"<script>&\u2028","title":"Launch notes"}"#)
4754 );
4755 assert_eq!(
4756 parsed.broker_access_json.as_deref(),
4757 Some(r#"{"storage":[{"store_id":"notes","scope":"user","operations":["read"]}]}"#)
4758 );
4759 }
4760
4761 #[test]
4762 fn rejects_hello_frame_without_channel_nonce() {
4763 let input = hello_frame(None, "[]");
4764 assert_eq!(
4765 validate_hello_frame(&input),
4766 Err(IpcError::MissingField {
4767 field: "channel_nonce"
4768 })
4769 );
4770 }
4771
4772 #[test]
4773 fn parses_runtime_lease_public_keys_from_hello() {
4774 let public_key = base64::engine::general_purpose::STANDARD.encode([7u8; 32]);
4775 let input = hello_frame(
4776 Some("nonce_1234567890"),
4777 &format!(
4778 r#"[{{"algorithm":"ed25519","key_id":"host_ephemeral_key_1","public_key_base64":"{public_key}"}}]"#
4779 ),
4780 );
4781 let keys = parse_runtime_lease_public_keys(&input).expect("keys");
4782 assert_eq!(
4783 keys,
4784 vec![RuntimeLeasePublicKey {
4785 key_id: "host_ephemeral_key_1".to_string(),
4786 public_key: [7u8; 32],
4787 }]
4788 );
4789 }
4790
4791 #[test]
4792 fn rejects_hello_without_runtime_lease_public_keys() {
4793 let missing = hello_frame(Some("nonce_1234567890"), "null");
4794 assert!(parse_runtime_lease_public_keys(&missing).is_err());
4795 let empty = hello_frame(Some("nonce_1234567890"), "[]");
4796 assert!(parse_runtime_lease_public_keys(&empty).is_err());
4797 }
4798
4799 #[test]
4800 fn verifies_worker_runtime_lease_signature() {
4801 let signing_key = runtime_lease_signing_key_for_test(7);
4802 let frame = signed_runtime_lease_invocation_for_test(&signing_key, None);
4803 let key = RuntimeLeasePublicKey {
4804 key_id: "host_ephemeral_key_1".to_string(),
4805 public_key: signing_key.verifying_key().to_bytes(),
4806 };
4807 verify_worker_runtime_lease_signature(&frame, &[key]).expect("signed lease");
4808 }
4809
4810 #[test]
4811 fn rejects_tampered_worker_runtime_lease_signature() {
4812 let signing_key = runtime_lease_signing_key_for_test(7);
4813 let frame =
4814 signed_runtime_lease_invocation_for_test(&signing_key, Some(("revoke_epoch", "14")));
4815 let key = RuntimeLeasePublicKey {
4816 key_id: "host_ephemeral_key_1".to_string(),
4817 public_key: signing_key.verifying_key().to_bytes(),
4818 };
4819 let err = verify_worker_runtime_lease_signature(&frame, &[key])
4820 .expect_err("tampered lease should fail");
4821 assert_eq!(
4822 err,
4823 IpcError::InvalidField {
4824 field: "runtime lease signature"
4825 }
4826 );
4827 }
4828
4829 #[test]
4830 fn rejects_unsigned_worker_runtime_lease_when_keys_are_configured() {
4831 let signing_key = runtime_lease_signing_key_for_test(7);
4832 let signed = signed_runtime_lease_invocation_for_test(&signing_key, None);
4833 let mut frame: serde_json::Value = serde_json::from_str(&signed).expect("signed frame");
4834 frame["payload"]["lease"]
4835 .as_object_mut()
4836 .expect("lease object")
4837 .remove("signature");
4838 let frame = serde_json::to_string(&frame).expect("unsigned frame");
4839 let err = verify_worker_runtime_lease_signature(
4840 &frame,
4841 &[RuntimeLeasePublicKey {
4842 key_id: "host_ephemeral_key_1".to_string(),
4843 public_key: signing_key.verifying_key().to_bytes(),
4844 }],
4845 )
4846 .expect_err("missing signature should fail");
4847 assert_eq!(err, IpcError::MissingField { field: "signature" });
4848 }
4849
4850 #[test]
4851 fn runtime_lease_signature_requires_closed_contract_fields() {
4852 let signing_key = runtime_lease_signing_key_for_test(7);
4853 let signed = signed_runtime_lease_invocation_for_test(&signing_key, None);
4854 let frame: serde_json::Value = serde_json::from_str(&signed).expect("signed frame");
4855 let lease = frame["payload"]["lease"]
4856 .as_object()
4857 .expect("lease object")
4858 .clone();
4859 let method = frame["payload"]["method"].as_str().expect("method");
4860
4861 for field in [
4862 "plugin_id",
4863 "plugin_version",
4864 "active_fingerprint",
4865 "owner_env_hash",
4866 "target_descriptor_hashes",
4867 "limits",
4868 "policy_revision",
4869 "management_revision",
4870 "revoke_epoch",
4871 "runtime_shard_id",
4872 "runtime_instance_id",
4873 "ipc_channel_id",
4874 "connection_nonce",
4875 ] {
4876 let mut missing = lease.clone();
4877 missing.remove(field);
4878 assert!(
4879 runtime_lease_signature_payload_json(&worker_lease_from_object(&missing), method)
4880 .is_err(),
4881 "accepted lease without {field}"
4882 );
4883 }
4884
4885 let mut zero_revoke_epoch = lease.clone();
4886 zero_revoke_epoch.insert("revoke_epoch".to_string(), serde_json::Value::from(0));
4887 assert_eq!(
4888 runtime_lease_signature_payload_json(
4889 &worker_lease_from_object(&zero_revoke_epoch),
4890 method
4891 )
4892 .unwrap_err(),
4893 IpcError::InvalidField {
4894 field: "revoke_epoch"
4895 }
4896 );
4897
4898 for field in [
4899 "timeout_ms",
4900 "memory_bytes",
4901 "max_payload_bytes",
4902 "max_stream_bytes_per_sec",
4903 ] {
4904 let mut missing = lease.clone();
4905 missing["limits"]
4906 .as_object_mut()
4907 .expect("limits object")
4908 .remove(field);
4909 assert!(
4910 runtime_lease_signature_payload_json(&worker_lease_from_object(&missing), method)
4911 .is_err(),
4912 "accepted limits without {field}"
4913 );
4914 }
4915
4916 let mut zero_limits = lease;
4917 for field in [
4918 "timeout_ms",
4919 "max_payload_bytes",
4920 "max_stream_bytes_per_sec",
4921 ] {
4922 zero_limits["limits"][field] = serde_json::Value::from(0);
4923 }
4924 let canonical =
4925 runtime_lease_signature_payload_json(&worker_lease_from_object(&zero_limits), method)
4926 .expect("zero-valued optional quota dimensions remain explicit");
4927 for field in [
4928 "\"timeout_ms\":0",
4929 "\"max_payload_bytes\":0",
4930 "\"max_stream_bytes_per_sec\":0",
4931 ] {
4932 assert!(
4933 canonical.contains(field),
4934 "canonical payload omitted {field}"
4935 );
4936 }
4937 }
4938
4939 #[test]
4940 fn rejects_worker_runtime_lease_without_runtime_keys() {
4941 let signing_key = runtime_lease_signing_key_for_test(7);
4942 let frame = signed_runtime_lease_invocation_for_test(&signing_key, None);
4943 let err = verify_worker_runtime_lease_signature(&frame, &[])
4944 .expect_err("missing runtime keyring should fail closed");
4945 assert_eq!(
4946 err,
4947 IpcError::MissingField {
4948 field: "runtime lease public keys"
4949 }
4950 );
4951 }
4952
4953 #[test]
4954 fn validates_worker_runtime_lease_expiry_and_execution_binding() {
4955 let frame = runtime_lease_invocation_fixture();
4956 validate_worker_runtime_lease(frame, 1_783_161_901_000)
4957 .expect("current runtime lease binding");
4958
4959 let expired = validate_worker_runtime_lease(frame, 1_783_161_930_000)
4960 .expect_err("expired runtime lease must fail closed");
4961 assert_eq!(
4962 expired,
4963 IpcError::ProtocolViolation {
4964 message: "runtime execution lease is expired"
4965 }
4966 );
4967
4968 let mut mismatched: Value = serde_json::from_str(frame).expect("invocation fixture");
4969 mismatched["payload"]["invocation"]["stream_id"] =
4970 Value::String("stream_other".to_string());
4971 let mismatch = validate_worker_runtime_lease(
4972 &serde_json::to_string(&mismatched).expect("mismatched invocation"),
4973 1_783_161_901_000,
4974 )
4975 .expect_err("execution handle mismatch must fail closed");
4976 assert_eq!(mismatch, IpcError::InvalidField { field: "stream_id" });
4977
4978 let mut wrong_environment: Value = serde_json::from_str(frame).expect("invocation fixture");
4979 wrong_environment["payload"]["invocation"]["owner_env_hash"] =
4980 Value::String("environment_other".to_string());
4981 let environment_mismatch = validate_worker_runtime_lease(
4982 &serde_json::to_string(&wrong_environment).expect("mismatched environment"),
4983 1_783_161_901_000,
4984 )
4985 .expect_err("environment mismatch must fail closed");
4986 assert_eq!(
4987 environment_mismatch,
4988 IpcError::InvalidField {
4989 field: "owner_env_hash"
4990 }
4991 );
4992
4993 let mut tampered_params: Value = serde_json::from_str(frame).expect("invocation fixture");
4994 tampered_params["payload"]["invocation"]["params"]["message"] =
4995 Value::String("tampered".to_string());
4996 let params_mismatch = validate_worker_runtime_lease(
4997 &serde_json::to_string(&tampered_params).expect("tampered invocation"),
4998 1_783_161_901_000,
4999 )
5000 .expect_err("tampered params must fail closed");
5001 assert_eq!(
5002 params_mismatch,
5003 IpcError::ProtocolViolation {
5004 message: "worker invocation params_sha256 does not match params"
5005 }
5006 );
5007
5008 let mut unbound_target: Value = serde_json::from_str(frame).expect("invocation fixture");
5009 unbound_target["payload"]["lease"]["target_descriptor_hashes"] = serde_json::json!([
5010 "method:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
5011 "worker:sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
5012 ]);
5013 let target_mismatch = validate_worker_runtime_lease(
5014 &serde_json::to_string(&unbound_target).expect("unbound invocation"),
5015 1_783_161_901_000,
5016 )
5017 .expect_err("unbound invocation target must fail closed");
5018 assert_eq!(
5019 target_mismatch,
5020 IpcError::ProtocolViolation {
5021 message: "runtime lease does not bind the worker invocation target"
5022 }
5023 );
5024 }
5025
5026 #[test]
5027 fn runtime_lease_signature_payload_matches_go_canonical_order() {
5028 let lease = serde_json::json!({
5029 "lease_id": "rel_lease_signature",
5030 "token_id": "rel_token_signature",
5031 "lease_nonce": "nonce_1234567890",
5032 "runtime_generation_id": "rtgen_1",
5033 "plugin_instance_id": "plugini_1",
5034 "plugin_id": "com.example.worker",
5035 "plugin_version": "1.2.3",
5036 "active_fingerprint": "sha256:active",
5037 "issued_at_unix_ms": 1783161900000_i64,
5038 "method": "worker.echo",
5039 "effect": "read",
5040 "execution": "sync",
5041 "audit_correlation_id": "audit_lease_signature",
5042 "surface_instance_id": "surface_runtime",
5043 "owner_session_hash": "session_hash",
5044 "owner_user_hash": "user_hash",
5045 "owner_env_hash": "env_hash",
5046 "session_channel_id_hash": "channel_hash",
5047 "bridge_channel_id": "bridge_runtime",
5048 "target_descriptor_hashes": [
5049 "method:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
5050 "worker:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5051 ],
5052 "limits": {
5053 "timeout_ms": 2000,
5054 "memory_bytes": 65536,
5055 "max_payload_bytes": 4096,
5056 "max_stream_bytes_per_sec": 1024
5057 },
5058 "policy_revision": 11,
5059 "management_revision": 12,
5060 "revoke_epoch": 13,
5061 "runtime_shard_id": "rtshard_1",
5062 "runtime_instance_id": "rtinst_1",
5063 "ipc_channel_id": "ipc_1",
5064 "connection_nonce": "connection_nonce_1234567890",
5065 "key_id": "host_ephemeral_key_1",
5066 "signature": "ed25519:not-part-of-the-payload",
5067 "expires_at_unix_ms": 1783161930000_i64
5068 });
5069 let payload =
5070 runtime_lease_signature_payload_json(&worker_lease_from_value(&lease), "worker.echo")
5071 .expect("payload");
5072 assert_eq!(
5073 payload,
5074 r#"{"schema_version":"redevplugin.runtime_execution_lease.v1","token_kind":"runtime_execution_lease","lease_id":"rel_lease_signature","token_id":"rel_token_signature","lease_nonce":"nonce_1234567890","plugin_instance_id":"plugini_1","plugin_id":"com.example.worker","plugin_version":"1.2.3","active_fingerprint":"sha256:active","issued_at_unix_ms":1783161900000,"method":"worker.echo","effect":"read","execution":"sync","audit_correlation_id":"audit_lease_signature","surface_instance_id":"surface_runtime","owner_session_hash":"session_hash","owner_user_hash":"user_hash","owner_env_hash":"env_hash","session_channel_id_hash":"channel_hash","bridge_channel_id":"bridge_runtime","target_descriptor_hashes":["method:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","worker:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],"limits":{"timeout_ms":2000,"memory_bytes":65536,"max_payload_bytes":4096,"max_stream_bytes_per_sec":1024},"policy_revision":11,"management_revision":12,"revoke_epoch":13,"expires_at_unix_ms":1783161930000,"runtime_shard_id":"rtshard_1","runtime_instance_id":"rtinst_1","runtime_generation_id":"rtgen_1","ipc_channel_id":"ipc_1","connection_nonce":"connection_nonce_1234567890","key_id":"host_ephemeral_key_1"}"#
5075 );
5076 assert!(!payload.contains("not-part-of-the-payload"));
5077 }
5078
5079 #[test]
5080 fn runtime_lease_signature_shared_fixture_matches_go() {
5081 let fixture: serde_json::Value =
5082 serde_json::from_str(include_str!("../testdata/runtime-lease-signature-v1.json"))
5083 .expect("shared runtime lease fixture");
5084 let lease = fixture
5085 .get("lease")
5086 .and_then(|value| value.as_object())
5087 .expect("fixture lease");
5088 let method = fixture
5089 .get("method")
5090 .and_then(|value| value.as_str())
5091 .expect("fixture method");
5092 let canonical = fixture
5093 .get("canonical_payload")
5094 .and_then(|value| value.as_str())
5095 .expect("fixture canonical payload");
5096 assert_eq!(
5097 runtime_lease_signature_payload_json(&worker_lease_from_object(lease), method,)
5098 .expect("canonical payload"),
5099 canonical
5100 );
5101 let public_key: [u8; 32] = base64::engine::general_purpose::STANDARD
5102 .decode(
5103 fixture
5104 .get("public_key_base64")
5105 .and_then(|value| value.as_str())
5106 .expect("fixture public key")
5107 .as_bytes(),
5108 )
5109 .expect("fixture public key base64")
5110 .try_into()
5111 .expect("fixture public key length");
5112 verify_worker_runtime_lease_signature(
5113 runtime_lease_invocation_fixture(),
5114 &[RuntimeLeasePublicKey {
5115 key_id: "host_ephemeral_fixture_v1".to_string(),
5116 public_key,
5117 }],
5118 )
5119 .expect("shared runtime lease fixture signature");
5120 }
5121
5122 #[test]
5123 fn renders_hello_ack_frame() {
5124 let actual_target = RuntimeTarget::LinuxAmd64;
5125 let frame = hello_ack_frame(HelloAckFrameRequest {
5126 request_id: "r1",
5127 runtime_generation_id: "g1",
5128 channel_nonce: "nonce_1",
5129 runtime_version: "0.0.0-dev",
5130 actual_target: &actual_target,
5131 wasm_abi_version: WASM_ABI_VERSION,
5132 limits: runtime_limits(),
5133 process_containment: &process_containment(),
5134 })
5135 .expect("hello acknowledgement frame");
5136 assert!(frame.contains(r#""frame_type":"hello_ack""#));
5137 assert!(frame.contains(r#""request_id":"r1""#));
5138 assert!(frame.contains(r#""runtime_generation_id":"g1""#));
5139 assert!(frame.contains(r#""actual_target":"linux/amd64""#));
5140 assert!(frame.contains(r#""rust_ipc_version":"rust-ipc-v6""#));
5141 assert!(frame.contains(&format!(r#""contract_set_sha256":"{CONTRACT_SET_SHA256}""#)));
5142 assert!(frame.contains(r#""channel_nonce":"nonce_1""#));
5143 assert!(frame.contains(r#""worker_count":8"#));
5144 assert!(frame.contains(r#""module_cache_source_bytes":134217728"#));
5145 }
5146
5147 #[test]
5148 fn hello_ack_frame_rejects_invalid_runtime_limits() {
5149 let actual_target = RuntimeTarget::LinuxAmd64;
5150 assert!(matches!(
5151 hello_ack_frame(HelloAckFrameRequest {
5152 request_id: "r1",
5153 runtime_generation_id: "g1",
5154 channel_nonce: "nonce_1",
5155 runtime_version: "0.0.0-dev",
5156 actual_target: &actual_target,
5157 wasm_abi_version: WASM_ABI_VERSION,
5158 limits: invalid_runtime_limits(),
5159 process_containment: &process_containment(),
5160 }),
5161 Err(IpcError::ProtocolViolation { .. })
5162 ));
5163 }
5164
5165 #[test]
5166 fn renders_error_response_frame() {
5167 let frame = error_response_frame(
5168 FRAME_TYPE_INVOKE_WORKER_RESULT,
5169 "r1",
5170 "g1",
5171 ResponseError::runtime(ERR_WASM_WORKER_FAILED, "runtime worker execution failed")
5172 .expect("runtime response error"),
5173 )
5174 .expect("runtime error response frame");
5175 assert!(frame.contains(r#""frame_type":"invoke_worker_result""#));
5176 assert!(frame.contains(r#""ok":false"#));
5177 assert!(frame.contains(r#""code":"WASM_WORKER_FAILED""#));
5178 assert!(frame.contains(r#""error_origin":"runtime""#));
5179
5180 let plugin_frame = error_response_frame(
5181 FRAME_TYPE_INVOKE_WORKER_RESULT,
5182 "r2",
5183 "g1",
5184 ResponseError::plugin("NOTE_NOT_FOUND", "note was not found")
5185 .expect("plugin response error"),
5186 )
5187 .expect("plugin error response frame");
5188 assert!(plugin_frame.contains(r#""error_origin":"plugin""#));
5189 }
5190
5191 #[test]
5192 fn error_response_rejects_empty_code() {
5193 assert_eq!(
5194 ResponseError::runtime(" ", "failed"),
5195 Err(IpcError::EmptyResponseErrorCode),
5196 );
5197 }
5198
5199 #[test]
5200 fn error_response_rejects_empty_message() {
5201 assert_eq!(
5202 ResponseError::runtime("FAILED", " "),
5203 Err(IpcError::EmptyResponseErrorMessage),
5204 );
5205 }
5206
5207 #[test]
5208 fn success_response_rejects_invalid_result_json_without_panicking() {
5209 assert_eq!(
5210 success_response_frame(FRAME_TYPE_INVOKE_WORKER_RESULT, "r1", "g1", "{"),
5211 Err(IpcError::InvalidResponseResultJson),
5212 );
5213 }
5214
5215 #[test]
5216 fn ipc_errors_are_cloneable_comparable_and_have_stable_redacted_display() {
5217 let error = IpcError::RemoteFailure {
5218 code: "NETWORK_TARGET_DENIED".to_string(),
5219 };
5220 assert_eq!(error.clone(), error);
5221 assert_eq!(
5222 error.to_string(),
5223 "hostcall response failed with code NETWORK_TARGET_DENIED"
5224 );
5225
5226 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"network_execute","request_id":"r1:network_execute","runtime_generation_id":"g1","payload":{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"bearer secret-token https://api.example.com/path?token=secret /Users/private/key","error_origin":"hostcall"}}"#;
5227 let error =
5228 validate_network_execute_response(failed, "r1:network_execute", "g1", "api", "http")
5229 .expect_err("hostcall failure");
5230 let display = error.to_string();
5231 assert_eq!(
5232 display,
5233 "hostcall response failed with code NETWORK_TARGET_DENIED"
5234 );
5235 for sensitive in [
5236 "secret-token",
5237 "api.example.com",
5238 "token=secret",
5239 "/Users/private",
5240 ] {
5241 assert!(!display.contains(sensitive), "display leaked {sensitive}");
5242 }
5243 }
5244
5245 #[test]
5246 fn ipc_golden_fixtures_match_rust_frame_contract() {
5247 let fixtures = [
5248 "valid_hello_ack.json",
5249 "valid_invoke_worker_result.json",
5250 "valid_validate_handle_grant.json",
5251 "missing_required.json",
5252 "replay_frame.json",
5253 "runtime_generation_mismatch.json",
5254 "unknown_enum.json",
5255 ];
5256 for fixture_name in fixtures {
5257 let fixture = load_ipc_fixture(fixture_name);
5258 assert_eq!(
5259 fixture["want_error"].as_bool(),
5260 Some(
5261 fixture_name != "valid_hello_ack.json"
5262 && fixture_name != "valid_invoke_worker_result.json"
5263 && fixture_name != "valid_validate_handle_grant.json"
5264 ),
5265 "fixture {fixture_name} want_error mismatch"
5266 );
5267 let frame = fixture.get("frame").expect("fixture frame").clone();
5268 let frame_json = serde_json::to_string(&frame).expect("compact frame");
5269 match fixture_name {
5270 "valid_hello_ack.json" => {
5271 let actual_target = RuntimeTarget::parse(
5272 frame["payload"]["actual_target"]
5273 .as_str()
5274 .expect("actual_target"),
5275 )
5276 .expect("actual_target");
5277 let encoded = hello_ack_frame(HelloAckFrameRequest {
5278 request_id: fixture["request_id"].as_str().expect("request_id"),
5279 runtime_generation_id: fixture["runtime_generation_id"]
5280 .as_str()
5281 .expect("runtime_generation_id"),
5282 channel_nonce: fixture["channel_nonce"].as_str().expect("channel_nonce"),
5283 runtime_version: frame["payload"]["runtime_version"]
5284 .as_str()
5285 .expect("runtime_version"),
5286 actual_target: &actual_target,
5287 wasm_abi_version: WASM_ABI_VERSION,
5288 limits: runtime_limits(),
5289 process_containment: &process_containment(),
5290 })
5291 .expect("hello acknowledgement frame");
5292 assert_json_eq(&frame_json, &encoded, fixture_name);
5293 }
5294 "valid_invoke_worker_result.json" => {
5295 let result =
5296 serde_json::to_string(&frame["payload"]["result"]).expect("compact result");
5297 let encoded = success_response_frame(
5298 FRAME_TYPE_INVOKE_WORKER_RESULT,
5299 fixture["request_id"].as_str().expect("request_id"),
5300 fixture["runtime_generation_id"]
5301 .as_str()
5302 .expect("runtime_generation_id"),
5303 &result,
5304 )
5305 .expect("success response frame");
5306 assert_json_eq(&frame_json, &encoded, fixture_name);
5307 }
5308 "valid_validate_handle_grant.json" => {
5309 let encoded = validate_handle_grant_frame(
5310 fixture["request_id"].as_str().expect("request_id"),
5311 fixture["runtime_generation_id"]
5312 .as_str()
5313 .expect("runtime_generation_id"),
5314 &handle_grant_validation_request(),
5315 )
5316 .expect("validate handle grant frame");
5317 let encoded = bind_parent_request_id(
5318 &encoded,
5319 fixture["parent_request_id"]
5320 .as_str()
5321 .expect("parent_request_id"),
5322 )
5323 .expect("bind validate handle grant parent");
5324 assert_json_eq(&frame_json, &encoded, fixture_name);
5325 }
5326 "missing_required.json" => {
5327 assert_eq!(
5328 parse_frame_identity(&frame_json),
5329 Err(IpcError::MissingField {
5330 field: "request_id"
5331 }),
5332 "fixture {fixture_name} should reject missing request_id"
5333 );
5334 }
5335 "replay_frame.json" => {
5336 let identity = parse_frame_identity(&frame_json).expect("parse replay fixture");
5337 assert_ne!(
5338 identity.request_id,
5339 fixture["request_id"].as_str().expect("expected request_id"),
5340 "fixture {fixture_name} should replay a different request_id"
5341 );
5342 }
5343 "runtime_generation_mismatch.json" => {
5344 let identity = parse_frame_identity(&frame_json)
5345 .expect("parse runtime generation mismatch fixture");
5346 assert_ne!(
5347 identity.runtime_generation_id,
5348 fixture["runtime_generation_id"]
5349 .as_str()
5350 .expect("expected runtime_generation_id"),
5351 "fixture {fixture_name} should carry mismatched runtime generation"
5352 );
5353 }
5354 "unknown_enum.json" => {
5355 let identity =
5356 parse_frame_identity(&frame_json).expect("parse unknown enum fixture");
5357 assert_ne!(
5358 identity.frame_type, FRAME_TYPE_INVOKE_WORKER_RESULT,
5359 "fixture {fixture_name} should use an unknown frame type"
5360 );
5361 }
5362 _ => panic!("unhandled fixture {fixture_name}"),
5363 }
5364 }
5365 }
5366
5367 fn load_ipc_fixture(name: &str) -> Value {
5368 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
5369 path.push("testdata");
5370 path.push("ipc");
5371 path.push(name);
5372 let raw = fs::read_to_string(&path).unwrap_or_else(|err| {
5373 panic!("read fixture {}: {err}", path.display());
5374 });
5375 serde_json::from_str(&raw).unwrap_or_else(|err| {
5376 panic!("decode fixture {}: {err}", path.display());
5377 })
5378 }
5379
5380 fn assert_json_eq(actual: &str, expected: &str, label: &str) {
5381 let actual: Value = serde_json::from_str(actual).expect("actual json");
5382 let expected: Value = serde_json::from_str(expected).expect("expected json");
5383 assert_eq!(actual, expected, "{label} json mismatch");
5384 }
5385
5386 #[test]
5387 fn renders_revoke_epoch_ack_result_json() {
5388 let result =
5389 revoke_epoch_ack_result_json(&environment_resource_scope(), "plugini_1", 7, 2, 3, 4)
5390 .expect("valid revoke result");
5391 assert!(
5392 result
5393 .contains(r#""resource_scope":{"kind":"environment","owner_env_hash":"env_hash"}"#)
5394 );
5395 assert!(result.contains(r#""plugin_instance_id":"plugini_1""#));
5396 assert!(result.contains(r#""revoke_epoch":7"#));
5397 assert!(result.contains(r#""closed_socket_count":2"#));
5398 assert!(result.contains(r#""closed_stream_count":3"#));
5399 assert!(result.contains(r#""closed_storage_handle_count":4"#));
5400 }
5401
5402 #[test]
5403 fn renders_heartbeat_ack_result_json() {
5404 let result = heartbeat_ack_result_json(
5405 "runtime_gen_1",
5406 101,
5407 5000,
5408 100,
5409 RuntimeHeartbeatStatus {
5410 active_invocations: 2,
5411 queued_invocations: 3,
5412 limits: runtime_limits(),
5413 module_cache: ModuleCacheMetrics {
5414 hits: 4,
5415 misses: 5,
5416 compiles: 1,
5417 entries: 1,
5418 source_bytes: 1024,
5419 },
5420 },
5421 )
5422 .expect("heartbeat acknowledgement result");
5423 assert!(result.contains(r#""runtime_generation_id":"runtime_gen_1""#));
5424 assert!(result.contains(r#""runtime_unix_nano":101"#));
5425 assert!(result.contains(r#""max_staleness_ms":5000"#));
5426 assert!(result.contains(r#""host_sent_unix_nano":100"#));
5427 }
5428
5429 #[test]
5430 fn heartbeat_ack_result_rejects_invalid_runtime_limits() {
5431 assert!(matches!(
5432 heartbeat_ack_result_json(
5433 "runtime_gen_1",
5434 101,
5435 5000,
5436 100,
5437 RuntimeHeartbeatStatus {
5438 active_invocations: 0,
5439 queued_invocations: 0,
5440 limits: invalid_runtime_limits(),
5441 module_cache: ModuleCacheMetrics {
5442 hits: 0,
5443 misses: 0,
5444 compiles: 0,
5445 entries: 0,
5446 source_bytes: 0,
5447 },
5448 },
5449 ),
5450 Err(IpcError::ProtocolViolation { .. })
5451 ));
5452 }
5453
5454 #[test]
5455 fn renders_open_handle_frame() {
5456 let identity = WorkerInvocationIdentity {
5457 package_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5458 .to_string(),
5459 artifact: "workers/backend.wasm".to_string(),
5460 artifact_sha256:
5461 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5462 .to_string(),
5463 worker_id: "backend".to_string(),
5464 method: "worker.echo".to_string(),
5465 };
5466 let frame = open_handle_frame("r1", "g1", &identity);
5467 assert!(frame.contains(r#""frame_type":"open_handle""#));
5468 assert!(frame.contains(r#""artifact":"workers/backend.wasm""#));
5469 }
5470
5471 #[test]
5472 fn renders_compile_flight_lifecycle_frames() {
5473 let identity = WorkerInvocationIdentity {
5474 package_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5475 .to_string(),
5476 artifact: "workers/backend.wasm".to_string(),
5477 artifact_sha256:
5478 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5479 .to_string(),
5480 worker_id: "backend".to_string(),
5481 method: "worker.echo".to_string(),
5482 };
5483 for (frame_type, actual) in [
5484 (
5485 FRAME_TYPE_COMPILE_FLIGHT_REGISTER,
5486 compile_flight_register_frame("invoke-1", "generation-1", &identity),
5487 ),
5488 (
5489 FRAME_TYPE_COMPILE_FLIGHT_COMPLETE,
5490 compile_flight_complete_frame("invoke-1", "generation-1", &identity),
5491 ),
5492 ] {
5493 let suffix = if frame_type == FRAME_TYPE_COMPILE_FLIGHT_REGISTER {
5494 "register"
5495 } else {
5496 "complete"
5497 };
5498 let expected = format!(
5499 r#"{{"ipc_version":"rust-ipc-v6","frame_type":"{frame_type}","request_id":"invoke-1:artifact:{suffix}","parent_request_id":"invoke-1","runtime_generation_id":"generation-1","payload":{{"artifact_request_id":"invoke-1:artifact","package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/backend.wasm","artifact_sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","wasm_abi_version":"redevplugin-wasm-worker-v2"}}}}"#
5500 );
5501 assert_json_eq(&actual, &expected, frame_type);
5502 }
5503 }
5504
5505 #[test]
5506 fn validates_open_handle_response() {
5507 let identity = WorkerInvocationIdentity {
5508 package_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5509 .to_string(),
5510 artifact: "workers/backend.wasm".to_string(),
5511 artifact_sha256:
5512 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5513 .to_string(),
5514 worker_id: "backend".to_string(),
5515 method: "worker.echo".to_string(),
5516 };
5517 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"open_handle","request_id":"r1:artifact","parent_request_id":"r1","runtime_generation_id":"g1","payload":{"ok":true,"package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/backend.wasm","sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","content_base64":"AAE="}}"#;
5518 validate_open_handle_response(frame, "r1:artifact", "r1", "g1", &identity)
5519 .expect("valid open_handle");
5520 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"open_handle","request_id":"r1:artifact","parent_request_id":"r1","runtime_generation_id":"g1","payload":{"ok":false,"code":"ARTIFACT_HANDLE_FAILED","message":"unavailable","error_origin":"hostcall"}}"#;
5521 let err = validate_open_handle_response(failed, "r1:artifact", "r1", "g1", &identity)
5522 .expect_err("failed open_handle response");
5523 assert_eq!(
5524 err,
5525 IpcError::RemoteFailure {
5526 code: "ARTIFACT_HANDLE_FAILED".to_string()
5527 }
5528 );
5529 }
5530
5531 #[test]
5532 fn renders_validate_handle_grant_frame() {
5533 let request = handle_grant_validation_request();
5534 let frame = validate_handle_grant_frame("r1:handle", "g1", &request)
5535 .expect("valid handle grant frame");
5536 assert!(frame.contains(r#""frame_type":"validate_handle_grant""#));
5537 assert!(frame.contains(r#""handle_id":"storage:db""#));
5538 assert!(frame.contains(r#""runtime_instance_id":"runtime_1""#));
5539 assert!(frame.contains(r#""runtime_shard_id":"runtime_shard_1""#));
5540 assert!(frame.contains(r#""owner_session_hash":"session_hash""#));
5541 assert!(frame.contains(r#""owner_user_hash":"user_hash""#));
5542 assert!(frame.contains(r#""owner_env_hash":"env_hash""#));
5543 assert!(frame.contains(r#""session_channel_id_hash":"channel_hash""#));
5544 assert!(frame.contains(r#""policy_revision":1"#));
5545
5546 let mut missing_session = request.clone();
5547 missing_session.owner_session_hash.clear();
5548 assert_eq!(
5549 validate_handle_grant_frame("r1:handle", "g1", &missing_session).unwrap_err(),
5550 IpcError::InvalidField {
5551 field: "handle grant session audience"
5552 }
5553 );
5554
5555 let mut mismatched_scope = request.clone();
5556 mismatched_scope.owner_env_hash = "env_other".to_string();
5557 assert_eq!(
5558 validate_handle_grant_frame("r1:handle", "g1", &mismatched_scope).unwrap_err(),
5559 IpcError::InvalidField {
5560 field: "handle grant resource scope"
5561 }
5562 );
5563
5564 let mut missing_token = request.clone();
5565 missing_token.handle_grant_token.clear();
5566 assert_eq!(
5567 validate_handle_grant_frame("r1:handle", "g1", &missing_token).unwrap_err(),
5568 IpcError::InvalidField {
5569 field: "handle_grant_token"
5570 }
5571 );
5572 assert_eq!(
5573 validate_handle_grant_frame("", "g1", &request).unwrap_err(),
5574 IpcError::InvalidField {
5575 field: "request_id"
5576 }
5577 );
5578 assert_eq!(
5579 validate_handle_grant_frame("r1:handle", "g2", &request).unwrap_err(),
5580 IpcError::ProtocolViolation {
5581 message: "validate_handle_grant runtime_generation_id mismatch"
5582 }
5583 );
5584 let mut unsafe_revision = request;
5585 unsafe_revision.policy_revision = MAX_JSON_SAFE_INTEGER + 1;
5586 assert_eq!(
5587 validate_handle_grant_frame("r1:handle", "g1", &unsafe_revision).unwrap_err(),
5588 IpcError::InvalidField {
5589 field: "policy_revision"
5590 }
5591 );
5592 }
5593
5594 #[test]
5595 fn zero_revoke_epoch_returns_typed_errors() {
5596 let revoke = r#"{"ipc_version":"rust-ipc-v6","frame_type":"revoke_epoch","request_id":"r1","runtime_generation_id":"g1","payload":{"resource_scope":{"kind":"environment","owner_env_hash":"env_hash"},"plugin_instance_id":"plugini_1","revoke_epoch":0}}"#;
5597 assert_eq!(
5598 parse_revoke_epoch_request(revoke).unwrap_err(),
5599 IpcError::InvalidField {
5600 field: "revoke_epoch"
5601 }
5602 );
5603 let worker = closed_worker_frame(
5604 r#"{"runtime_shard_id":"runtime_shard_signed","policy_revision":1,"management_revision":2,"revoke_epoch":0}"#,
5605 r#"{"plugin_id":"com.example.worker","plugin_instance_id":"plugini_1","active_fingerprint":"sha256:active","runtime_instance_id":"runtime_1","runtime_generation_id":"g1","method":"worker.echo"}"#,
5606 );
5607 assert_eq!(
5608 parse_worker_invocation_context(&worker).unwrap_err(),
5609 IpcError::InvalidField {
5610 field: "revoke_epoch"
5611 }
5612 );
5613 assert_eq!(
5614 revoke_epoch_ack_result_json(&environment_resource_scope(), "plugini_1", 0, 0, 0, 0)
5615 .unwrap_err(),
5616 IpcError::InvalidField {
5617 field: "revoke_epoch"
5618 }
5619 );
5620
5621 let mut handle_grant = handle_grant_validation_request();
5622 handle_grant.revoke_epoch = 0;
5623 assert_eq!(
5624 validate_handle_grant_frame("r1:handle", "g1", &handle_grant).unwrap_err(),
5625 IpcError::InvalidField {
5626 field: "revoke_epoch"
5627 }
5628 );
5629
5630 let mut network_grant = NetworkGrantRequest {
5631 plugin_instance_id: "plugini_1".to_string(),
5632 active_fingerprint: "sha256:active".to_string(),
5633 resource_scope: user_resource_scope(),
5634 runtime_instance_id: "runtime_1".to_string(),
5635 runtime_generation_id: "g1".to_string(),
5636 runtime_shard_id: "runtime_shard_1".to_string(),
5637 policy_revision: 1,
5638 management_revision: 2,
5639 revoke_epoch: 0,
5640 connector_id: "api".to_string(),
5641 transport: "http".to_string(),
5642 destination: "https://api.example.com".to_string(),
5643 ttl_ms: 30000,
5644 };
5645 assert_eq!(
5646 network_grant_frame("r1:network_grant", "g1", &network_grant).unwrap_err(),
5647 IpcError::InvalidField {
5648 field: "revoke_epoch"
5649 }
5650 );
5651 network_grant.revoke_epoch = 1;
5652 network_grant.resource_scope.owner_env_hash = " env_hash".to_string();
5653 assert_eq!(
5654 network_grant_frame("r1:network_grant", "g1", &network_grant).unwrap_err(),
5655 IpcError::InvalidField {
5656 field: "network resource scope"
5657 }
5658 );
5659 }
5660
5661 #[test]
5662 fn validates_handle_grant_response() {
5663 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"validate_handle_grant","request_id":"r1:handle","runtime_generation_id":"g1","payload":{"ok":true,"handle_grant_id":"h1","handle_id":"storage:db","method":"storage.sqlite","runtime_generation_id":"g1","resource_scope":{"kind":"user","owner_env_hash":"env_hash","owner_user_hash":"user_hash"},"max_total_bytes":4096}}"#;
5664 validate_handle_grant_response(
5665 frame,
5666 "r1:handle",
5667 "g1",
5668 "storage:db",
5669 "storage.sqlite",
5670 &user_resource_scope(),
5671 )
5672 .expect("valid handle grant");
5673 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"validate_handle_grant","request_id":"r1:handle","runtime_generation_id":"g1","payload":{"ok":false,"code":"HANDLE_GRANT_VALIDATION_FAILED","message":"denied","error_origin":"hostcall"}}"#;
5674 let err = validate_handle_grant_response(
5675 failed,
5676 "r1:handle",
5677 "g1",
5678 "storage:db",
5679 "storage.sqlite",
5680 &user_resource_scope(),
5681 )
5682 .expect_err("failed handle grant response");
5683 assert_eq!(
5684 err,
5685 IpcError::RemoteFailure {
5686 code: "HANDLE_GRANT_VALIDATION_FAILED".to_string()
5687 }
5688 );
5689 }
5690
5691 #[test]
5692 fn renders_storage_file_frame() {
5693 let mut req = StorageFileRequest {
5694 handle_grant_token: "handle_grant.secret".to_string(),
5695 plugin_instance_id: "plugini_1".to_string(),
5696 active_fingerprint: "sha256:active".to_string(),
5697 runtime_instance_id: "runtime_1".to_string(),
5698 runtime_generation_id: "g1".to_string(),
5699 runtime_shard_id: "runtime_shard_1".to_string(),
5700 handle_id: "storage:workspace".to_string(),
5701 method: "storage.files".to_string(),
5702 resource_scope: user_resource_scope(),
5703 policy_revision: 1,
5704 management_revision: 2,
5705 revoke_epoch: 3,
5706 operation: "read".to_string(),
5707 store_id: "workspace".to_string(),
5708 path: "notes/today.txt".to_string(),
5709 data_base64: "".to_string(),
5710 max_bytes: 1024,
5711 max_entries: 10,
5712 recursive: false,
5713 };
5714 let frame =
5715 storage_file_frame("r1:storage_file", "g1", &req).expect("valid storage file frame");
5716 assert!(frame.contains(r#""frame_type":"storage_file""#));
5717 assert!(frame.contains(r#""handle_id":"storage:workspace""#));
5718 assert!(frame.contains(r#""method":"storage.files""#));
5719 assert!(frame.contains(r#""operation":"read""#));
5720 req.policy_revision = MAX_JSON_SAFE_INTEGER + 1;
5721 assert_eq!(
5722 storage_file_frame("r1:storage_file", "g1", &req).unwrap_err(),
5723 IpcError::InvalidField {
5724 field: "policy_revision"
5725 }
5726 );
5727 }
5728
5729 #[test]
5730 fn validates_storage_file_response() {
5731 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_file","request_id":"r1:storage_file","runtime_generation_id":"g1","payload":{"ok":true,"path":"notes/today.txt","data_base64":"aGVsbG8=","size_bytes":5,"usage":{"plugin_instance_id":"plugini_1","store_id":"workspace","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}}}"#;
5732 validate_storage_file_response(frame, "r1:storage_file", "g1", "read")
5733 .expect("valid storage file response");
5734 let payload = storage_file_payload_json(frame, "read").expect("storage file payload");
5735 assert!(payload.contains(r#""path":"notes/today.txt""#));
5736 for wrong_operation in ["write", "delete", "list"] {
5737 assert!(
5738 validate_storage_file_response(frame, "r1:storage_file", "g1", wrong_operation)
5739 .is_err(),
5740 "read response accepted as {wrong_operation}"
5741 );
5742 }
5743 let mixed = frame.replace(r#""usage":{"#, r#""entries":[],"usage":{"#);
5744 assert!(validate_storage_file_response(&mixed, "r1:storage_file", "g1", "read").is_err());
5745 let missing = without_payload_field(frame, "data_base64");
5746 assert!(validate_storage_file_response(&missing, "r1:storage_file", "g1", "read").is_err());
5747 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_file","request_id":"r1:storage_file","runtime_generation_id":"g1","payload":{"ok":false,"code":"STORAGE_FILE_NOT_FOUND","message":"missing","error_origin":"hostcall"}}"#;
5748 let err = validate_storage_file_response(failed, "r1:storage_file", "g1", "read")
5749 .expect_err("failed storage file response");
5750 assert_eq!(
5751 err,
5752 IpcError::RemoteFailure {
5753 code: "STORAGE_FILE_NOT_FOUND".to_string()
5754 }
5755 );
5756 let missing_origin = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_file","request_id":"r1:storage_file","runtime_generation_id":"g1","payload":{"ok":false,"code":"STORAGE_FILE_NOT_FOUND","message":"missing"}}"#;
5757 let err = validate_storage_file_response(missing_origin, "r1:storage_file", "g1", "read")
5758 .expect_err("hostcall origin is required");
5759 assert_eq!(
5760 err,
5761 IpcError::DecodeFailed {
5762 context: "hostcall failure response payload"
5763 }
5764 );
5765 let spoofed_origin = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_file","request_id":"r1:storage_file","runtime_generation_id":"g1","payload":{"ok":false,"code":"STORAGE_FILE_NOT_FOUND","message":"missing","error_origin":"plugin"}}"#;
5766 let err = validate_storage_file_response(spoofed_origin, "r1:storage_file", "g1", "read")
5767 .expect_err("hostcall origin cannot be spoofed");
5768 assert_eq!(
5769 err,
5770 IpcError::ProtocolViolation {
5771 message: "hostcall response error_origin must be hostcall"
5772 }
5773 );
5774 }
5775
5776 #[test]
5777 fn validates_all_storage_file_success_operations() {
5778 let usage = r#"{"plugin_instance_id":"plugini_1","store_id":"workspace","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}"#;
5779 let cases = [
5780 (
5781 "read",
5782 format!(
5783 r#"{{"ok":true,"path":"a.txt","data_base64":"YQ==","size_bytes":1,"usage":{usage}}}"#
5784 ),
5785 ),
5786 (
5787 "write",
5788 format!(r#"{{"ok":true,"path":"a.txt","size_bytes":1,"usage":{usage}}}"#),
5789 ),
5790 ("delete", r#"{"ok":true,"path":"a.txt"}"#.to_string()),
5791 (
5792 "list",
5793 format!(r#"{{"ok":true,"path":"","entries":[],"usage":{usage}}}"#),
5794 ),
5795 ];
5796 for (operation, payload) in &cases {
5797 let frame = hostcall_response_frame(FRAME_TYPE_STORAGE_FILE, payload);
5798 validate_storage_file_response(&frame, "r1", "g1", operation)
5799 .unwrap_or_else(|err| panic!("{operation} response: {err}"));
5800 for (other, _) in &cases {
5801 if other != operation {
5802 assert!(validate_storage_file_response(&frame, "r1", "g1", other).is_err());
5803 }
5804 }
5805 }
5806 }
5807
5808 #[test]
5809 fn renders_storage_kv_frame() {
5810 let mut req = StorageKVRequest {
5811 handle_grant_token: "handle_grant.secret".to_string(),
5812 plugin_instance_id: "plugini_1".to_string(),
5813 active_fingerprint: "sha256:active".to_string(),
5814 runtime_instance_id: "runtime_1".to_string(),
5815 runtime_generation_id: "g1".to_string(),
5816 runtime_shard_id: "runtime_shard_1".to_string(),
5817 handle_id: "storage:settings".to_string(),
5818 method: "storage.kv".to_string(),
5819 resource_scope: user_resource_scope(),
5820 policy_revision: 1,
5821 management_revision: 2,
5822 revoke_epoch: 3,
5823 operation: "put".to_string(),
5824 store_id: "settings".to_string(),
5825 key: "demo/last_broker_run".to_string(),
5826 value_base64: "aGVsbG8=".to_string(),
5827 prefix: String::new(),
5828 max_bytes: 0,
5829 max_entries: 10,
5830 };
5831 let frame = storage_kv_frame("r1:storage_kv", "g1", &req).expect("valid storage kv frame");
5832 assert!(frame.contains(r#""frame_type":"storage_kv""#));
5833 assert!(frame.contains(r#""handle_id":"storage:settings""#));
5834 assert!(frame.contains(r#""method":"storage.kv""#));
5835 assert!(frame.contains(r#""operation":"put""#));
5836 req.management_revision = MAX_JSON_SAFE_INTEGER + 1;
5837 assert_eq!(
5838 storage_kv_frame("r1:storage_kv", "g1", &req).unwrap_err(),
5839 IpcError::InvalidField {
5840 field: "management_revision"
5841 }
5842 );
5843 assert!(frame.contains(r#""key":"demo/last_broker_run""#));
5844 }
5845
5846 #[test]
5847 fn validates_storage_kv_response() {
5848 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_kv","request_id":"r1:storage_kv","runtime_generation_id":"g1","payload":{"ok":true,"key":"demo/last_broker_run","value_base64":"aGVsbG8=","size_bytes":5,"usage":{"plugin_instance_id":"plugini_1","store_id":"settings","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}}}"#;
5849 validate_storage_kv_response(frame, "r1:storage_kv", "g1", "get")
5850 .expect("valid storage kv response");
5851 let payload = storage_kv_payload_json(frame, "get").expect("storage kv payload");
5852 assert!(payload.contains(r#""key":"demo/last_broker_run""#));
5853 for wrong_operation in ["put", "delete", "list"] {
5854 assert!(
5855 validate_storage_kv_response(frame, "r1:storage_kv", "g1", wrong_operation)
5856 .is_err()
5857 );
5858 }
5859 let mixed = frame.replace(r#""usage":{"#, r#""entries":[],"usage":{"#);
5860 assert!(validate_storage_kv_response(&mixed, "r1:storage_kv", "g1", "get").is_err());
5861 let missing = without_payload_field(frame, "value_base64");
5862 assert!(validate_storage_kv_response(&missing, "r1:storage_kv", "g1", "get").is_err());
5863 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_kv","request_id":"r1:storage_kv","runtime_generation_id":"g1","payload":{"ok":false,"code":"STORAGE_KV_NOT_FOUND","message":"missing","error_origin":"hostcall"}}"#;
5864 let err = validate_storage_kv_response(failed, "r1:storage_kv", "g1", "get")
5865 .expect_err("failed storage kv response");
5866 assert_eq!(
5867 err,
5868 IpcError::RemoteFailure {
5869 code: "STORAGE_KV_NOT_FOUND".to_string()
5870 }
5871 );
5872 }
5873
5874 #[test]
5875 fn validates_all_storage_kv_success_operations() {
5876 let usage = r#"{"plugin_instance_id":"plugini_1","store_id":"settings","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}"#;
5877 let cases = [
5878 (
5879 "get",
5880 format!(
5881 r#"{{"ok":true,"key":"theme","value_base64":"ZGFyaw==","size_bytes":4,"usage":{usage}}}"#
5882 ),
5883 ),
5884 (
5885 "put",
5886 format!(r#"{{"ok":true,"key":"theme","size_bytes":4,"usage":{usage}}}"#),
5887 ),
5888 ("delete", r#"{"ok":true,"key":"theme"}"#.to_string()),
5889 (
5890 "list",
5891 format!(r#"{{"ok":true,"prefix":"","entries":[],"usage":{usage}}}"#),
5892 ),
5893 ];
5894 for (operation, payload) in &cases {
5895 let frame = hostcall_response_frame(FRAME_TYPE_STORAGE_KV, payload);
5896 validate_storage_kv_response(&frame, "r1", "g1", operation)
5897 .unwrap_or_else(|err| panic!("{operation} response: {err}"));
5898 for (other, _) in &cases {
5899 if other != operation {
5900 assert!(validate_storage_kv_response(&frame, "r1", "g1", other).is_err());
5901 }
5902 }
5903 }
5904 }
5905
5906 #[test]
5907 fn renders_worker_success_with_storage_result() {
5908 let identity = WorkerInvocationIdentity {
5909 package_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5910 .to_string(),
5911 artifact: "workers/backend.wasm".to_string(),
5912 artifact_sha256:
5913 "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
5914 .to_string(),
5915 worker_id: "backend".to_string(),
5916 method: "worker.echo".to_string(),
5917 };
5918 let result = worker_success_result_json(
5919 &identity,
5920 42,
5921 Some(r#"{"ok":true,"path":"notes/from-wasm.txt","size_bytes":5}"#),
5922 Some(r#"{"ok":true,"key":"demo/last_broker_run","size_bytes":12}"#),
5923 Some(r#"{"ok":true,"database":"plugin.sqlite","rows_affected":1}"#),
5924 Some(r#"{"ok":true,"transport":"http","status_code":201,"stream_id":"stream_http_1"}"#),
5925 );
5926 assert!(result.contains(r#""storage_file":{"ok":true"#));
5927 assert!(result.contains(r#""storage_kv":{"ok":true"#));
5928 assert!(result.contains(r#""storage_sqlite":{"ok":true"#));
5929 assert!(result.contains(r#""network_execute":{"ok":true"#));
5930 assert!(result.contains(r#""stream_id":"stream_http_1""#));
5931 assert!(result.contains(r#""wasm_byte_len":42"#));
5932 }
5933
5934 #[test]
5935 fn renders_storage_sqlite_frame() {
5936 let mut req = StorageSQLiteRequest {
5937 handle_grant_token: "handle_grant.secret".to_string(),
5938 plugin_instance_id: "plugini_1".to_string(),
5939 active_fingerprint: "sha256:active".to_string(),
5940 runtime_instance_id: "runtime_1".to_string(),
5941 runtime_generation_id: "g1".to_string(),
5942 runtime_shard_id: "runtime_shard_1".to_string(),
5943 handle_id: "storage:db".to_string(),
5944 method: "storage.sqlite".to_string(),
5945 resource_scope: user_resource_scope(),
5946 policy_revision: 1,
5947 management_revision: 2,
5948 revoke_epoch: 3,
5949 operation: "query".to_string(),
5950 store_id: "db".to_string(),
5951 database: "plugin.sqlite".to_string(),
5952 sql: "SELECT title FROM events WHERE score = ?".to_string(),
5953 args_json: r#"[{"int":7}]"#.to_string(),
5954 max_rows: 10,
5955 max_response_bytes: 4096,
5956 timeout_ms: 1000,
5957 };
5958 let frame = storage_sqlite_frame("r1:storage_sqlite", "g1", &req)
5959 .expect("valid storage sqlite frame");
5960 assert!(frame.contains(r#""frame_type":"storage_sqlite""#));
5961 assert!(frame.contains(r#""handle_id":"storage:db""#));
5962 assert!(frame.contains(r#""method":"storage.sqlite""#));
5963 assert!(frame.contains(r#""operation":"query""#));
5964 assert!(frame.contains(r#""args":[{"int":7}]"#));
5965 req.policy_revision = MAX_JSON_SAFE_INTEGER + 1;
5966 assert_eq!(
5967 storage_sqlite_frame("r1:storage_sqlite", "g1", &req).unwrap_err(),
5968 IpcError::InvalidField {
5969 field: "policy_revision"
5970 }
5971 );
5972 }
5973
5974 #[test]
5975 fn validates_storage_sqlite_response() {
5976 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_sqlite","request_id":"r1:storage_sqlite","runtime_generation_id":"g1","payload":{"ok":true,"database":"plugin.sqlite","columns":["title"],"rows":[[{"text":"stored from wasm"}]],"usage":{"plugin_instance_id":"plugini_1","store_id":"db","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}}}"#;
5977 validate_storage_sqlite_response(frame, "r1:storage_sqlite", "g1", "query")
5978 .expect("valid storage sqlite response");
5979 let payload = storage_sqlite_payload_json(frame, "query").expect("storage sqlite payload");
5980 assert!(payload.contains(r#""database":"plugin.sqlite""#));
5981 assert!(
5982 validate_storage_sqlite_response(frame, "r1:storage_sqlite", "g1", "exec").is_err()
5983 );
5984 let mixed = frame.replace(
5985 r#""columns":["title"]"#,
5986 r#""rows_affected":1,"columns":["title"]"#,
5987 );
5988 assert!(
5989 validate_storage_sqlite_response(&mixed, "r1:storage_sqlite", "g1", "query").is_err()
5990 );
5991 let missing = without_payload_field(frame, "rows");
5992 assert!(
5993 validate_storage_sqlite_response(&missing, "r1:storage_sqlite", "g1", "query").is_err()
5994 );
5995 let empty_blob = frame.replace(r#"{"text":"stored from wasm"}"#, r#"{"blob_base64":""}"#);
5996 validate_storage_sqlite_response(&empty_blob, "r1:storage_sqlite", "g1", "query")
5997 .expect("empty SQLite blob is a valid typed value");
5998 for invalid_value in [
5999 r#"{}"#,
6000 r#"{"null":false}"#,
6001 r#"{"int":1,"text":"ambiguous"}"#,
6002 ] {
6003 let invalid = frame.replace(r#"{"text":"stored from wasm"}"#, invalid_value);
6004 assert!(
6005 validate_storage_sqlite_response(&invalid, "r1:storage_sqlite", "g1", "query")
6006 .is_err(),
6007 "accepted invalid SQLite value {invalid_value}"
6008 );
6009 }
6010 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"storage_sqlite","request_id":"r1:storage_sqlite","runtime_generation_id":"g1","payload":{"ok":false,"code":"STORAGE_SQLITE_RESULT_TOO_LARGE","message":"too large","error_origin":"hostcall"}}"#;
6011 let err = validate_storage_sqlite_response(failed, "r1:storage_sqlite", "g1", "query")
6012 .expect_err("failed storage sqlite response");
6013 assert_eq!(
6014 err,
6015 IpcError::RemoteFailure {
6016 code: "STORAGE_SQLITE_RESULT_TOO_LARGE".to_string()
6017 }
6018 );
6019 }
6020
6021 #[test]
6022 fn validates_all_storage_sqlite_success_operations() {
6023 let usage = r#"{"plugin_instance_id":"plugini_1","store_id":"db","usage_bytes":5,"quota_bytes":100,"usage_files":1,"quota_files":10}"#;
6024 let cases = [
6025 (
6026 "exec",
6027 format!(
6028 r#"{{"ok":true,"database":"plugin.sqlite","rows_affected":0,"usage":{usage}}}"#
6029 ),
6030 ),
6031 (
6032 "query",
6033 format!(
6034 r#"{{"ok":true,"database":"plugin.sqlite","columns":[],"rows":[],"usage":{usage}}}"#
6035 ),
6036 ),
6037 ];
6038 for (operation, payload) in &cases {
6039 let frame = hostcall_response_frame(FRAME_TYPE_STORAGE_SQLITE, payload);
6040 validate_storage_sqlite_response(&frame, "r1", "g1", operation)
6041 .unwrap_or_else(|err| panic!("{operation} response: {err}"));
6042 for (other, _) in &cases {
6043 if other != operation {
6044 assert!(validate_storage_sqlite_response(&frame, "r1", "g1", other).is_err());
6045 }
6046 }
6047 }
6048 }
6049
6050 #[test]
6051 fn renders_network_grant_frame() {
6052 let mut req = NetworkGrantRequest {
6053 plugin_instance_id: "plugini_1".to_string(),
6054 active_fingerprint: "sha256:active".to_string(),
6055 resource_scope: NetworkResourceScope {
6056 kind: "user".to_string(),
6057 owner_env_hash: "env_hash".to_string(),
6058 owner_user_hash: "user_hash".to_string(),
6059 },
6060 runtime_instance_id: "runtime_1".to_string(),
6061 runtime_generation_id: "g1".to_string(),
6062 runtime_shard_id: "runtime_shard_1".to_string(),
6063 policy_revision: 1,
6064 management_revision: 2,
6065 revoke_epoch: 3,
6066 connector_id: "api".to_string(),
6067 transport: "http".to_string(),
6068 destination: "https://api.example.com".to_string(),
6069 ttl_ms: 30000,
6070 };
6071 let frame =
6072 network_grant_frame("r1:network_grant", "g1", &req).expect("network grant frame");
6073 assert!(frame.contains(r#""frame_type":"network_grant""#));
6074 assert!(frame.contains(r#""connector_id":"api""#));
6075 assert!(frame.contains(r#""transport":"http""#));
6076 assert!(frame.contains(r#""resource_scope":{"kind":"user","owner_env_hash":"env_hash","owner_user_hash":"user_hash"}"#));
6077 assert!(frame.contains(r#""ttl_ms":30000"#));
6078 req.management_revision = MAX_JSON_SAFE_INTEGER + 1;
6079 assert_eq!(
6080 network_grant_frame("r1:network_grant", "g1", &req).unwrap_err(),
6081 IpcError::InvalidField {
6082 field: "management_revision"
6083 }
6084 );
6085 }
6086
6087 #[test]
6088 fn validates_network_grant_response() {
6089 let scope = NetworkResourceScope {
6090 kind: "user".to_string(),
6091 owner_env_hash: "env_hash".to_string(),
6092 owner_user_hash: "user_hash".to_string(),
6093 };
6094 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"network_grant","request_id":"r1:network_grant","runtime_generation_id":"g1","payload":{"ok":true,"grant_id":"netgrant_00112233445566778899aabbccddeeff","plugin_instance_id":"plugini_1","active_fingerprint":"sha256:active","resource_scope":{"kind":"user","owner_env_hash":"env_hash","owner_user_hash":"user_hash"},"policy_revision":1,"management_revision":2,"revoke_epoch":3,"connector_id":"api","transport":"http","destination":{"transport":"http","scheme":"https","host":"api.example.com","port":443},"runtime_generation_id":"g1","target_classifier_version":"target-classifier-v2","expires_at":"2026-06-30T10:00:30Z"}}"#;
6095 validate_network_grant_response(frame, "r1:network_grant", "g1", "api", "http", &scope)
6096 .expect("valid network grant response");
6097 let unsafe_revision = frame.replace(
6098 r#""policy_revision":1"#,
6099 r#""policy_revision":9007199254740992"#,
6100 );
6101 assert_eq!(
6102 validate_network_grant_response(
6103 &unsafe_revision,
6104 "r1:network_grant",
6105 "g1",
6106 "api",
6107 "http",
6108 &scope,
6109 )
6110 .unwrap_err(),
6111 IpcError::InvalidField {
6112 field: "policy_revision"
6113 }
6114 );
6115 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"network_grant","request_id":"r1:network_grant","runtime_generation_id":"g1","payload":{"ok":false,"code":"NETWORK_TARGET_DENIED","message":"blocked","error_origin":"hostcall"}}"#;
6116 let err = validate_network_grant_response(
6117 failed,
6118 "r1:network_grant",
6119 "g1",
6120 "api",
6121 "http",
6122 &scope,
6123 )
6124 .expect_err("failed network grant response");
6125 assert_eq!(
6126 err,
6127 IpcError::RemoteFailure {
6128 code: "NETWORK_TARGET_DENIED".to_string()
6129 }
6130 );
6131 }
6132
6133 #[test]
6134 fn renders_network_execute_frame() {
6135 let mut req = NetworkExecuteRequest {
6136 plugin_id: "com.example.worker".to_string(),
6137 plugin_instance_id: "plugini_1".to_string(),
6138 active_fingerprint: "sha256:active".to_string(),
6139 resource_scope: NetworkResourceScope {
6140 kind: "user".to_string(),
6141 owner_env_hash: "env_hash".to_string(),
6142 owner_user_hash: "user_hash".to_string(),
6143 },
6144 runtime_instance_id: "runtime_1".to_string(),
6145 runtime_generation_id: "g1".to_string(),
6146 runtime_shard_id: "runtime_shard_1".to_string(),
6147 policy_revision: 1,
6148 management_revision: 2,
6149 revoke_epoch: 3,
6150 connector_id: "api".to_string(),
6151 transport: "http".to_string(),
6152 destination: "https://api.example.com".to_string(),
6153 ttl_ms: 30000,
6154 operation: "http".to_string(),
6155 method: "POST".to_string(),
6156 path: "/v1/worker".to_string(),
6157 query_json: r#"{"lang":["en"],"units":["metric"]}"#.to_string(),
6158 headers_json: r#"{"X-Test":["ok"]}"#.to_string(),
6159 message_type: "".to_string(),
6160 body_base64: "e30=".to_string(),
6161 payload_base64: "".to_string(),
6162 max_request_bytes: 1024,
6163 max_response_bytes: 2048,
6164 max_chunk_bytes: 256,
6165 max_buffered_bytes: 65536,
6166 timeout_ms: 2000,
6167 stream_id: "stream_1".to_string(),
6168 stream_method: "worker.echo".to_string(),
6169 stream_effect: "read".to_string(),
6170 stream_execution: "subscription".to_string(),
6171 surface_instance_id: "surface_1".to_string(),
6172 owner_session_hash: "session_hash".to_string(),
6173 owner_user_hash: "user_hash".to_string(),
6174 owner_env_hash: "env_hash".to_string(),
6175 session_channel_id_hash: "channel_hash".to_string(),
6176 bridge_channel_id: "bridge_1".to_string(),
6177 content_type: "text/plain".to_string(),
6178 };
6179 let frame =
6180 network_execute_frame("r1:network_execute", "g1", &req).expect("network execute frame");
6181 assert!(frame.contains(r#""frame_type":"network_execute""#));
6182 assert!(frame.contains(r#""operation":"http""#));
6183 assert!(frame.contains(r#""headers":{"X-Test":["ok"]}"#));
6184 assert!(frame.contains(r#""query":{"lang":["en"],"units":["metric"]}"#));
6185 req.policy_revision = MAX_JSON_SAFE_INTEGER + 1;
6186 assert_eq!(
6187 network_execute_frame("r1:network_execute", "g1", &req).unwrap_err(),
6188 IpcError::InvalidField {
6189 field: "policy_revision"
6190 }
6191 );
6192 req.policy_revision = 1;
6193 assert!(frame.contains(r#""body_base64":"e30=""#));
6194 assert!(frame.contains(r#""stream_id":"stream_1""#));
6195 assert!(frame.contains(r#""owner_session_hash":"session_hash""#));
6196 assert!(frame.contains(r#""max_chunk_bytes":256"#));
6197 assert!(frame.contains(r#""timeout_ms":2000"#));
6198
6199 req.query_json = "[]".to_string();
6200 assert_eq!(
6201 network_execute_frame("r1:network_execute", "g1", &req),
6202 Err(IpcError::InvalidField {
6203 field: "network execute query"
6204 })
6205 );
6206 req.query_json = "{}".to_string();
6207 req.headers_json = "[".to_string();
6208 assert_eq!(
6209 network_execute_frame("r1:network_execute", "g1", &req),
6210 Err(IpcError::InvalidField {
6211 field: "network execute headers"
6212 })
6213 );
6214 req.headers_json = "{}".to_string();
6215 req.resource_scope.owner_user_hash.clear();
6216 assert_eq!(
6217 network_execute_frame("r1:network_execute", "g1", &req),
6218 Err(IpcError::InvalidField {
6219 field: "network resource scope"
6220 })
6221 );
6222 }
6223
6224 #[test]
6225 fn validates_network_execute_response() {
6226 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"network_execute","request_id":"r1:network_execute","runtime_generation_id":"g1","payload":{"ok":true,"transport":"http","destination":{"transport":"http","scheme":"https","host":"api.example.com","port":443},"status_code":201,"headers":{"X-Worker":["ok"]},"body_base64":"e30=","grant_id":"netgrant_00112233445566778899aabbccddeeff","connector_id":"api","runtime_generation_id":"g1"}}"#;
6227 validate_network_execute_response(frame, "r1:network_execute", "g1", "api", "http")
6228 .expect("valid network execute response");
6229 let failed = r#"{"ipc_version":"rust-ipc-v6","frame_type":"network_execute","request_id":"r1:network_execute","runtime_generation_id":"g1","payload":{"ok":false,"code":"NETWORK_RESPONSE_TOO_LARGE","message":"too large","error_origin":"hostcall"}}"#;
6230 let err =
6231 validate_network_execute_response(failed, "r1:network_execute", "g1", "api", "http")
6232 .expect_err("failed network execute response");
6233 assert_eq!(
6234 err,
6235 IpcError::RemoteFailure {
6236 code: "NETWORK_RESPONSE_TOO_LARGE".to_string()
6237 }
6238 );
6239 }
6240
6241 #[test]
6242 fn parses_worker_invocation_identity() {
6243 let frame = closed_worker_frame(
6244 "{}",
6245 r#"{"package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/backend.wasm","artifact_sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worker_id":"backend","method":"worker.echo"}"#,
6246 );
6247 let identity = parse_worker_invocation_identity(&frame).expect("valid invocation");
6248 assert_eq!(
6249 identity.package_hash,
6250 "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
6251 );
6252 assert_eq!(identity.artifact, "workers/backend.wasm");
6253 assert_eq!(identity.worker_id, "backend");
6254 }
6255
6256 #[test]
6257 fn validates_worker_artifact_content_hash() {
6258 let identity = WorkerInvocationIdentity {
6259 package_hash: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
6260 .to_string(),
6261 artifact: "workers/backend.wasm".to_string(),
6262 artifact_sha256:
6263 "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
6264 .to_string(),
6265 worker_id: "backend".to_string(),
6266 method: "worker.echo".to_string(),
6267 };
6268 validate_worker_artifact_bytes(&identity, b"hello").unwrap();
6269 assert!(validate_worker_artifact_bytes(&identity, b"tampered").is_err());
6270 }
6271
6272 #[test]
6273 fn rejects_unsupported_worker_runtime_contract() {
6274 let valid = runtime_lease_invocation_fixture();
6275 parse_worker_invocation(valid)
6276 .unwrap()
6277 .validate_worker_contract()
6278 .unwrap();
6279 let invalid = valid.replacen(
6280 "redevplugin-wasm-worker-v2",
6281 "redevplugin-wasm-worker-v99",
6282 1,
6283 );
6284 assert!(
6285 parse_worker_invocation(&invalid)
6286 .unwrap()
6287 .validate_worker_contract()
6288 .is_err()
6289 );
6290 }
6291
6292 #[test]
6293 fn projects_closed_worker_request_v2() {
6294 let frame = r#"{"ipc_version":"rust-ipc-v6","frame_type":"invoke_worker","request_id":"r1","runtime_generation_id":"g1","payload":{"lease":{},"method":"notes.save","invocation":{"plugin_id":"com.example.notes","plugin_instance_id":"plugini_1","storage_handle_grants":{"notes":"handle-secret"},"method":"notes.save","params":{"title":"Launch notes","body":"Ship the examples"}}}}"#;
6295
6296 let request = worker_request_json_v2(frame).expect("worker request projection");
6297
6298 assert_eq!(
6299 request,
6300 r#"{"schema_version":"redevplugin.worker_request.v2","method":"notes.save","params":{"body":"Ship the examples","title":"Launch notes"}}"#
6301 );
6302 assert!(!request.contains("handle-secret"));
6303 assert!(!request.contains("plugin_instance_id"));
6304 }
6305
6306 #[test]
6307 fn reads_positive_worker_memory_limit_from_signed_lease() {
6308 let frame = closed_worker_frame(
6309 r#"{"limits":{"memory_bytes":33554432}}"#,
6310 r#"{"method":"worker.echo"}"#,
6311 );
6312 assert_eq!(
6313 runtime_lease_memory_limit_bytes(&frame).expect("memory limit"),
6314 33_554_432
6315 );
6316 for lease in [
6317 r#"{"limits":{}}"#,
6318 r#"{"limits":{"memory_bytes":0}}"#,
6319 r#"{"limits":{"memory_bytes":268435457}}"#,
6320 ] {
6321 assert!(
6322 runtime_lease_memory_limit_bytes(&closed_worker_frame(
6323 lease,
6324 r#"{"method":"worker.echo"}"#,
6325 ))
6326 .is_err()
6327 );
6328 }
6329 }
6330
6331 #[test]
6332 fn read_effect_rejects_mutating_storage_broker_operations() {
6333 for operation in ["write", "delete", "put", "exec"] {
6334 let frame = format!(
6335 r#"{{"effect":"read","broker_access":{{"storage":[{{"store_id":"store","scope":"user","operations":["{operation}"]}}]}}}}"#
6336 );
6337 let frame = closed_worker_frame("{}", &frame);
6338 let err = validate_worker_storage_broker_access(&frame, "store", operation)
6339 .expect_err("read methods must not mutate storage");
6340 assert_eq!(
6341 err,
6342 IpcError::ProtocolViolation {
6343 message: "worker method with read effect cannot perform the storage mutation"
6344 }
6345 );
6346 }
6347 }
6348
6349 #[test]
6350 fn read_effect_allows_declared_http_post_network_request() {
6351 let frame = closed_worker_frame(
6352 "{}",
6353 r#"{"effect":"read","broker_access":{"network":[{"connector_id":"search","transport":"http","scope":"user","operations":["http"],"http_methods":["POST"]}]}}"#,
6354 );
6355 validate_worker_network_broker_access(&frame, "search", "http", "http", "POST")
6356 .expect("HTTP verbs do not define method effect");
6357 }
6358
6359 #[test]
6360 fn parses_worker_response_v2_success_and_rejects_extra_authority() {
6361 let success =
6362 parse_worker_response_v2(r#"{"ok":true,"data":{"saved":true,"id":"note_1"}}"#)
6363 .expect("worker success response");
6364 assert_eq!(
6365 success,
6366 WorkerResponseV2::Success(r#"{"saved":true,"id":"note_1"}"#.to_string())
6367 );
6368
6369 let error = parse_worker_response_v2(
6370 r#"{"ok":true,"data":{"saved":true},"gateway_token":"secret"}"#,
6371 )
6372 .expect_err("extra response authority must fail closed");
6373 assert_eq!(
6374 error,
6375 IpcError::DecodeFailed {
6376 context: "worker response"
6377 }
6378 );
6379 assert!(!error.to_string().contains("gateway_token"));
6380 assert!(!error.to_string().contains("secret"));
6381 }
6382
6383 #[test]
6384 fn worker_response_v2_enforces_closed_success_and_failure_branches() {
6385 let failure = parse_worker_response_v2(
6386 r#"{"ok":false,"error_code":"WORKER_FAILED","message":"failed"}"#,
6387 )
6388 .expect("worker failure response");
6389 assert_eq!(
6390 failure,
6391 WorkerResponseV2::Failure {
6392 code: "WORKER_FAILED".to_string(),
6393 message: "failed".to_string(),
6394 }
6395 );
6396
6397 for invalid in [
6398 r#"{"ok":true}"#,
6399 r#"{"ok":true,"data":{},"error_code":"WORKER_FAILED"}"#,
6400 r#"{"ok":false,"data":{},"error_code":"WORKER_FAILED","message":"failed"}"#,
6401 r#"{"ok":false,"message":"failed"}"#,
6402 r#"{"ok":false,"error_code":"WORKER_FAILED"}"#,
6403 ] {
6404 assert!(
6405 parse_worker_response_v2(invalid).is_err(),
6406 "accepted ambiguous worker response {invalid}"
6407 );
6408 }
6409 }
6410
6411 #[test]
6412 fn worker_response_v2_preserves_large_raw_success_payload() {
6413 let payload = "x".repeat(512 * 1024 - 64);
6414 let input = format!(r#"{{"ok":true,"data":{{"payload":"{payload}"}}}}"#);
6415 let response = parse_worker_response_v2(&input).expect("large worker response");
6416 match response {
6417 WorkerResponseV2::Success(data) => {
6418 assert_eq!(data.len(), payload.len() + r#"{"payload":""}"#.len());
6419 assert!(data.ends_with("\"}"));
6420 }
6421 WorkerResponseV2::Failure { .. } => panic!("expected success response"),
6422 }
6423 }
6424
6425 #[test]
6426 fn rejects_worker_invocation_without_artifact_identity() {
6427 let frame = closed_worker_frame("{}", r#"{"artifact":"../backend.wasm"}"#);
6428 let err = parse_worker_invocation_identity(&frame).expect_err("invalid invocation");
6429 assert_eq!(
6430 err,
6431 IpcError::MissingField {
6432 field: "package_hash"
6433 }
6434 );
6435 let frame = closed_worker_frame(
6436 "{}",
6437 r#"{"package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/../backend.wasm","artifact_sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worker_id":"backend","method":"worker.echo"}"#,
6438 );
6439 let err = parse_worker_invocation_identity(&frame).expect_err("invalid artifact");
6440 assert_eq!(err, IpcError::InvalidField { field: "artifact" });
6441 }
6442
6443 #[test]
6444 fn parses_worker_lease_replay_key() {
6445 let input = closed_worker_frame(
6446 r#"{"lease_id":"lease_1","lease_nonce":"nonce_1","expires_at_unix_ms":2000}"#,
6447 r#"{"method":"worker.echo"}"#,
6448 );
6449 let key = parse_worker_lease_replay_key(&input).expect("valid replay key");
6450 assert_eq!(key.lease_id, "lease_1");
6451 assert_eq!(key.lease_nonce, "nonce_1");
6452 assert_eq!(key.expires_at_unix_ms, 2_000);
6453 }
6454
6455 #[test]
6456 fn rejects_worker_lease_replay_key_without_nonce() {
6457 let input = closed_worker_frame(r#"{"lease_id":"lease_1"}"#, r#"{"method":"worker.echo"}"#);
6458 let err = parse_worker_lease_replay_key(&input).expect_err("missing nonce should fail");
6459 assert_eq!(
6460 err,
6461 IpcError::MissingField {
6462 field: "lease_nonce"
6463 }
6464 );
6465 }
6466
6467 fn runtime_lease_signing_key_for_test(seed_byte: u8) -> SigningKey {
6468 SigningKey::from_bytes(&[seed_byte; 32])
6469 }
6470
6471 fn runtime_lease_invocation_fixture() -> &'static str {
6472 include_str!("../testdata/runtime-lease-signature-v1-invocation.json")
6473 }
6474
6475 fn signed_runtime_lease_invocation_for_test(
6476 signing_key: &SigningKey,
6477 replace: Option<(&str, &str)>,
6478 ) -> String {
6479 let mut lease = serde_json::json!({
6480 "lease_id": "rel_lease_signature",
6481 "token_id": "rel_token_signature",
6482 "lease_nonce": "nonce_1234567890",
6483 "runtime_generation_id": "rtgen_1",
6484 "plugin_instance_id": "plugini_1",
6485 "plugin_id": "com.example.worker",
6486 "plugin_version": "1.2.3",
6487 "active_fingerprint": "sha256:active",
6488 "issued_at_unix_ms": 1783161900000_i64,
6489 "method": "worker.echo",
6490 "effect": "read",
6491 "execution": "sync",
6492 "audit_correlation_id": "audit_lease_signature",
6493 "surface_instance_id": "surface_runtime",
6494 "owner_session_hash": "session_hash",
6495 "owner_user_hash": "user_hash",
6496 "owner_env_hash": "env_hash",
6497 "session_channel_id_hash": "channel_hash",
6498 "bridge_channel_id": "bridge_runtime",
6499 "target_descriptor_hashes": [
6500 "method:sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
6501 "worker:sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
6502 ],
6503 "limits": {
6504 "timeout_ms": 2000,
6505 "memory_bytes": 65536,
6506 "max_payload_bytes": 4096,
6507 "max_stream_bytes_per_sec": 1024
6508 },
6509 "policy_revision": 11,
6510 "management_revision": 12,
6511 "revoke_epoch": 13,
6512 "runtime_shard_id": "rtshard_1",
6513 "runtime_instance_id": "rtinst_1",
6514 "ipc_channel_id": "ipc_1",
6515 "connection_nonce": "connection_nonce_1234567890",
6516 "key_id": "host_ephemeral_key_1",
6517 "expires_at_unix_ms": 1783161930000_i64
6518 });
6519 let payload =
6520 runtime_lease_signature_payload_json(&worker_lease_from_value(&lease), "worker.echo")
6521 .expect("payload");
6522 let signature = signing_key.sign(payload.as_bytes());
6523 lease["signature"] = serde_json::Value::String(format!(
6524 "ed25519:{}",
6525 base64::engine::general_purpose::STANDARD.encode(signature.to_bytes())
6526 ));
6527 if let Some((key, value)) = replace {
6528 let parsed = value.parse::<u64>().expect("numeric replacement");
6529 lease[key] = serde_json::Value::Number(parsed.into());
6530 }
6531 format!(
6532 r#"{{"ipc_version":"rust-ipc-v6","frame_type":"invoke_worker","request_id":"r1","runtime_generation_id":"rtgen_1","payload":{{"lease":{},"method":"worker.echo","invocation":{{"package_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","artifact":"workers/backend.wasm","artifact_sha256":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","worker_id":"backend","method":"worker.echo"}}}}}}"#,
6533 serde_json::to_string(&lease).expect("lease json")
6534 )
6535 }
6536}