Repository: project-oak/oak
Pull request #1408 (2020-09-01 11:21:13)

Move oak_derive out of SDK
This change moves `oak_derive` out of SDK directory to the top-level.

Fixes https://github.com/project-oak/oak/issues/1386

# Checklist
- [x] Pull request affects core Oak functionality (e.g. runtime, SDK, ABI)
  - [x] I have checked that these tests are run by [Cloudbuild](/cloudbuild.yaml)



Patch:
diff --git a/FORCE_CI b/FORCE_CI
index e440e5c842..b8626c4cff 100644
--- a/FORCE_CI
+++ b/FORCE_CI
@@ -1,1 +1,1 @@
-3
\ No newline at end of file
+4
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 9f2a598b86..76a85aa317 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -40,7 +40,7 @@ members = [
 # Oak.
 oak = { path = "../sdk/rust/oak" }
 oak_abi = { path = "../oak_abi" }
-oak_derive = { path = "../sdk/rust/oak_derive" }
+oak_derive = { path = "../oak_derive" }
 oak_io = { path = "../oak_io" }
 oak_runtime = { path = "../oak_runtime" }
 oak_services = { path = "../oak_services" }
diff --git a/examples/aggregator/client/aggregator.cc b/examples/aggregator/client/aggregator.cc
index 2ffad119c3..42e84b7e2e 100644
--- a/examples/aggregator/client/aggregator.cc
+++ b/examples/aggregator/client/aggregator.cc
@@ -79,7 +79,7 @@ int main(int argc, char** argv) {
   // The particular value corresponds to the hash on the `aggregator.wasm` line in
   // https://github.com/project-oak/oak/blob/hashes/reproducibility_index.
   oak::label::Label label = oak::WebAssemblyModuleLabel(
-      absl::HexStringToBytes("b08b93cf5a92e9b31589f9bacbe378d33c104f53557ccfa930383b53fa4e67fa"));
+      absl::HexStringToBytes("c995a15ab48aa7e091b65e8bb8d7b67c71f4a10c236b8eaa0599fe7439053530"));
   // Connect to the Oak Application.
   auto stub = Aggregator::NewStub(oak::ApplicationClient::CreateChannel(
       address, oak::ApplicationClient::GetTlsChannelCredentials(ca_cert), label));
diff --git a/examples/aggregator/oak_app_manifest.toml b/examples/aggregator/oak_app_manifest.toml
index cd885d5833..5d6aae61fd 100644
--- a/examples/aggregator/oak_app_manifest.toml
+++ b/examples/aggregator/oak_app_manifest.toml
@@ -1,4 +1,4 @@
 name = "aggregator"
 
 [modules]
-app = { external = { url = "https://storage.googleapis.com/oak-modules/aggregator/b08b93cf5a92e9b31589f9bacbe378d33c104f53557ccfa930383b53fa4e67fa", sha256 = "b08b93cf5a92e9b31589f9bacbe378d33c104f53557ccfa930383b53fa4e67fa" } }
+app = { external = { url = "https://storage.googleapis.com/oak-modules/aggregator/c995a15ab48aa7e091b65e8bb8d7b67c71f4a10c236b8eaa0599fe7439053530", sha256 = "c995a15ab48aa7e091b65e8bb8d7b67c71f4a10c236b8eaa0599fe7439053530" } }
diff --git a/examples/trusted_information_retrieval/oak_app_manifest.toml b/examples/trusted_information_retrieval/oak_app_manifest.toml
index a5b98560fc..21c8d4b345 100644
--- a/examples/trusted_information_retrieval/oak_app_manifest.toml
+++ b/examples/trusted_information_retrieval/oak_app_manifest.toml
@@ -2,4 +2,4 @@ name = "trusted_information_retrieval"
 
 [modules]
 app = { path = "examples/trusted_information_retrieval/bin/trusted_information_retrieval.wasm" }
-database_proxy = { external = { url = "https://storage.googleapis.com/oak-modules/database_proxy/3606a86d9fa01d223dc3cb092c1aceb9e3afe0ed9799ac5d5b214b1610209f8f", sha256 = "3606a86d9fa01d223dc3cb092c1aceb9e3afe0ed9799ac5d5b214b1610209f8f" } }
+database_proxy = { external = { url = "https://storage.googleapis.com/oak-modules/database_proxy/8e2a242fd67575626a3a07288a4e162984f2023309126955b2811a7d234ce9cb", sha256 = "8e2a242fd67575626a3a07288a4e162984f2023309126955b2811a7d234ce9cb" } }
diff --git a/oak/module/oak_main.h b/oak/module/oak_main.h
index a3d5c58fdb..e57e63916a 100644
--- a/oak/module/oak_main.h
+++ b/oak/module/oak_main.h
@@ -69,7 +69,16 @@ WASM_EXPORT void oak_main(oak_abi::Handle _handle) {
   if (result != oak_abi::OakStatus::OK) {
     return;
   }
-  result = channel_write(write_handle, nullptr, 0, (uint8_t*)&grpc_out_handle, 1);
+
+  // Manually create a GrpcInvocationSender protobuf and send it with the handle. The id is a
+  // placeholder that will be replaced with the actual handle.
+  //    0a                     b00001.010 = tag 1 (GrpcInvocationSender.sender), length-delimited
+  //    09                     length=9
+  //      09                   b00001.001 = tag 1 (Sender.id), fixed-length 64 bit
+  //        0100000000000000   1 (little-endian byte order)
+  uint8_t invocation_sender[] = "\x0a\x09\x09\x01\x00\x00\x00\x00\x00\x00\x00";
+  result = channel_write(write_handle, invocation_sender, sizeof(invocation_sender) - 1,
+                         (uint8_t*)&grpc_out_handle, 1);
   if (result != oak_abi::OakStatus::OK) {
     return;
   }
@@ -95,12 +104,14 @@ WASM_EXPORT void oak_main(oak_abi::Handle _handle) {
       return;
     }
 
-    // Reading from main channel should give no data and a (read, write) pair of handles.
+    // Reading from main channel should give a (read, write) pair of handles.
+    // Ignoring the data, as it is the encoded GrpcInvocation proto.
+    uint8_t proto[100];
     uint32_t actual_size;
     uint32_t handle_count;
     oak_abi::Handle handles[2];
-    channel_read(grpc_in_handle, nullptr, 0, &actual_size, handles, 2, &handle_count);
-    if ((actual_size != 0) || (handle_count != 2)) {
+    channel_read(grpc_in_handle, proto, sizeof(proto), &actual_size, handles, 2, &handle_count);
+    if (handle_count != 2) {
       return;
     }
     oak_abi::Handle req_handle = handles[0];
diff --git a/oak_derive/Cargo.lock b/oak_derive/Cargo.lock
new file mode 100644
index 0000000000..d9b446c9a6
--- /dev/null
+++ b/oak_derive/Cargo.lock
@@ -0,0 +1,428 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "anyhow"
+version = "1.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b"
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bytes"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "either"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f"
+
+[[package]]
+name = "fixedbitset"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d"
+
+[[package]]
+name = "getrandom"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "heck"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itertools"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3"
+
+[[package]]
+name = "log"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "multimap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1255076139a83bb467426e7f8d0134968a8118844faa755985e077cf31850333"
+
+[[package]]
+name = "oak_abi"
+version = "0.1.0"
+dependencies = [
+ "oak_utils",
+ "prost",
+ "prost-build",
+ "prost-types",
+ "serde",
+]
+
+[[package]]
+name = "oak_derive"
+version = "0.1.0"
+dependencies = [
+ "oak_io",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "oak_io"
+version = "0.1.0"
+dependencies = [
+ "oak_abi",
+ "oak_derive",
+ "oak_utils",
+ "prost",
+ "prost-build",
+ "prost-types",
+]
+
+[[package]]
+name = "oak_utils"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "maplit",
+ "proc-macro2",
+ "prost",
+ "prost-build",
+ "quote",
+ "tempfile",
+ "tonic-build",
+ "walkdir",
+]
+
+[[package]]
+name = "petgraph"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "prost"
+version = "0.6.1"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.6.1"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "log",
+ "multimap",
+ "petgraph",
+ "prost",
+ "prost-types",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.6.1"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.6.1"
+dependencies = [
+ "bytes",
+ "prost",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom",
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19970cf58f3acc820962be74c4021b8bbc8e8a1c4e3a02095d0aa60cde5f3633"
+dependencies = [
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "which"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/oak_derive/Cargo.toml b/oak_derive/Cargo.toml
new file mode 100644
index 0000000000..85bb2f1232
--- /dev/null
+++ b/oak_derive/Cargo.toml
@@ -0,0 +1,28 @@
+[workspace]
+
+[package]
+name = "oak_derive"
+version = "0.1.0"
+authors = ["Daan de Graaf <daagra@google.com>"]
+edition = "2018"
+license = "Apache-2.0"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = "1.0"
+quote = "1.0"
+proc-macro2 = "1.0"
+
+[dev-dependencies]
+oak_io = "*"
+
+[patch.crates-io]
+# Oak.
+oak_io = { path = "../oak_io" }
+# Third party.
+prost = { path = "../third_party/prost" }
+prost-build = { path = "../third_party/prost/prost-build" }
+prost-derive = { path = "../third_party/prost/prost-derive" }
+prost-types = { path = "../third_party/prost/prost-types" }
diff --git a/oak_derive/deny.toml b/oak_derive/deny.toml
new file mode 100644
index 0000000000..897eed5b8f
--- /dev/null
+++ b/oak_derive/deny.toml
@@ -0,0 +1,31 @@
+# Configuration used for dependency checking with cargo-deny.
+#
+# For further details on all configuration options see:
+# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
+targets = [
+  { triple = "wasm32-unknown-unknown" },
+  { triple = "x86_64-unknown-linux-musl" },
+]
+
+# Deny all advisories unless explicitly ignored.
+[advisories]
+vulnerability = "deny"
+unmaintained = "deny"
+yanked = "deny"
+notice = "deny"
+
+# Deny multiple versions unless explicitly skipped.
+[bans]
+multiple-versions = "deny"
+
+# List of allowed licenses.
+# For more detailed information see http://go/thirdpartylicenses.
+[licenses]
+allow = ["Apache-2.0", "MIT"]
+copyleft = "deny"
+
+[[licenses.clarify]]
+name = "ring"
+version = "*"
+expression = "MIT AND ISC AND OpenSSL"
+license-files = [{ path = "LICENSE", hash = 3171872035 }]
diff --git a/sdk/rust/oak_derive/src/lib.rs b/oak_derive/src/lib.rs
similarity index 100%
rename from sdk/rust/oak_derive/src/lib.rs
rename to oak_derive/src/lib.rs
diff --git a/sdk/rust/oak_derive/tests/visit.rs b/oak_derive/tests/visit.rs
similarity index 100%
rename from sdk/rust/oak_derive/tests/visit.rs
rename to oak_derive/tests/visit.rs
diff --git a/oak_io/Cargo.toml b/oak_io/Cargo.toml
index 96ce082482..0a79d9c346 100644
--- a/oak_io/Cargo.toml
+++ b/oak_io/Cargo.toml
@@ -9,7 +9,7 @@ license = "Apache-2.0"
 
 [dependencies]
 oak_abi = { path = "../oak_abi" }
-oak_derive = { path = "../sdk/rust/oak_derive" }
+oak_derive = { path = "../oak_derive" }
 prost = "*"
 prost-types = "*"
 
diff --git a/oak_io/src/handle.rs b/oak_io/src/handle.rs
index 582858a090..5cc0df57a4 100644
--- a/oak_io/src/handle.rs
+++ b/oak_io/src/handle.rs
@@ -187,12 +187,15 @@ macro_rules! handle_visit_blanket_impl {
 // See: https://github.com/danburkert/prost#scalar-values
 handle_visit_blanket_impl!((), f64, f32, i32, i64, u32, u64, bool, String, Vec<u8>);
 
-// Provide an implementation for the Any type in Prost.
-handle_visit_blanket_impl!(prost_types::Any);
+// Provide an implementation for the Any and Timestamp types in Prost.
+handle_visit_blanket_impl!(prost_types::Any, prost_types::Timestamp);
 
 // Provide an implementation for oak_abi type that an implementation cannot be derived for.
 // It does not contain handles, so a blanket impl is sufficient.
-handle_visit_blanket_impl!(oak_abi::proto::oak::application::ConfigMap);
+handle_visit_blanket_impl!(
+    oak_abi::proto::oak::application::ConfigMap,
+    oak_abi::proto::oak::label::Label
+);
 
 // Make the HandleVisit derivation macro publicly available without having to explicictly reference
 // the oak_derive crate.
diff --git a/oak_runtime/build.rs b/oak_runtime/build.rs
index 82a0297f23..f81f4bfe78 100644
--- a/oak_runtime/build.rs
+++ b/oak_runtime/build.rs
@@ -28,12 +28,13 @@ fn main() {
     .expect("Proto compilation failed.");
 
     compile_protos_with_options(
-        &["../proto/introspection_events.proto"],
-        &["../proto"],
+        &[
+            "../oak_services/proto/grpc_invocation.proto",
+            "../oak_services/proto/http_invocation.proto",
+            "../proto/introspection_events.proto",
+        ],
+        &[".."],
         ProtoOptions {
-            // Exclude generation of HandleVisit auto-derive, as it would
-            // require a reference to the Oak SDK to compile.
-            derive_handle_visit: false,
             ..Default::default()
         },
     );
diff --git a/oak_runtime/introspection_browser_client/.gitignore b/oak_runtime/introspection_browser_client/.gitignore
index c7306509b6..25fde76bf2 100644
--- a/oak_runtime/introspection_browser_client/.gitignore
+++ b/oak_runtime/introspection_browser_client/.gitignore
@@ -1,4 +1,4 @@
 # Build output
 /dist
 # Programmatically generated code from protobufs
-/proto
+/protoc_out
diff --git a/oak_runtime/introspection_browser_client/build b/oak_runtime/introspection_browser_client/build
index 0ceed9fe3e..8da86cd5c9 100755
--- a/oak_runtime/introspection_browser_client/build
+++ b/oak_runtime/introspection_browser_client/build
@@ -6,9 +6,9 @@
 npm ci
 
 # Generate JavaScript code from the introspection_events proto
-mkdir -p proto
+mkdir -p ./protoc_out
 readonly PROTOC_GEN_TS_PATH="./node_modules/.bin/protoc-gen-ts"
-readonly PROTO_OUT_DIR="./proto"
+readonly PROTO_OUT_DIR="./protoc_out"
 protoc \
     --proto_path=../../proto \
     --plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}" \
diff --git a/oak_runtime/introspection_browser_client/index.tsx b/oak_runtime/introspection_browser_client/index.tsx
index 0860bce75f..79cc1237b1 100644
--- a/oak_runtime/introspection_browser_client/index.tsx
+++ b/oak_runtime/introspection_browser_client/index.tsx
@@ -16,7 +16,7 @@
 
 import React from 'react';
 import ReactDOM from 'react-dom';
-import introspectionEventsProto from './proto/introspection_events_pb';
+import introspectionEventsProto from './protoc_out/introspection_events_pb';
 
 // Requests the list of introspection events provided by the Oak runtime's
 // auxiliary introspection server.
@@ -45,6 +45,142 @@ function loadSerializedEvents(): Promise<Uint8Array> {
   });
 }
 
+interface OakApplicationState {
+  nodeInfos: NodeInfos;
+  channels: Channels;
+}
+
+type NodeId = number;
+type AbiHandle = number;
+type NodeInfos = Map<NodeId, NodeInfo>;
+interface NodeInfo {
+  abiHandles: Map<AbiHandle, ChannelHalf>;
+}
+
+type ChannelID = number;
+enum ChannelHalfDirection {
+  Read,
+  Write
+}
+interface ChannelHalf {
+  channelId: ChannelID;
+  direction: ChannelHalfDirection;
+}
+interface Message {
+  data: Uint8Array;
+  channels: ChannelHalf[];
+}
+interface Channel {
+  id: ChannelID;
+  messages: Message[];
+}
+type Channels = Map<ChannelID, Channel>;
+
+function eventReducer(
+  applicationState: OakApplicationState,
+  event: introspectionEventsProto.Event
+) {
+  const eventType = event.getEventDetailsCase();
+  const { EventDetailsCase } = introspectionEventsProto.Event;
+
+  switch (eventType) {
+    case EventDetailsCase.NODE_CREATED:
+      applicationState.nodeInfos.set(event!.getNodeCreated()!.getNodeId(), {
+        abiHandles: new Map()
+      });
+
+      break;
+    case EventDetailsCase.NODE_DESTROYED:
+      {
+        const nodeId = event!.getNodeDestroyed()!.getNodeId();
+        if (applicationState.nodeInfos.delete(nodeId) === false) {
+          throw new Error(
+            `Couldn't delete Node with id "${nodeId}", as it does not exist.`
+          );
+        }
+      }
+
+      break;
+    case EventDetailsCase.CHANNEL_CREATED:
+      {
+        const channelId = event!.getChannelCreated()!.getChannelId();
+        applicationState.channels.set(channelId, {
+          id: channelId,
+          messages: []
+        });
+      }
+
+      break;
+    case EventDetailsCase.CHANNEL_DESTROYED:
+      {
+        const channelId = event!.getChannelDestroyed()!.getChannelId();
+        if (applicationState.channels.delete(channelId) === false) {
+          throw new Error(
+            `Couldn't delete Channel with id "${channelId}", as it does not exist.`
+          );
+        }
+      }
+
+      break;
+    case EventDetailsCase.HANDLE_CREATED:
+      {
+        const details = event.getHandleCreated();
+        const nodeId = details!.getNodeId();
+        const node = applicationState.nodeInfos.get(nodeId);
+
+        if (node === undefined) {
+          throw new Error(
+            `Couldn't get Node with id "${nodeId}", as it does not exist.`
+          );
+        }
+
+        node.abiHandles.set(details!.getHandle(), {
+          channelId: details!.getChannelId(),
+          // TODO(#913): Add a direction property in the introspection
+          // event and use the real value here.
+          direction: 0
+        });
+      }
+
+      break;
+    case EventDetailsCase.HANDLE_DESTROYED:
+      {
+        const details = event.getHandleDestroyed();
+        const nodeId = details!.getNodeId();
+        const node = applicationState.nodeInfos.get(nodeId);
+
+        if (node === undefined) {
+          throw new Error(
+            `Couldn't get Node with id "${nodeId}", as it does not exist.`
+          );
+        }
+
+        const handle = details!.getHandle();
+
+        if (node.abiHandles.delete(handle) === false) {
+          throw new Error(
+            `Couldn't delete ABI handle "${handle}" on Node with id "${nodeId}", as it does not exist.`
+          );
+        }
+      }
+
+      break;
+    case EventDetailsCase.MESSAGE_ENQUEUED:
+      // TODO(#913): Add support for displaying messages
+
+      break;
+    case EventDetailsCase.MESSAGE_DEQUEUED:
+      // TODO(#913): Add support for displaying messages
+
+      break;
+    default:
+      // This should never happen
+      throw new Error(`Encountered unhandled event of type ${eventType}`);
+  }
+
+  return applicationState;
+}
+
 const EventList = () => {
   const [events, setEvents] = React.useState<introspectionEventsProto.Event[]>(
     []
@@ -60,16 +196,55 @@ const EventList = () => {
 
     loadEvents();
   }, []);
+  const applicationState = React.useMemo(
+    (): OakApplicationState =>
+      events.reduce(eventReducer, {
+        nodeInfos: new Map(),
+        channels: new Map()
+      }),
+    [events]
+  );
 
   return (
-    <ol>
-      {events.map((event, index) => (
-        // Usually it's not advisable to use the index as a key. However since
-        // the list of events is append-only it's fine in this case.
-        // Ref: https://reactjs.org/docs/lists-and-keys.html#keys
-        <li key={index}>{JSON.stringify(event.toObject())}</li>
-      ))}
-    </ol>
+    <>
+      <section>
+        <strong>Application State</strong>
+        <p>{applicationState.nodeInfos.size} Nodes:</p>
+        <ul>
+          {[...applicationState.nodeInfos.entries()].map(([id, nodeInfo]) => (
+            <li key={id}>
+              <dl>
+                <dt>ID:</dt>
+                <dd>{id}</dd>
+                <dt>Handles:</dt>
+                <dd>
+                  <ul>
+                    {[...nodeInfo.abiHandles.entries()].map(
+                      ([handle, channelHalf]) => (
+                        <li key={handle}>
+                          {handle} pointing to channel {channelHalf.channelId}
+                        </li>
+                      )
+                    )}
+                  </ul>
+                </dd>
+              </dl>
+            </li>
+          ))}
+        </ul>
+      </section>
+      <section>
+        <strong>Events List</strong>
+        <ol>
+          {events.map((event, index) => (
+            // Usually it's not advisable to use the index as a key. However since
+            // the list of events is append-only it's fine in this case.
+            // Ref: https://reactjs.org/docs/lists-and-keys.html#keys
+            <li key={index}>{JSON.stringify(event.toObject())}</li>
+          ))}
+        </ol>
+      </section>
+    </>
   );
 };
 
diff --git a/oak_runtime/introspection_browser_client/tsconfig.json b/oak_runtime/introspection_browser_client/tsconfig.json
index 4c3133b393..eec06fb383 100644
--- a/oak_runtime/introspection_browser_client/tsconfig.json
+++ b/oak_runtime/introspection_browser_client/tsconfig.json
@@ -5,6 +5,7 @@
     "target": "ES2015",
     "jsx": "react",
     "esModuleInterop": true,
-    "noImplicitAny": true
+    "noImplicitAny": true,
+    "strictNullChecks": true
   }
 }
diff --git a/oak_runtime/src/node/grpc/invocation.rs b/oak_runtime/src/node/grpc/invocation.rs
index 2fe4f48d89..9e1d9e5044 100644
--- a/oak_runtime/src/node/grpc/invocation.rs
+++ b/oak_runtime/src/node/grpc/invocation.rs
@@ -16,61 +16,44 @@
 
 //! Functionality for handling gRPC method invocations.
 
+pub use crate::proto::oak::invocation::GrpcInvocation as Invocation;
 use crate::{
-    io::{Receiver, ReceiverExt, Sender, SenderExt},
-    NodeMessage, RuntimeProxy,
+    io::{ReceiverExt, SenderExt},
+    RuntimeProxy,
 };
 use log::error;
 use oak_abi::OakStatus;
-use oak_io::{
-    handle::{ReadHandle, WriteHandle},
-    Decodable, OakError,
-};
+use oak_io::OakError;
 use oak_services::proto::oak::encap::{GrpcRequest, GrpcResponse};
 
-/// A gRPC invocation, consisting of exactly two channels: one to read incoming requests from the
-/// client (wrapped in a [`Receiver`]), and one to write outgoing responses to the client (wrapped
-/// in a [`Sender`]).
-pub struct Invocation {
-    receiver: Receiver<GrpcRequest>,
-    sender: Sender<GrpcResponse>,
-}
-
-impl Decodable for Invocation {
-    fn decode(message: &NodeMessage) -> Result<Self, OakError> {
-        if !message.bytes.is_empty() {
-            error!("Non-empty data field");
-            return Err(OakStatus::ErrInternal.into());
-        }
-        if message.handles.len() != 2 {
-            error!(
-                "Incorrect number of handles received: {} (expected: 2)",
-                message.handles.len()
-            );
-            return Err(OakStatus::ErrInternal.into());
-        }
-        Ok(Self {
-            receiver: Receiver::new(ReadHandle {
-                handle: message.handles[0],
-            }),
-            sender: Sender::new(WriteHandle {
-                handle: message.handles[1],
-            }),
-        })
-    }
-}
-
 impl Invocation {
+    /// Closes the channels used by the sender and receiver fields.
+    ///
+    /// Any errors while trying to close the sender or receive are logged, but otherwise ignored.
     pub fn close(self, runtime: &RuntimeProxy) {
-        if let Err(err) = self.receiver.close(runtime) {
-            error!("Failed to close receiver channel in invocation: {:?}", err);
-        }
-        if let Err(err) = self.sender.close(runtime) {
-            error!("Failed to close sender channel in invocation: {:?}", err);
-        }
+        match self.receiver {
+            Some(receiver) => {
+                if let Err(err) = receiver.close(runtime) {
+                    error!("Failed to close receiver channel in invocation: {:?}", err);
+                }
+            }
+            None => error!("No receiver on invocation."),
+        };
+        match self.sender {
+            Some(sender) => {
+                if let Err(err) = sender.close(runtime) {
+                    error!("Failed to close sender channel in invocation: {:?}", err);
+                }
+            }
+            None => error!("No sender on invocation."),
+        };
     }
+
     pub fn receive_request(&self, runtime: &RuntimeProxy) -> Result<GrpcRequest, OakError> {
-        self.receiver.receive(runtime)
+        match &self.receiver {
+            Some(receiver) => receiver.receive(runtime),
+            None => Err(OakError::OakStatus(OakStatus::ErrBadHandle)),
+        }
     }
 
     pub fn send_response(
@@ -78,8 +61,12 @@ impl Invocation {
         response: GrpcResponse,
         runtime: &RuntimeProxy,
     ) -> Result<(), OakError> {
-        self.sender.send(response, runtime)
+        match &self.sender {
+            Some(sender) => sender.send(response, runtime),
+            None => Err(OakError::OakStatus(OakStatus::ErrBadHandle)),
+        }
     }
+
     /// Send an error response for the invocation.
     pub fn send_error(
         &self,
@@ -88,7 +75,7 @@ impl Invocation {
         runtime: &RuntimeProxy,
     ) {
         error!("Fail invocation with {:?} '{}'", code, msg);
-        let _ = self.sender.send(
+        let _ = self.send_response(
             GrpcResponse {
                 rsp_msg: vec![],
                 status: Some(oak_services::proto::google::rpc::Status {
diff --git a/oak_runtime/src/node/grpc/server/mod.rs b/oak_runtime/src/node/grpc/server/mod.rs
index 60ba0a9236..e11123d0e3 100644
--- a/oak_runtime/src/node/grpc/server/mod.rs
+++ b/oak_runtime/src/node/grpc/server/mod.rs
@@ -18,17 +18,21 @@
 
 use crate::{
     auth::oidc_utils::ClientInfo,
+    io::{ReceiverExt, SenderExt},
     metrics::Metrics,
     node::{
         grpc::{codec::VecCodec, to_tonic_status},
         ConfigurationError, Node,
     },
-    ChannelHalfDirection, RuntimeProxy,
+    proto::oak::invocation::{GrpcInvocation, GrpcInvocationSender},
+    RuntimeProxy,
 };
 use hyper::service::Service;
 use log::{debug, error, info, trace, warn};
-use oak_abi::{
-    label::Label, proto::oak::application::GrpcServerConfiguration, ChannelReadStatus, OakStatus,
+use oak_abi::{label::Label, proto::oak::application::GrpcServerConfiguration, OakStatus};
+use oak_io::{
+    handle::{ReadHandle, WriteHandle},
+    OakError, Receiver, Sender,
 };
 use oak_services::proto::{
     google::rpc,
@@ -89,66 +93,30 @@ impl GrpcServerNode {
         })
     }
 
-    /// Reads the [`oak_abi::Handle`] for the write half of an invocation from a startup channel.
-    /// Returns an error if the startup channel couldn't be read, or if the initial message
-    /// is invalid (doesn't contain exactly one write handle).
+    /// Reads the [`WriteHandle`] (to be used for sending new invocations) from a startup channel.
+    /// Returns an error if the startup channel couldn't be read, or if the initial message is
+    /// invalid (it must be an encoded [`GrpcInvocationSender`]).
     fn get_invocation_channel(
         runtime: &RuntimeProxy,
         startup_handle: oak_abi::Handle,
-    ) -> Result<oak_abi::Handle, OakStatus> {
-        let read_status = runtime
-            .wait_on_channels(&[startup_handle])
-            .map_err(|error| {
-                error!("Couldn't wait on the initial reader handle: {:?}", error);
-                OakStatus::ErrInternal
-            })?;
-
-        // TODO(#389): Automatically generate this code.
-        let invocation_channel = if read_status[0] == ChannelReadStatus::ReadReady {
-            runtime
-                .channel_read(startup_handle)
-                .map_err(|error| {
-                    error!("Couldn't read from the initial reader handle {:?}", error);
-                    OakStatus::ErrInternal
-                })
-                .and_then(|message| {
-                    message
-                        .ok_or_else(|| {
-                            error!("Empty message");
-                            OakStatus::ErrInternal
-                        })
-                        .and_then(|m| {
-                            if m.handles.len() == 1 {
-                                let handle = m.handles[0];
-                                match runtime.channel_direction(handle)? {
-                                    ChannelHalfDirection::Write => Ok(handle),
-                                    ChannelHalfDirection::Read => {
-                                        error!(
-                                            "gRPC server pseudo-node should receive a writer handle, found reader handle {}",
-                                            handle
-                                        );
-                                        Err(OakStatus::ErrBadHandle)
-                                    },
-                                }
-                            } else {
-                                error!(
-                                    "gRPC server pseudo-node should receive a single writer handle, found {}",
-                                    m.handles.len()
-                                );
-                                Err(OakStatus::ErrInternal)
-                            }
-                        })
-                })
-        } else {
-            error!("Couldn't read channel: {:?}", read_status[0]);
-            Err(OakStatus::ErrInternal)
-        }?;
-
-        info!(
-            "Invocation channel write handle received: {}",
-            invocation_channel
-        );
-        Ok(invocation_channel)
+    ) -> Result<WriteHandle, OakError> {
+        let startup_receiver = Receiver::<GrpcInvocationSender>::new(ReadHandle {
+            handle: startup_handle,
+        });
+        let invocation_channel = startup_receiver.receive(&runtime)?;
+        match &invocation_channel.sender {
+            Some(invocation_sender) => {
+                info!(
+                    "Invocation channel write handle received: {}",
+                    invocation_sender.handle.handle
+                );
+                Ok(invocation_sender.handle)
+            }
+            None => {
+                error!("Couldn't receive the invocation sender.");
+                Err(OakError::OakStatus(OakStatus::ErrBadHandle))
+            }
+        }
     }
 }
 
@@ -252,7 +220,7 @@ struct HttpRequestHandler {
     /// Reference to the Runtime in the context of this gRPC server pseudo-Node.
     runtime: RuntimeProxy,
     /// Channel handle used for writing gRPC invocations.
-    invocation_channel: oak_abi::Handle,
+    invocation_channel: WriteHandle,
 }
 
 /// Set a mandatory prefix for all gRPC requests processed by a gRPC pseudo-Node.
@@ -274,7 +242,7 @@ impl Service<http::Request<hyper::Body>> for HttpRequestHandler {
     fn call(&mut self, request: http::Request<hyper::Body>) -> Self::Future {
         let grpc_handler = GrpcInvocationHandler::new(
             self.runtime.clone(),
-            self.invocation_channel,
+            Sender::<GrpcInvocation>::new(self.invocation_channel),
             request.uri().path().to_string(),
         );
 
@@ -358,7 +326,7 @@ struct GrpcInvocationHandler {
     /// Reference to the Runtime in the context of this gRPC server pseudo-Node.
     runtime: RuntimeProxy,
     /// Channel handle used for writing gRPC invocations.
-    invocation_channel: oak_abi::Handle,
+    invocation_channel: Sender<GrpcInvocation>,
     /// Name of the gRPC method being invoked.
     method_name: String,
 }
@@ -429,7 +397,7 @@ impl ServerStreamingService<Vec<u8>> for GrpcInvocationHandler {
 impl GrpcInvocationHandler {
     fn new(
         runtime: RuntimeProxy,
-        invocation_channel: oak_abi::Handle,
+        invocation_channel: Sender<GrpcInvocation>,
         method_name: String,
     ) -> Self {
         Self {
@@ -461,37 +429,30 @@ impl GrpcInvocationHandler {
                 warn!("could not create gRPC response channel: {:?}", err);
             })?;
 
-        // Create an invocation message and attach the method-invocation specific channels to it.
-        //
-        // This message should be in sync with the [`oak::grpc::Invocation`] from the Oak SDK:
-        // the order of the `request_reader` and `response_writer` must be consistent.
-        let invocation = crate::NodeMessage {
-            bytes: vec![],
-            handles: vec![request_reader, response_writer],
-        };
-
-        // Serialize gRPC request into a message.
-        let mut message = crate::NodeMessage {
-            bytes: vec![],
-            handles: vec![],
+        let invocation = GrpcInvocation {
+            receiver: Some(Receiver::<GrpcRequest>::new(ReadHandle {
+                handle: request_reader,
+            })),
+            sender: Some(Sender::<GrpcResponse>::new(WriteHandle {
+                handle: response_writer,
+            })),
         };
-        request.encode(&mut message.bytes).map_err(|error| {
-            error!("Couldn't serialize GrpcRequest message: {}", error);
-        })?;
 
         // Put the gRPC request message inside the per-invocation request channel.
-        self.runtime
-            .channel_write(request_writer, message)
-            .map_err(|error| {
-                error!(
-                    "Couldn't write message to the gRPC request channel: {:?}",
-                    error
-                );
-            })?;
+        Sender::<GrpcRequest>::new(WriteHandle {
+            handle: request_writer,
+        })
+        .send(request, &self.runtime)
+        .map_err(|error| {
+            error!(
+                "Couldn't write message to the gRPC request channel: {:?}",
+                error
+            );
+        })?;
 
         // Send an invocation message (with attached handles) to the Oak Node.
-        self.runtime
-            .channel_write(self.invocation_channel, invocation)
+        self.invocation_channel
+            .send(invocation, &self.runtime)
             .map_err(|error| {
                 error!("Couldn't write gRPC invocation message: {:?}", error);
             })?;
@@ -518,7 +479,9 @@ impl GrpcInvocationHandler {
 
         Ok(GrpcResponseIterator::new(
             self.runtime.clone(),
-            response_reader,
+            ReadHandle {
+                handle: response_reader,
+            },
             self.method_name.clone(),
         ))
     }
@@ -574,7 +537,7 @@ impl Drop for MetricsRecorder {
 
 struct GrpcResponseIterator {
     runtime: RuntimeProxy,
-    response_reader: oak_abi::Handle,
+    response_reader: Option<Receiver<GrpcResponse>>,
     method_name: String,
     // The lifetime of the metrics_recorder matches the lifetime of the
     // iterator, updating the metrics when the iterator is dropped.
@@ -583,13 +546,14 @@ struct GrpcResponseIterator {
 }
 
 impl GrpcResponseIterator {
-    fn new(runtime: RuntimeProxy, response_reader: oak_abi::Handle, method_name: String) -> Self {
+    fn new(runtime: RuntimeProxy, response_read_handle: ReadHandle, method_name: String) -> Self {
         trace!(
             "Create new GrpcResponseIterator for '{}', reading from {}",
             method_name,
-            response_reader
+            response_read_handle.handle
         );
         let metrics_recorder = MetricsRecorder::new(runtime.clone(), method_name.clone());
+        let response_reader = Some(Receiver::<GrpcResponse>::new(response_read_handle));
         GrpcResponseIterator {
             runtime,
             response_reader,
@@ -604,13 +568,15 @@ impl GrpcResponseIterator {
 /// is always closed.
 impl Drop for GrpcResponseIterator {
     fn drop(&mut self) {
-        trace!(
-            "Dropping GrpcResponseIterator for '{}': close channel {}",
-            self.method_name,
-            self.response_reader
-        );
-        if let Err(err) = self.runtime.channel_close(self.response_reader) {
-            error!("Failed to close gRPC response reader channel: {:?}", err);
+        if let Some(receiver) = self.response_reader.take() {
+            trace!(
+                "Dropping GrpcResponseIterator for '{}': close channel {}",
+                self.method_name,
+                receiver.handle.handle
+            );
+            if let Err(err) = receiver.close(&self.runtime) {
+                error!("Failed to close gRPC response reader channel: {:?}", err);
+            }
         }
         // Note that dropping self.metrics_recorder will record the duration, and update the
         // `grpc_server_msg_sent_total` metric.
@@ -625,17 +591,9 @@ impl Iterator for GrpcResponseIterator {
         if self.done {
             return None;
         }
-        let read_status = self
-            .runtime
-            .wait_on_channels(&[self.response_reader])
-            .map_err(|error| {
-                error!("Couldn't wait on the gRPC response channel: {:?}", error);
-            })
-            .ok()?;
-
-        if read_status[0] == ChannelReadStatus::ReadReady {
-            match self.runtime.channel_read(self.response_reader) {
-                Ok(Some(msg)) => match GrpcResponse::decode(msg.bytes.as_slice()) {
+        match &self.response_reader {
+            Some(receiver) => {
+                match receiver.receive(&self.runtime) {
                     Ok(grpc_rsp) => {
                         self.metrics_recorder
                             .observe_message_with_len(grpc_rsp.rsp_msg.len());
@@ -659,29 +617,13 @@ impl Iterator for GrpcResponseIterator {
                         );
                         Some(grpc_rsp)
                     }
-                    Err(err) => {
-                        error!("Couldn't parse the GrpcResponse message: {}", err);
+                    Err(error) => {
+                        error!("Error reading gRPC response: {:?}", error);
                         None
                     }
-                },
-                Ok(None) => {
-                    error!("No message available on gRPC response channel");
-                    None
-                }
-                Err(status) => {
-                    error!("Couldn't read from the gRPC response channel: {:?}", status);
-                    None
                 }
             }
-        } else if read_status[0] == ChannelReadStatus::Orphaned {
-            debug!("gRPC response channel closed");
-            None
-        } else {
-            error!(
-                "Couldn't read from the gRPC response channel: {:?}",
-                read_status[0]
-            );
-            None
+            None => None,
         }
     }
 }
diff --git a/oak_runtime/src/node/http/mod.rs b/oak_runtime/src/node/http/mod.rs
index 87ae23e9d4..c4b7a9eeeb 100644
--- a/oak_runtime/src/node/http/mod.rs
+++ b/oak_runtime/src/node/http/mod.rs
@@ -22,7 +22,8 @@
 use crate::{
     io::{ReceiverExt, SenderExt},
     node::{ConfigurationError, Node},
-    ChannelHalfDirection, RuntimeProxy,
+    proto::oak::invocation::{HttpInvocation, HttpInvocationSender},
+    RuntimeProxy,
 };
 use core::task::{Context, Poll};
 use futures_util::stream::Stream;
@@ -31,12 +32,10 @@ use hyper::{
     Body, Request, Response, Server, StatusCode,
 };
 use log::{debug, error, info, warn};
-use oak_abi::{
-    label::Label, proto::oak::application::HttpServerConfiguration, ChannelReadStatus, OakStatus,
-};
+use oak_abi::{label::Label, proto::oak::application::HttpServerConfiguration, OakStatus};
 use oak_io::{
     handle::{ReadHandle, WriteHandle},
-    OakError,
+    OakError, Receiver, Sender,
 };
 use oak_services::proto::oak::encap::{HttpRequest, HttpResponse};
 use std::{io, net::SocketAddr, pin::Pin};
@@ -109,77 +108,6 @@ impl HttpServerNode {
         })
     }
 
-    /// Reads the [`oak_abi::Handle`] for the write half of an invocation from a startup channel.
-    /// Returns an error if the startup channel couldn't be read, or if the initial message
-    /// is invalid (doesn't contain exactly one write handle).
-    fn try_get_invocation_channel(
-        runtime: &RuntimeProxy,
-        startup_handle: oak_abi::Handle,
-    ) -> Result<oak_abi::Handle, OakStatus> {
-        // Wait until a message is available on the startup channel
-        let read_status = runtime
-            .wait_on_channels(&[startup_handle])
-            .map_err(|error| {
-                error!("Couldn't wait on the initial reader handle: {:?}", error);
-                OakStatus::ErrInternal
-            })?;
-
-        // TODO(#389): Automatically generate this code.
-        let invocation_channel = if read_status[0] == ChannelReadStatus::ReadReady {
-            HttpServerNode::get_invocation_channel(runtime, startup_handle)
-        } else {
-            error!("Couldn't read channel: {:?}", read_status[0]);
-            Err(OakStatus::ErrInternal)
-        }?;
-
-        info!(
-            "Invocation channel write handle received: {}",
-            invocation_channel
-        );
-        Ok(invocation_channel)
-    }
-
-    fn get_invocation_channel(
-        runtime: &RuntimeProxy,
-        startup_handle: oak_abi::Handle,
-    ) -> Result<oak_abi::Handle, OakStatus> {
-        runtime.channel_read(startup_handle)
-                .map_err(|error| {
-                    error!("Couldn't read from the initial reader handle {:?}", error);
-                    OakStatus::ErrInternal
-                })
-                .and_then(|message| {
-                    message
-                        .ok_or_else(|| {
-                            error!("Empty message");
-                            OakStatus::ErrInternal
-                        })
-                        .and_then(|m| {
-                            // TODO(#1186): create an Init object and define encode/decode for it
-                            // Ref: https://github.com/project-oak/oak/pull/1261#discussion_r457943479
-                            if m.handles.len() == 1 {
-                                let handle = m.handles[0];
-                                match runtime.channel_direction(handle)? {
-                                    ChannelHalfDirection::Write => Ok(handle),
-                                    ChannelHalfDirection::Read => {
-                                        error!(
-                                            "Http server pseudo-node should receive a writer handle, found reader handle {}",
-                                            handle
-                                        );
-                                        Err(OakStatus::ErrBadHandle)
-                                    },
-                                }
-                            } else {
-                                error!(
-                                    "Http server pseudo-node should receive a single writer handle, found {}",
-                                    m.handles.len()
-                                );
-                                Err(OakStatus::ErrInternal)
-                            }
-                        })
-                })
-    }
-
     // Make a server, with graceful shutdown, from the given [`HttpRequestHandler`].
     async fn make_server(
         &self,
@@ -289,9 +217,18 @@ impl Node for HttpServerNode {
         // At start-of-day we need/expect to receive a write handle for an invocation channel
         // to use for all subsequent activity.
         info!("{}: Waiting for invocation channel", self.node_name);
+        let startup_receiver = Receiver::<HttpInvocationSender>::new(ReadHandle {
+            handle: startup_handle,
+        });
         let invocation_channel =
-            match HttpServerNode::try_get_invocation_channel(&runtime, startup_handle) {
-                Ok(writer) => writer,
+            match startup_receiver
+                .receive(&runtime)
+                .and_then(|invocation_sender| {
+                    invocation_sender
+                        .sender
+                        .ok_or(OakError::OakStatus(OakStatus::ErrBadHandle))
+                }) {
+                Ok(sender) => sender.handle.handle,
                 Err(status) => {
                     error!(
                         "Failed to retrieve invocation channel write handle: {:?}",
@@ -300,7 +237,7 @@ impl Node for HttpServerNode {
                     return;
                 }
             };
-        if let Err(err) = runtime.channel_close(startup_handle) {
+        if let Err(err) = startup_receiver.close(&runtime) {
             error!(
                 "Failed to close initial inbound channel {}: {:?}",
                 startup_handle, err
@@ -443,16 +380,20 @@ impl Pipe {
         runtime: &RuntimeProxy,
         invocation_channel: oak_abi::Handle,
     ) -> Result<(), ()> {
-        // Create an invocation message and attach the request specific channels to it.
-        // TODO(#1186): Use a generic version of gRPC invocation, instead of serializing manually
-        let invocation = crate::NodeMessage {
-            bytes: vec![],
-            handles: vec![self.request_reader, self.response_writer],
+        // Create an invocation containing request-specific channels.
+        let invocation = HttpInvocation {
+            receiver: Some(Receiver::new(ReadHandle {
+                handle: self.request_reader,
+            })),
+            sender: Some(Sender::new(WriteHandle {
+                handle: self.response_writer,
+            })),
         };
-
-        // Send an invocation message (with attached handles) to the Oak Node.
-        runtime
-            .channel_write(invocation_channel, invocation)
+        let invocation_sender = crate::io::Sender::new(WriteHandle {
+            handle: invocation_channel,
+        });
+        invocation_sender
+            .send(invocation, runtime)
             .map_err(|error| {
                 error!("Couldn't write the invocation message: {:?}", error);
             })
diff --git a/oak_runtime/src/proto/mod.rs b/oak_runtime/src/proto/mod.rs
index a0ab19eb1e..324bca3fea 100644
--- a/oak_runtime/src/proto/mod.rs
+++ b/oak_runtime/src/proto/mod.rs
@@ -24,4 +24,8 @@ pub mod oak {
     pub mod introspection_events {
         include!(concat!(env!("OUT_DIR"), "/oak.introspection_events.rs"));
     }
+
+    pub mod invocation {
+        include!(concat!(env!("OUT_DIR"), "/oak.invocation.rs"));
+    }
 }
diff --git a/oak_services/build.rs b/oak_services/build.rs
index 67e114ebec..225302bec6 100644
--- a/oak_services/build.rs
+++ b/oak_services/build.rs
@@ -21,18 +21,32 @@ fn main() {
         &[
             "../oak_services/proto/grpc_encap.proto",
             "../oak_services/proto/http_encap.proto",
-            "../oak_services/proto/log.proto",
-            "../oak_services/proto/roughtime_service.proto",
             "../third_party/google/rpc/code.proto",
             "../third_party/google/rpc/status.proto",
         ],
         &[".."],
         oak_utils::ProtoOptions {
-            // Exclude generation of service code and HandleVisit auto-derive, as it would require a
-            // reference to the Oak SDK to compile.
+            // Exclude generation of service code as it would require a reference to the Oak SDK to
+            // compile.
             generate_services: false,
+            // Automatic derivation of HandleVisit is not supported for the encap protos (or protos
+            // they reference) due to special handling of the `oak.encap` package in the
+            // code generation.
             derive_handle_visit: false,
             ..Default::default()
         },
     );
+    oak_utils::compile_protos_with_options(
+        &[
+            "../oak_services/proto/log.proto",
+            "../oak_services/proto/roughtime_service.proto",
+        ],
+        &[".."],
+        oak_utils::ProtoOptions {
+            // Exclude generation of service code as it would require a reference to the Oak SDK to
+            // compile.
+            generate_services: false,
+            ..Default::default()
+        },
+    );
 }
diff --git a/oak_services/proto/grpc_invocation.proto b/oak_services/proto/grpc_invocation.proto
new file mode 100644
index 0000000000..d7e31e8556
--- /dev/null
+++ b/oak_services/proto/grpc_invocation.proto
@@ -0,0 +1,38 @@
+//
+// Copyright 2020 The Project Oak Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+syntax = "proto3";
+
+package oak.invocation;
+
+import "oak_services/proto/grpc_encap.proto";
+import "proto/handle.proto";
+
+// A gRPC invocation, consisting of exactly two channels: one to read incoming requests from the
+// client (wrapped in a [`Receiver`]), and one to write outgoing responses to the client (wrapped
+// in a [`Sender`]).
+//
+// [`Receiver`]: oak_io::Receiver
+// [`Sender`]: oak_io::Sender
+message GrpcInvocation {
+  oak.handle.Receiver receiver = 1 [(oak.handle.message_type) = ".oak.encap.GrpcRequest"];
+  oak.handle.Sender sender = 2 [(oak.handle.message_type) = ".oak.encap.GrpcResponse"];
+}
+
+// A wrapper for sending a [`GrpcInvocation`] over a channel.
+message GrpcInvocationSender {
+  oak.handle.Sender sender = 1 [(oak.handle.message_type) = ".oak.invocation.GrpcInvocation"];
+}
diff --git a/oak_services/proto/http_encap.proto b/oak_services/proto/http_encap.proto
index ad8d2d3e8d..fec227a7bb 100644
--- a/oak_services/proto/http_encap.proto
+++ b/oak_services/proto/http_encap.proto
@@ -18,8 +18,6 @@ syntax = "proto3";
 
 package oak.encap;
 
-import "third_party/google/rpc/status.proto";
-
 // Protocol buffer encoding representing an HTTP request.
 message HttpRequest {
   // The path to which the request was sent.
diff --git a/oak_services/proto/http_invocation.proto b/oak_services/proto/http_invocation.proto
new file mode 100644
index 0000000000..215cba07f9
--- /dev/null
+++ b/oak_services/proto/http_invocation.proto
@@ -0,0 +1,38 @@
+//
+// Copyright 2020 The Project Oak Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+syntax = "proto3";
+
+package oak.invocation;
+
+import "oak_services/proto/http_encap.proto";
+import "proto/handle.proto";
+
+// An HTTP invocation, consisting of exactly two channels: one to read incoming requests from the
+// client (wrapped in a [`Receiver`]), and one to write outgoing responses to the client (wrapped
+// in a [`Sender`]).
+//
+// [`Receiver`]: oak_io::Receiver
+// [`Sender`]: oak_io::Sender
+message HttpInvocation {
+  oak.handle.Receiver receiver = 1 [(oak.handle.message_type) = ".oak.encap.HttpRequest"];
+  oak.handle.Sender sender = 2 [(oak.handle.message_type) = ".oak.encap.HttpResponse"];
+}
+
+// A wrapper for sending an [`HttpInvocation`] over a channel.
+message HttpInvocationSender {
+  oak.handle.Sender sender = 1 [(oak.handle.message_type) = ".oak.invocation.HttpInvocation"];
+}
diff --git a/oak_services/src/proto/mod.rs b/oak_services/src/proto/mod.rs
index 63f4408c40..88f008b765 100644
--- a/oak_services/src/proto/mod.rs
+++ b/oak_services/src/proto/mod.rs
@@ -41,6 +41,5 @@ handle_visit_blanket_impl!(
     oak::encap::GrpcResponse,
     oak::encap::GrpcRequest,
     oak::encap::HttpResponse,
-    oak::encap::HttpRequest,
-    oak::log::LogMessage
+    oak::encap::HttpRequest
 );
diff --git a/runner/src/main.rs b/runner/src/main.rs
index f8f12d8045..6316dfe576 100644
--- a/runner/src/main.rs
+++ b/runner/src/main.rs
@@ -924,6 +924,7 @@ fn is_ignored_path(path: &PathBuf) -> bool {
         || components.contains(&std::path::Component::Normal("bazel-wasm-oak".as_ref()))
         || components.contains(&std::path::Component::Normal("cargo-cache".as_ref()))
         || components.contains(&std::path::Component::Normal("node_modules".as_ref()))
+        || components.contains(&std::path::Component::Normal("protoc_out".as_ref())) // Code generated by protoc
         || components.contains(&std::path::Component::Normal("target".as_ref())) // Rust artifacts.
         || components.contains(&std::path::Component::Normal("third_party".as_ref()))
 }
diff --git a/sdk/Cargo.lock b/sdk/Cargo.lock
index f40fc85dfa..c084a28561 100644
--- a/sdk/Cargo.lock
+++ b/sdk/Cargo.lock
@@ -856,7 +856,6 @@ dependencies = [
 name = "oak_derive"
 version = "0.1.0"
 dependencies = [
- "oak_io",
  "proc-macro2",
  "quote",
  "syn",
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index bd61c6b027..e7ebaffffc 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,10 +1,5 @@
 [workspace]
-members = [
-  "rust/oak",
-  "rust/oak_app_build",
-  "rust/oak_derive",
-  "rust/oak_tests"
-]
+members = ["rust/oak", "rust/oak_app_build", "rust/oak_tests"]
 
 # Patch dependencies on oak crates so that they refer to the versions within this same repository.
 #
@@ -16,7 +11,6 @@ members = [
 oak = { path = "rust/oak" }
 oak_abi = { path = "../oak_abi" }
 oak_app_build = { path = "rust/oak_app_build" }
-oak_derive = { path = "rust/oak_derive" }
 oak_io = { path = "../oak_io" }
 oak_runtime = { path = "../oak_runtime" }
 oak_services = { path = "../oak_services" }
diff --git a/sdk/rust/oak/Cargo.toml b/sdk/rust/oak/Cargo.toml
index 0d897a2d2c..3d3dcdcea5 100644
--- a/sdk/rust/oak/Cargo.toml
+++ b/sdk/rust/oak/Cargo.toml
@@ -7,7 +7,7 @@ license = "Apache-2.0"
 
 [dependencies]
 oak_abi = "=0.1.0"
-oak_derive = "=0.1.0"
+oak_derive = { path = "../../../oak_derive" }
 oak_io = "=0.1.0"
 oak_services = "=0.1.0"
 prost = "*"
diff --git a/sdk/rust/oak/build.rs b/sdk/rust/oak/build.rs
index 5d95b5660a..9343779982 100644
--- a/sdk/rust/oak/build.rs
+++ b/sdk/rust/oak/build.rs
@@ -17,12 +17,13 @@
 fn main() {
     oak_utils::compile_protos_with_options(
         &[
+            "../../../oak_services/proto/grpc_invocation.proto",
+            "../../../oak_services/proto/http_invocation.proto",
             "../../../oak_services/proto/storage_service.proto",
             "../../../oak_services/proto/roughtime_service.proto",
         ],
         &["../../.."],
         oak_utils::ProtoOptions {
-            derive_handle_visit: false,
             ..Default::default()
         },
     );
diff --git a/sdk/rust/oak/src/grpc/mod.rs b/sdk/rust/oak/src/grpc/mod.rs
index 965cc19e71..d2d88a2a4b 100644
--- a/sdk/rust/oak/src/grpc/mod.rs
+++ b/sdk/rust/oak/src/grpc/mod.rs
@@ -18,7 +18,7 @@
 
 use crate::{
     io::{ReceiverExt, SenderExt},
-    OakError,
+    OakError, OakStatus,
 };
 use log::{error, warn};
 use oak_services::proto::google::rpc;
@@ -34,7 +34,7 @@ pub mod server;
 ///
 /// [`Status`]: oak_services::proto::google::rpc::Status
 pub type Result<T> = std::result::Result<T, rpc::Status>;
-pub type Invocation = crate::invocations::Invocation<GrpcRequest, GrpcResponse>;
+pub type Invocation = crate::proto::oak::invocation::GrpcInvocation;
 
 /// Helper to create a gRPC status object.
 pub fn build_status(code: rpc::Code, msg: &str) -> rpc::Status {
@@ -157,10 +157,17 @@ impl<T: ServerNode> crate::Node<Invocation> for T {
     ///
     /// [`invoke`]: ServerNode::invoke
     fn handle_command(&mut self, invocation: Invocation) -> std::result::Result<(), OakError> {
-        let response_writer = ChannelResponseWriter::new(invocation.response_sender);
+        let response_writer = ChannelResponseWriter::new(
+            invocation
+                .sender
+                .ok_or(OakError::OakStatus(OakStatus::ErrBadHandle))?,
+        );
 
+        let request_receiver = invocation
+            .receiver
+            .ok_or(OakError::OakStatus(OakStatus::ErrBadHandle))?;
         // Read a single encapsulated request message from the read half.
-        let req: GrpcRequest = invocation.request_receiver.receive().map_err(|err| {
+        let req: GrpcRequest = request_receiver.receive().map_err(|err| {
             // TODO(#1078): If it's clear that the issue is caused by a too restrictive label
             // (permission denied), then return a more specific error to the gRPC client.
             warn!(
@@ -171,8 +178,9 @@ impl<T: ServerNode> crate::Node<Invocation> for T {
             // gRPC error response back to the gRPC client, rather than just returning from this
             // method with `?`, since otherwise that would leave the gRPC client hanging
             // forever.
-            // If sending this gRPC request also fails, we just log the error here and discard it,
-            // but we still return the original error to the caller of this function.
+            // If sending this gRPC request also fails, we just log the error here and discard
+            // it, but we still return the original error to the caller of this
+            // function.
             if let Err(err) = response_writer.close(Result::Err(build_status(
                 rpc::Code::InvalidArgument,
                 "could not process gRPC request",
@@ -187,7 +195,7 @@ impl<T: ServerNode> crate::Node<Invocation> for T {
 
         // Since we are expecting a single message, close the channel immediately.
         // This will change when we implement client streaming (#97).
-        invocation.request_receiver.close()?;
+        request_receiver.close()?;
         if !req.last {
             // TODO(#97): Implement client streaming.
             panic!("Support for streaming requests not yet implemented");
@@ -243,8 +251,8 @@ where
     // Build an Invocation holding the two channels and send it down the
     // specified channel.
     let invocation = Invocation {
-        request_receiver: req_receiver.clone(),
-        response_sender: rsp_sender.clone(),
+        receiver: Some(req_receiver.clone()),
+        sender: Some(rsp_sender.clone()),
     };
     invocation_channel
         .send(&invocation)
diff --git a/sdk/rust/oak/src/grpc/server.rs b/sdk/rust/oak/src/grpc/server.rs
index 5953372f6f..ada35619aa 100644
--- a/sdk/rust/oak/src/grpc/server.rs
+++ b/sdk/rust/oak/src/grpc/server.rs
@@ -18,21 +18,20 @@
 
 use crate::{
     grpc::Invocation,
-    invocations::ServerInvocationChannel,
     io::{Receiver, ReceiverExt, SenderExt},
+    proto::oak::invocation::GrpcInvocationSender,
     OakStatus,
 };
 
-use oak_services::proto::oak::encap::{GrpcRequest, GrpcResponse};
-
 /// Initializes a gRPC server pseudo-Node listening on the provided address.
 ///
 /// Returns a [`Receiver`] to read gRPC [`Invocation`]s from.
+///
+/// [`Receiver`]: oak_io::Receiver
 pub fn init(address: &str) -> Result<Receiver<Invocation>, OakStatus> {
     // Create a channel and pass the read half to a new gRPC server pseudo-Node.
-    let (init_sender, init_receiver) =
-        crate::io::channel_create::<ServerInvocationChannel<GrpcRequest, GrpcResponse>>()
-            .expect("Couldn't create init channel to gRPC server pseudo-node");
+    let (init_sender, init_receiver) = crate::io::channel_create::<GrpcInvocationSender>()
+        .expect("Couldn't create init channel to gRPC server pseudo-node");
     let config = crate::node_config::grpc_server(address);
     crate::node_create(&config, init_receiver.handle)?;
     init_receiver
@@ -44,7 +43,9 @@ pub fn init(address: &str) -> Result<Receiver<Invocation>, OakStatus> {
         crate::io::channel_create::<Invocation>().expect("Couldn't create gRPC invocation channel");
     let local_invocation_sender = invocation_sender.handle;
 
-    let grpc_server_init = ServerInvocationChannel::new(invocation_sender);
+    let grpc_server_init = GrpcInvocationSender {
+        sender: Some(invocation_sender),
+    };
     init_sender
         .send(&grpc_server_init)
         .expect("Could not send init message to gRPC server pseudo-node");
diff --git a/sdk/rust/oak/src/http/mod.rs b/sdk/rust/oak/src/http/mod.rs
index db0bdaafa7..ef484584d3 100644
--- a/sdk/rust/oak/src/http/mod.rs
+++ b/sdk/rust/oak/src/http/mod.rs
@@ -17,24 +17,61 @@
 //! Functionality to help Oak Nodes create HTTP server pseudo-Nodes.
 
 use crate::{
-    invocations::ServerInvocationChannel,
     io::{Receiver, ReceiverExt, SenderExt},
-    OakStatus,
+    proto::oak::invocation::HttpInvocationSender,
+    OakError, OakStatus,
 };
-use oak_services::proto::oak::encap::{HttpRequest, HttpResponse};
+use log::warn;
+pub use oak_services::proto::oak::encap::{HttpRequest, HttpResponse};
 
-pub type Invocation = crate::invocations::Invocation<HttpRequest, HttpResponse>;
+pub type Invocation = crate::proto::oak::invocation::HttpInvocation;
+
+impl Invocation {
+    pub fn receive(&self) -> std::result::Result<HttpRequest, crate::OakError> {
+        self.receiver
+            .as_ref()
+            .ok_or(OakError::OakStatus(OakStatus::ErrBadHandle))?
+            .receive()
+    }
+
+    pub fn send(&self, response: &HttpResponse) -> std::result::Result<(), crate::OakError> {
+        self.sender
+            .as_ref()
+            .ok_or(OakError::OakStatus(OakStatus::ErrBadHandle))?
+            .send(response)
+    }
+
+    pub fn close_channels(&self) {
+        match self.receiver.as_ref() {
+            Some(receiver) => {
+                if let Err(error) = receiver.close() {
+                    warn!("Error when trying to close receiver: {}", error);
+                }
+            }
+            None => warn!("No receive present on invocation."),
+        };
+        match self.sender.as_ref() {
+            Some(sender) => {
+                if let Err(error) = sender.close() {
+                    warn!("Error when trying to close response_sender: {}", error);
+                }
+            }
+            None => warn!("No receive present on invocation."),
+        };
+    }
+}
 
 /// Initializes an HTTP server pseudo-Node listening on the provided address.
 ///
 /// Returns a [`Receiver`] to read HTTP [`Invocation`]s from.
+///
+/// [`Receiver`]: oak_io::Receiver
 pub fn init(address: &str) -> Result<Receiver<Invocation>, OakStatus> {
     let config = crate::node_config::http_server(address);
 
     // Create a channel and pass the read half to a new http server pseudo-Node.
-    let (init_sender, init_receiver) =
-        crate::io::channel_create::<ServerInvocationChannel<HttpRequest, HttpResponse>>()
-            .expect("Couldn't create init channel to HTTP server pseudo-node");
+    let (init_sender, init_receiver) = crate::io::channel_create::<HttpInvocationSender>()
+        .expect("Couldn't create init channel to HTTP server pseudo-node");
     crate::node_create(&config, init_receiver.handle)?;
     init_receiver
         .close()
@@ -44,7 +81,9 @@ pub fn init(address: &str) -> Result<Receiver<Invocation>, OakStatus> {
     let (invocation_sender, invocation_receiver) =
         crate::io::channel_create::<Invocation>().expect("Couldn't create HTTP invocation channel");
 
-    let http_server_init = ServerInvocationChannel::new(invocation_sender);
+    let http_server_init = HttpInvocationSender {
+        sender: Some(invocation_sender),
+    };
     init_sender
         .send(&http_server_init)
         .expect("Could not send init message to HTTP server pseudo-node");
diff --git a/sdk/rust/oak/src/invocations.rs b/sdk/rust/oak/src/invocations.rs
deleted file mode 100644
index c0c49dd63b..0000000000
--- a/sdk/rust/oak/src/invocations.rs
+++ /dev/null
@@ -1,130 +0,0 @@
-//
-// Copyright 2020 The Project Oak Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-use crate::io::{ReceiverExt, SenderExt};
-use log::warn;
-
-/// An invocation, consisting of exactly two channels: one to read incoming requests from the
-/// client, and one to write outgoing responses to the client.
-pub struct Invocation<R: crate::io::Decodable, S: crate::io::Encodable> {
-    pub request_receiver: crate::io::Receiver<R>,
-    pub response_sender: crate::io::Sender<S>,
-}
-
-// TODO(#389): Automatically generate this code.
-impl<R: crate::io::Decodable, S: crate::io::Encodable> crate::io::Encodable for Invocation<R, S> {
-    fn encode(&self) -> Result<crate::io::Message, crate::OakError> {
-        let bytes = vec![];
-        let handles = vec![
-            self.request_receiver.handle.handle,
-            self.response_sender.handle.handle,
-        ];
-        Ok(crate::io::Message { bytes, handles })
-    }
-}
-
-// TODO(#389): Automatically generate this code.
-impl<R: crate::io::Decodable, S: crate::io::Encodable> crate::io::Decodable for Invocation<R, S> {
-    fn decode(message: &crate::io::Message) -> Result<Self, crate::OakError> {
-        if !message.bytes.is_empty() {
-            panic!(
-                "incorrect number of bytes received: {} (expected: 0)",
-                message.bytes.len()
-            );
-        }
-        if message.handles.len() != 2 {
-            panic!(
-                "incorrect number of handles received: {} (expected: 2)",
-                message.handles.len()
-            );
-        }
-        Ok(Invocation {
-            request_receiver: crate::io::Receiver::new(crate::ReadHandle {
-                handle: message.handles[0],
-            }),
-            response_sender: crate::io::Sender::new(crate::WriteHandle {
-                handle: message.handles[1],
-            }),
-        })
-    }
-}
-
-impl<R: crate::io::Decodable, S: crate::io::Encodable> Invocation<R, S> {
-    pub fn receive(&self) -> Result<R, crate::OakError> {
-        self.request_receiver.receive()
-    }
-
-    pub fn send(&self, response: &S) -> Result<(), crate::OakError> {
-        self.response_sender.send(response)
-    }
-
-    pub fn close_channels(&self) {
-        if let Err(error) = self.request_receiver.close() {
-            warn!("Error when trying to close request_receiver: {}", error);
-        }
-        if let Err(error) = self.response_sender.close() {
-            warn!("Error when trying to close response_sender: {}", error);
-        }
-    }
-}
-
-/// Wrapper for a sender that can be used to send invocations to an Oak node that will handle the
-/// requests.
-pub struct ServerInvocationChannel<R: crate::io::Decodable, S: crate::io::Encodable> {
-    invocation_sender: crate::io::Sender<Invocation<R, S>>,
-}
-
-impl<R: crate::io::Decodable, S: crate::io::Encodable> ServerInvocationChannel<R, S> {
-    pub fn new(invocation_sender: crate::io::Sender<Invocation<R, S>>) -> Self {
-        ServerInvocationChannel { invocation_sender }
-    }
-}
-
-// TODO(#389): Automatically generate this code.
-impl<R: crate::io::Decodable, S: crate::io::Encodable> crate::io::Encodable
-    for ServerInvocationChannel<R, S>
-{
-    fn encode(&self) -> Result<crate::io::Message, crate::OakError> {
-        let bytes = vec![];
-        let handles = vec![self.invocation_sender.handle.handle];
-        Ok(crate::io::Message { bytes, handles })
-    }
-}
-
-// TODO(#389): Automatically generate this code.
-impl<R: crate::io::Decodable, S: crate::io::Encodable> crate::io::Decodable
-    for ServerInvocationChannel<R, S>
-{
-    fn decode(message: &crate::io::Message) -> Result<Self, crate::OakError> {
-        if !message.bytes.is_empty() {
-            panic!(
-                "incorrect number of bytes received: {} (expected: 0)",
-                message.bytes.len()
-            );
-        }
-        if message.handles.len() != 1 {
-            panic!(
-                "incorrect number of handles received: {} (expected: 1)",
-                message.handles.len()
-            );
-        }
-        Ok(ServerInvocationChannel {
-            invocation_sender: crate::io::Sender::new(crate::WriteHandle {
-                handle: message.handles[0],
-            }),
-        })
-    }
-}
diff --git a/sdk/rust/oak/src/lib.rs b/sdk/rust/oak/src/lib.rs
index 122b5a2179..706352393d 100644
--- a/sdk/rust/oak/src/lib.rs
+++ b/sdk/rust/oak/src/lib.rs
@@ -38,7 +38,6 @@ mod stubs;
 pub mod grpc;
 pub mod handle;
 pub mod http;
-pub mod invocations;
 pub mod io;
 pub mod logger;
 pub mod node_config;
@@ -52,6 +51,9 @@ pub mod proto {
         // The storage protobuf messages use the label.Label type which is built
         // in the `oak_abi` crate, so make it available here too.
         pub use oak_abi::proto::oak::{application, label};
+        pub mod invocation {
+            include!(concat!(env!("OUT_DIR"), "/oak.invocation.rs"));
+        }
         pub mod storage {
             include!(concat!(env!("OUT_DIR"), "/oak.storage.rs"));
         }
diff --git a/sdk/rust/oak_derive/Cargo.toml b/sdk/rust/oak_derive/Cargo.toml
deleted file mode 100644
index 7b68ca9120..0000000000
--- a/sdk/rust/oak_derive/Cargo.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "oak_derive"
-version = "0.1.0"
-authors = ["Daan de Graaf <daagra@google.com>"]
-edition = "2018"
-license = "Apache-2.0"
-
-[lib]
-proc-macro = true
-
-[dependencies]
-syn = "1.0"
-quote = "1.0"
-proc-macro2 = "1.0"
-
-[dev-dependencies]
-oak_io = "*"


Test patch:
diff --git a/oak_runtime/src/node/http/tests.rs b/oak_runtime/src/node/http/tests.rs
index 15d6e0b652..c167030aec 100644
--- a/oak_runtime/src/node/http/tests.rs
+++ b/oak_runtime/src/node/http/tests.rs
@@ -17,7 +17,6 @@
 use super::*;
 use maplit::hashmap;
 use oak_abi::{label::Label, proto::oak::application::ApplicationConfiguration};
-use prost::Message;
 use std::{fs, option::Option, thread::JoinHandle};
 
 struct HttpServerTester {
@@ -156,48 +155,41 @@ fn create_communication_channel(runtime: &RuntimeProxy) -> (oak_abi::Handle, oak
     let (invocation_sender, invocation_receiver) = runtime
         .channel_create(&Label::public_untrusted())
         .expect("Could not create channel");
-    let message = crate::NodeMessage {
-        bytes: vec![],
-        handles: vec![invocation_sender],
+    let invocation_sender = HttpInvocationSender {
+        sender: Some(Sender::<HttpInvocation>::new(WriteHandle {
+            handle: invocation_sender,
+        })),
     };
+    let init_sender = Sender::<HttpInvocationSender>::new(WriteHandle {
+        handle: init_sender,
+    });
 
-    let _ = runtime
-        .channel_write(init_sender, message)
-        .map_err(|err| panic!("Could not write to the `init_sender` channel: {}", err));
-    let _ = runtime
-        .channel_close(init_sender)
-        .map_err(|err| panic!("Could not close the `init_sender` channel: {}.", err));
+    if let Err(error) = init_sender.send(invocation_sender, runtime) {
+        panic!("Could not write to the `init_sender` channel: {}", error);
+    }
+    if let Err(error) = init_sender.close(runtime) {
+        panic!("Could not close the `init_sender` channel: {}", error);
+    }
 
     (init_receiver, invocation_receiver)
 }
 
 fn oak_node_simulator(runtime: &RuntimeProxy, invocation_receiver: oak_abi::Handle) {
     // Get invocation message that contains the response_writer handle.
-    let read_status = runtime
-        .wait_on_channels(&[invocation_receiver])
-        .expect("Error while waiting on invocation_receiver");
-    if read_status[0] == ChannelReadStatus::ReadReady {
-        if let Ok(Some(msg)) = runtime.channel_read(invocation_receiver) {
-            // Prepare the response
-            let resp = HttpResponse {
-                body: vec![],
-                status: http::status::StatusCode::OK.as_u16() as i32,
-                headers: hashmap! {},
-            };
-            let mut message = crate::NodeMessage {
-                bytes: vec![],
-                handles: vec![],
-            };
-            resp.encode(&mut message.bytes)
-                .expect("could not serialize response to bytes");
-
-            // Send the response over the response_writer channel
-            let response_writer_handle = msg.handles[1];
-            let _ = runtime.channel_write(response_writer_handle, message);
-        }
-    } else {
-        panic!("Error while waiting for message on invocation_receiver");
-    }
+    let invocation_receiver = Receiver::<HttpInvocation>::new(ReadHandle {
+        handle: invocation_receiver,
+    });
+    let invocation = invocation_receiver.receive(runtime).unwrap();
+    let resp = HttpResponse {
+        body: vec![],
+        status: http::status::StatusCode::OK.as_u16() as i32,
+        headers: hashmap! {},
+    };
+    invocation
+        .sender
+        .expect("Empty sender on invocation.")
+        .send(resp, runtime)
+        .unwrap();
 }
 
 async fn send_request(uri: &str) -> Result<http::response::Response<hyper::Body>, hyper::Error> {