diff --git a/apps/system/components/SDKRuntime/kata-sdk-component/src/run.rs b/apps/system/components/SDKRuntime/kata-sdk-component/src/run.rs
index 7c492c5..aa8caf3 100644
--- a/apps/system/components/SDKRuntime/kata-sdk-component/src/run.rs
+++ b/apps/system/components/SDKRuntime/kata-sdk-component/src/run.rs
@@ -48,7 +48,6 @@ use log::error;
 use sdk_interface::KeyValueData;
 use sdk_interface::SDKAppId;
 use sdk_interface::SDKError;
-use sdk_interface::SDKReplyHeader;
 use sdk_interface::SDKRuntimeError;
 use sdk_interface::SDKRuntimeInterface;
 use sdk_interface::SDKRuntimeRequest;
@@ -140,20 +139,10 @@ fn delete_path(path: &seL4_CPath) -> seL4_Result {
     unsafe { seL4_CNode_Delete(path.0, path.1, path.2 as u8) }
 }
 
-fn reply_error(error: SDKError, reply_slice: &mut [u8]) {
-    // XXX check return
-    let _ = postcard::to_slice(
-        &SDKReplyHeader {
-            status: error.into(),
-        },
-        reply_slice,
-    );
-}
-
 /// Server-side of SDKRuntime request processing.  Note CAmkES does not
 /// participate in the RPC processing we use the control thread instead
