Skip to main content

redevplugin_ipc/
lib.rs

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    /// Returns the exact session scope when the invocation is session-bound.
1007    /// Background invocations may omit both session-specific hashes. A partial
1008    /// session identity is rejected instead of being treated as unscoped.
1009    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}