-/// of having CAmkES create an interface thread and pass parameters through
-/// a page frame attached to the IPC buffer.
+/// of having CAmkES create an interface thread, and pass parameters
+/// through a page frame attached to the IPC buffer.
 #[no_mangle]
 pub unsafe extern "C" fn run() -> ! {
     let recv_path = &Camkes::top_level_path(KATA_SDK_RECV_SLOT);
@@ -164,7 +153,12 @@ pub unsafe extern "C" fn run() -> ! {
 
     // Do initial Recv; after this we use ReplyRecv to minimize syscalls.
     let mut sdk_runtime_badge: seL4_Word = 0;
-    seL4_Recv(KATA_SDK_ENDPOINT, &mut sdk_runtime_badge as _, KATA_SDK_REPLY);
+    let mut response: Result<(), SDKError>;
+    let mut info = seL4_Recv(
+        /*src=*/ KATA_SDK_ENDPOINT,
+        /*sender=*/ &mut sdk_runtime_badge as _,
+        /*reply=*/ KATA_SDK_REPLY,
+    );
     loop {
         Camkes::debug_assert_slot_frame("run", recv_path);
         // seL4_Recv & seL4_ReplyRecv return any badge but do not reset
@@ -173,46 +167,58 @@ pub unsafe extern "C" fn run() -> ! {
         // outbound capability. To guard against this clear the field here
         // (so it happens for both calls) with clear_request_cap().
         Camkes::clear_request_cap();
-        // Map the frame with RPC parameters and decode the request header.
+        // Map the frame with RPC parameters and process the request.
         if copy_region.map(recv_path.1).is_ok() {
-            // The client serializes an SDKRequestHeader first with the
-            // request id. This is followed by request-specific arguments
-            // that must be processed by each handler.
+            // The request token is passed in the MessageInfo label field.
+            // Any request-specific parameters are serialized in the first
+            // half of the page, with the second half reserved for reply data.
+            // We might consider sending a request length out-of-band (like
+            // the request token) to enable variable page splitting.
             let (request_slice, reply_slice) = copy_region
                 .as_mut()
                 .split_at_mut(SDKRUNTIME_REQUEST_DATA_SIZE);
             let request_slice = &*request_slice; // NB: immutable alias
-            match postcard::take_from_bytes::<sdk_interface::SDKRequestHeader>(request_slice) {
-                Ok((header, args_slice)) => {
-                    let app_id = sdk_runtime_badge as SDKAppId; // XXX safe?
-                    if let Err(status) = match header.request {
-                        SDKRuntimeRequest::Ping => ping_request(app_id, args_slice, reply_slice),
-                        SDKRuntimeRequest::Log => log_request(app_id, args_slice, reply_slice),
-                        SDKRuntimeRequest::ReadKey => {
-                            read_key_request(app_id, args_slice, reply_slice)
-                        }
-                        SDKRuntimeRequest::WriteKey => {
-                            write_key_request(app_id, args_slice, reply_slice)
-                        }
-                        SDKRuntimeRequest::DeleteKey => {
-                            delete_key_request(app_id, args_slice, reply_slice)
-                        }
-                    } {
-                        reply_error(status, reply_slice);
-                    }
+
+            let app_id = sdk_runtime_badge as SDKAppId; // XXX safe?
+            response = match SDKRuntimeRequest::try_from(info.get_label()) {
+                Ok(SDKRuntimeRequest::Ping) => ping_request(app_id, request_slice, reply_slice),
+                Ok(SDKRuntimeRequest::Log) => log_request(app_id, request_slice, reply_slice),
+                Ok(SDKRuntimeRequest::ReadKey) => {
+                    read_key_request(app_id, request_slice, reply_slice)
                 }
-                Err(err) => reply_error(deserialize_failure(err), reply_slice),
-            }
+                Ok(SDKRuntimeRequest::WriteKey) => {
+                    write_key_request(app_id, request_slice, reply_slice)
+                }
+                Ok(SDKRuntimeRequest::DeleteKey) => {
+                    delete_key_request(app_id, request_slice, reply_slice)
+                }
+                Err(_) => {
+                    // TODO(b/254286176): possible ddos
+                    error!("Unknown RPC request {}", info.get_label());
+                    Err(SDKError::UnknownRequest)
+                }
+            };
             copy_region.unmap().expect("unmap");
         } else {
+            // TODO(b/254286176): possible ddos
             error!("Unable to map RPC parameters; badge {}", sdk_runtime_badge);
-            // TODO(jtgans): no way to return an error; signal ProcessManager to stop app?
+            response = Err(SDKError::MapPageFailed);
         }
         delete_path(recv_path).expect("delete");
         Camkes::debug_assert_slot_empty("run", recv_path);
 
-        let info = seL4_MessageInfo::new(0, 0, 0, /*length=*/ 0);
-        seL4_ReplyRecv(KATA_SDK_ENDPOINT, info, &mut sdk_runtime_badge as _, KATA_SDK_REPLY);
+        info = seL4_ReplyRecv(
+            /*src=*/ KATA_SDK_ENDPOINT,
+            /*msgInfo=*/
+            seL4_MessageInfo::new(
+                /*label=*/ SDKRuntimeError::from(response) as seL4_Word,
+                /*capsUnwrapped=*/ 0,
+                /*extraCaps=*/ 0,
+                /*length=*/ 0,
+            ),
+            /*sender=*/ &mut sdk_runtime_badge as _,
+            /*reply=*/ KATA_SDK_REPLY,
+        );
     }
 }
 
@@ -257,14 +263,8 @@ fn read_key_request(
     #[allow(clippy::uninit_assumed_init)]
     let mut keyval: KeyValueData = unsafe { ::core::mem::MaybeUninit::uninit().assume_init() };
     let value = unsafe { KATA_SDK.read_key(app_id, request.key, &mut keyval)? };
-    let _ = postcard::to_slice(
-        &sdk_interface::ReadKeyResponse {
-            header: SDKReplyHeader::new(SDKRuntimeError::SDKSuccess),
-            value,
-        },
-        reply_slice,
-    )
-    .map_err(serialize_failure)?;
+    let _ = postcard::to_slice(&sdk_interface::ReadKeyResponse { value }, reply_slice)
+        .map_err(serialize_failure)?;
     Ok(())
 }
 
diff --git a/apps/system/components/SDKRuntime/sdk-interface/Cargo.toml b/apps/system/components/SDKRuntime/sdk-interface/Cargo.toml
index 0177226..8a6459b 100644
--- a/apps/system/components/SDKRuntime/sdk-interface/Cargo.toml
+++ b/apps/system/components/SDKRuntime/sdk-interface/Cargo.toml
@@ -18,6 +18,7 @@ version = "0.1.0"
 edition = "2021"
 
 [dependencies]
+num_enum = { version = "0.5", default-features = false }
 postcard = { version = "0.7", features = ["alloc"], default-features = false }
 sel4-sys = { path = "../../kata-os-common/src/sel4-sys", default-features = false }
 serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
diff --git a/apps/system/components/SDKRuntime/sdk-interface/src/error.rs b/apps/system/components/SDKRuntime/sdk-interface/src/error.rs
index aff8f56..7f58176 100644
--- a/apps/system/components/SDKRuntime/sdk-interface/src/error.rs
+++ b/apps/system/components/SDKRuntime/sdk-interface/src/error.rs
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use serde::{Deserialize, Serialize};
+use num_enum::TryFromPrimitive;
 
 /// Rust Error enum used for representing an SDK error with postcard. This is
 /// what most rust components will actually use as their error handling enum.
@@ -25,15 +25,19 @@ pub enum SDKError {
     ReadKeyFailed,
     WriteKeyFailed,
     DeleteKeyFailed,
+    MapPageFailed,
+    UnknownRequest,
+    UnknownResponse,
 }
 
 impl From<postcard::Error> for SDKError {
     fn from(_err: postcard::Error) -> SDKError { SDKError::SerializeFailed }
 }
 
-/// C-version of SDKError presented over the CAmkES rpc interface.
-#[repr(C)]
-#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
+/// SDKError presented over the seL4 IPC interface. We need repr(seL4_Word)
+/// but cannot use that so use the implied usize type instead.
+#[repr(usize)]
+#[derive(Debug, Eq, PartialEq, TryFromPrimitive)]
 pub enum SDKRuntimeError {
     SDKSuccess = 0,
     SDKDeserializeFailed,
@@ -43,6 +47,9 @@ pub enum SDKRuntimeError {
     SDKReadKeyFailed,
     SDKWriteKeyFailed,
     SDKDeleteKeyFailed,
+    SDKMapPageFailed,
+    SDKUnknownRequest,
+    SDKUnknownResponse,
 }
 
 /// Mapping function from Rust -> C.
@@ -56,6 +63,9 @@ impl From<SDKError> for SDKRuntimeError {
             SDKError::ReadKeyFailed => SDKRuntimeError::SDKReadKeyFailed,
             SDKError::WriteKeyFailed => SDKRuntimeError::SDKWriteKeyFailed,
             SDKError::DeleteKeyFailed => SDKRuntimeError::SDKDeleteKeyFailed,
+            SDKError::MapPageFailed => SDKRuntimeError::SDKMapPageFailed,
+            SDKError::UnknownRequest => SDKRuntimeError::SDKUnknownRequest,
+            SDKError::UnknownResponse => SDKRuntimeError::SDKUnknownResponse,
         }
     }
 }
@@ -79,6 +89,9 @@ impl From<SDKRuntimeError> for Result<(), SDKError> {
             SDKRuntimeError::SDKReadKeyFailed => Err(SDKError::ReadKeyFailed),
             SDKRuntimeError::SDKWriteKeyFailed => Err(SDKError::WriteKeyFailed),
             SDKRuntimeError::SDKDeleteKeyFailed => Err(SDKError::DeleteKeyFailed),
+            SDKRuntimeError::SDKMapPageFailed => Err(SDKError::DeleteKeyFailed),
+            SDKRuntimeError::SDKUnknownRequest => Err(SDKError::UnknownRequest),
+            SDKRuntimeError::SDKUnknownResponse => Err(SDKError::UnknownResponse),
         }
     }
 }
diff --git a/apps/system/components/SDKRuntime/sdk-interface/src/lib.rs b/apps/system/components/SDKRuntime/sdk-interface/src/lib.rs
index efe113d..a083d1a 100644
--- a/apps/system/components/SDKRuntime/sdk-interface/src/lib.rs
+++ b/apps/system/components/SDKRuntime/sdk-interface/src/lib.rs
@@ -21,6 +21,7 @@ pub mod error;
 pub use error::SDKError;
 pub use error::SDKRuntimeError;
 
+use num_enum::{IntoPrimitive, TryFromPrimitive};
 use serde::{Deserialize, Serialize};
 
 use sel4_sys::seL4_CPtr;
@@ -61,33 +62,6 @@ pub type SDKAppId = usize;
 pub const KEY_VALUE_DATA_SIZE: usize = 100;
 pub type KeyValueData = [u8; KEY_VALUE_DATA_SIZE];
 
-/// All RPC request must have an SDKRequestHeader at the front.
-#[derive(Serialize, Deserialize)]
-pub struct SDKRequestHeader {
-    pub request: SDKRuntimeRequest,
-}
-impl SDKRequestHeader {
-    pub fn new(request: SDKRuntimeRequest) -> Self { Self { request } }
-}
-
-/// All RPC responses must have an SDKReplyHeader at the front.
-#[derive(Serialize, Deserialize)]
-pub struct SDKReplyHeader {
-    pub status: SDKRuntimeError,
-}
-impl SDKReplyHeader {
-    pub fn new(status: SDKRuntimeError) -> Self { Self { status } }
-}
-impl From<SDKReplyHeader> for Result<(), SDKRuntimeError> {
-    fn from(header: SDKReplyHeader) -> Result<(), SDKRuntimeError> {
-        if header.status == SDKRuntimeError::SDKSuccess {
-            Ok(())
-        } else {
-            Err(header.status)
-        }
-    }
-}
-
 /// SDKRuntimeRequest::Ping
 #[derive(Serialize, Deserialize)]
 pub struct PingRequest {}
@@ -105,7 +79,6 @@ pub struct ReadKeyRequest<'a> {
 }
 #[derive(Serialize, Deserialize)]
 pub struct ReadKeyResponse<'a> {
-    pub header: SDKReplyHeader,
     pub value: &'a [u8],
 }
 
@@ -122,8 +95,10 @@ pub struct DeleteKeyRequest<'a> {
     pub key: &'a str,
 }
 
-#[repr(C)] // XXX needed?
-#[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Debug)]
+/// SDKRequest token sent over the seL4 IPC interface. We need repr(seL4_Word)
+/// but cannot use that so use the implied usize type instead.
+#[repr(usize)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
 pub enum SDKRuntimeRequest {
     Ping = 0, // Check runtime is alive
     Log,      // Log message: [msg: &str]
@@ -170,10 +145,10 @@ pub trait SDKRuntimeInterface {
 /// Rust client-side request processing. Note there is no CAmkES stub to
 /// call; everything is done here. A single page frame is attached to the
 /// IPC buffer with request parameters in the first half and return values
-/// in the second half. Requests must have an SDKRequestHeader serialized
-/// separately from any arguments. Responses must have an SDKReplyHeader
-/// included in the reply data. For the moment this uses postcard to do
-/// serde work; this may change in the future (e.g. to flatbuffers).
+/// in the second half. Requests must have an SDKRequestHeader written to
+/// the label field of the MessageInfo. Responses must have an SDKRuntimeError
+/// written to the label field of the reply. For the moment this uses
+/// postcard for serde work; this may change in the future (e.g. to flatbuffers).
 ///
 /// The caller is responsible for synchronizing access to KATA_SDK_* state
 /// and the IPC buffer.
@@ -185,10 +160,6 @@ pub trait SDKRuntimeInterface {
 //   to lookup the mapped page early. Downside to a fixed mapping is it
 //   limits how to handle requests w/ different-sized params (e.g. sensor
 //   frame vs key-value params).
-// TODO(sleffler): could send request header and reponse statatus inline.
-//   This would align request arguments to the page boundary which might
-//   be useful and having the reply inline would mean SDKRuntime could
-//   send a meaningful error back when unable to map the page frame.
 fn sdk_request<'a, S: Serialize, D: Deserialize<'a>>(
     request: SDKRuntimeRequest,
     request_args: &S,
@@ -199,24 +170,32 @@ fn sdk_request<'a, S: Serialize, D: Deserialize<'a>>(
     let (request_slice, reply_slice) = params_slice.split_at_mut(SDKRUNTIME_REQUEST_DATA_SIZE);
     reply_slice.fill(0); // XXX paranoid, could zero-pad request too
 
-    // Encode heeader with request.
-    // TODO(sleffler): eliminate struct? (could add a sequence #)
-    let header_size = (postcard::to_slice(&SDKRequestHeader::new(request), request_slice)
-        .map_err(|_| SDKRuntimeError::SDKSerializeFailed)?)
-    .len();
-
-    // Encode arguments immediately after.
-    let (_, args_slice) = request_slice.split_at_mut(header_size);
-    let _ = postcard::to_slice(request_args, args_slice)
+    // Encode request arguments.
+    let _ = postcard::to_slice(request_args, request_slice)
         .map_err(|_| SDKRuntimeError::SDKSerializeFailed)?;
 
     // Attach params & call the SDKRuntime; then wait (block) for a reply.
     unsafe {
         seL4_SetCap(0, KATA_SDK_FRAME);
-        seL4_Call(KATA_SDK_ENDPOINT, seL4_MessageInfo::new(0, 0, 1, 0));
+        let info = seL4_Call(
+            KATA_SDK_ENDPOINT,
+            seL4_MessageInfo::new(
+                /*label=*/ request.into(),
+                /*capsUnrapped=*/ 0,
+                /*extraCaps=*/ 1,
+                /*length=*/ 0,
+            ),
+        );
         seL4_SetCap(0, 0);
+
+        let status = SDKRuntimeError::try_from(info.get_label())
+            .map_err(|_| SDKRuntimeError::SDKUnknownResponse)?;
+        if status != SDKRuntimeError::SDKSuccess {
+            return Err(status);
+        }
     }
 
+    // Decode response data.
     postcard::from_bytes::<D>(reply_slice).map_err(|_| SDKRuntimeError::SDKDeserializeFailed)
 }
 
@@ -224,22 +203,19 @@ fn sdk_request<'a, S: Serialize, D: Deserialize<'a>>(
 #[inline]
 #[allow(dead_code)]
 pub fn sdk_ping() -> Result<(), SDKRuntimeError> {
-    let header =
-        sdk_request::<PingRequest, SDKReplyHeader>(SDKRuntimeRequest::Ping, &PingRequest {})?;
-    header.into()
+    sdk_request::<PingRequest, ()>(SDKRuntimeRequest::Ping, &PingRequest {})
 }
 
 /// Rust client-side wrapper for the log method.
 #[inline]
 #[allow(dead_code)]
 pub fn sdk_log(msg: &str) -> Result<(), SDKRuntimeError> {
-    let header = sdk_request::<LogRequest, SDKReplyHeader>(
+    sdk_request::<LogRequest, ()>(
         SDKRuntimeRequest::Log,
         &LogRequest {
             msg: msg.as_bytes(),
         },
-    )?;
-    header.into()
+    )
 }
 
 /// Rust client-side wrapper for the read key method.
@@ -251,34 +227,20 @@ pub fn sdk_read_key<'a>(key: &str, keyval: &'a mut [u8]) -> Result<&'a [u8], SDK
         SDKRuntimeRequest::ReadKey,
         &ReadKeyRequest { key },
     )?;
-    match response.header.status {
-        SDKRuntimeError::SDKSuccess => {
-            let (left, _) = keyval.split_at_mut(response.value.len());
-            left.copy_from_slice(response.value);
-            Ok(left)
-        }
-        e => Err(e),
-    }
+    keyval.copy_from_slice(response.value);
+    Ok(keyval)
 }
 
 /// Rust client-side wrapper for the write key method.
 #[inline]
 #[allow(dead_code)]
 pub fn sdk_write_key(key: &str, value: &[u8]) -> Result<(), SDKRuntimeError> {
-    let header = sdk_request::<WriteKeyRequest, SDKReplyHeader>(
-        SDKRuntimeRequest::WriteKey,
-        &WriteKeyRequest { key, value },
-    )?;
-    header.into()
+    sdk_request::<WriteKeyRequest, ()>(SDKRuntimeRequest::WriteKey, &WriteKeyRequest { key, value })
 }
 
 /// Rust client-side wrapper for the delete key method.
 #[inline]
 #[allow(dead_code)]
 pub fn sdk_delete_key(key: &str) -> Result<(), SDKRuntimeError> {
-    let header = sdk_request::<DeleteKeyRequest, SDKReplyHeader>(
-        SDKRuntimeRequest::DeleteKey,
-        &DeleteKeyRequest { key },
-    )?;
-    header.into()
+    sdk_request::<DeleteKeyRequest, ()>(SDKRuntimeRequest::DeleteKey, &DeleteKeyRequest { key })
 }