Container Proxy Endpoints

A request on the container proxy connection opens a scope for exactly one of seven endpoints, designated by the tag byte that begins its payload; a request the server cannot read is answered by a bare response finish.

A request the client sends on the connection of the Container Proxy opens a scope for exactly one endpoint. The request’s payload begins with a single tag byte designating the endpoint; the remainder of the payload is the endpoint’s request content, in the form the endpoint defines.

TagEndpoint
0agents::begin
1tools::begin
2fuse::mount
3filesystem::tree
4filesystem::read
5filesystem::write
6filesystem::fuse_serve

The tag values are normative as tabulated. Each endpoint has its own section. Every section of this layer states requirements of the server — the proxy — only: a sentence describing what the client sends states the input the server serves, and imposes no requirement on the client beyond those the Endpoints layer states of the provider’s server.

  • The parties. The provider’s server is the client of the connection and the proxy is the server, in the sense of Frames: the client sends the frame types that layer defines for a client, and the server sends the frame types that layer defines for a server. Every page of this layer uses the two words so, and the crate names the client’s traffic client and the server’s traffic server under every endpoint.
  • The channels. The channels the client opens on a scope are listed under the endpoint’s Client Channels section; the channels the server opens are listed under its Server Channels section, as the Endpoints layer lists them. Each channel is its own section, as Channels provides. The numbering of a channel is its opener’s, per scope, and a channel number of one opener means nothing to the other.
  • An unreadable request is answered, not dropped. A request whose payload the server cannot read — no tag, a tag this table does not list, or content that does not conform to the endpoint’s form — is answered by a response finish that no response precedes. The server sends nothing else for it.
  • Growth is new tag values. A future endpoint is a new tag value in this table. No tag value listed here changes its endpoint.
  • Notation. The JSON forms and the byte layouts of this layer are read as Notation provides.

The requests are defined by diverge-sdk-rs/src/container_proxy/outside/endpoints/client_request.rs:

//! Every request the server can open a scope with.

use std::convert::Infallible;

use super::{agents, filesystem, fuse, tools};
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};

/// The payload of a
/// [`Request`](crate::wire::frame::client::ClientFrame::Request).
///
/// One variant per scope, in tag order, plus
/// [`Invalid`](Self::Invalid). This is the only place the tag values
/// meet: each request states its own in its own module, and nothing
/// else can see all of them at once — so this is what the table in
/// [`container_proxy_endpoints`](super) is a table OF.
///
/// # Decoding cannot fail
///
/// There is no error type. A payload this does not understand — an
/// unknown tag, a body that will not parse, no bytes at all — becomes
/// [`Invalid`](Self::Invalid) rather than a failure, so the proxy
/// always has a request to answer and a malformed one is a scope like
/// any other.
///
/// The alternative was refusing to decode, which would leave the proxy
/// holding bytes it could not name and no scope to complain in. The
/// only thing left would be closing the connection, which is the
/// container's whole life over one bad frame.
#[derive(Debug, Clone, PartialEq)]
pub enum ClientRequest<'a> {
    /// Tag `0`. Begin the server's work on an agent container.
    AgentsBegin(agents::begin::client::request::Frame),
    /// Tag `1`. Begin the server's work on a tool container.
    ToolsBegin(tools::begin::client::request::Frame),
    /// Tag `2`. Make one FUSE mount.
    FuseMount(fuse::mount::client::request::Frame),
    /// Tag `3`. Watch the container's tree.
    FilesystemTree(filesystem::tree::client::request::Frame),
    /// Tag `4`. Read one file out of the container.
    FilesystemRead(filesystem::read::client::request::Frame),
    /// Tag `5`. Write one file into the container.
    FilesystemWrite(filesystem::write::client::request::Frame),
    /// Tag `6`. Serve a subtree of the container live.
    FilesystemFuseServe(filesystem::fuse_serve::client::request::Frame),
    /// Something this version cannot read, kept as it arrived.
    ///
    /// No tag of its own. It is not a request the server sends — it is
    /// what a request BECOMES when the proxy cannot make one of the
    /// others out of it, so it carries the payload verbatim and
    /// encodes back to exactly those bytes.
    ///
    /// # It is answered, not dropped
    ///
    /// The proxy finishes the scope over it, with nothing in front:
    /// seven scopes have seven error vocabularies, and an invalid request
    /// names none of them — where a finish with nothing before it is
    /// already what the wire means by a request that could not be
    /// served. Which is the point all the same: a server that sent
    /// something wrong learns so, in the scope it opened, and its
    /// other work carries on.
    ///
    /// # Why it carries no reason
    ///
    /// The proxy has the bytes and can ask the specific request to
    /// decode them, which answers precisely: an unknown tag, a body
    /// that would not parse, or nothing at all. Storing a reason here
    /// would mean this type choosing which of seven error vocabularies
    /// to speak, and choosing wrong for six of them.
    Invalid(&'a [u8]),
}

impl Encode for ClientRequest<'_> {
    /// The ordinary JSON failure: every request is JSON after its
    /// tag.
    type Error = serde_json::Error;

    fn encode(
        &self,
        out: &mut Writer<'_>,
    ) -> Result<(), serde_json::Error> {
        match self {
            ClientRequest::AgentsBegin(frame) => frame.encode(out),
            ClientRequest::ToolsBegin(frame) => frame.encode(out),
            ClientRequest::FuseMount(frame) => frame.encode(out),
            ClientRequest::FilesystemTree(frame) => frame.encode(out),
            ClientRequest::FilesystemRead(frame) => frame.encode(out),
            ClientRequest::FilesystemWrite(frame) => frame.encode(out),
            ClientRequest::FilesystemFuseServe(frame) => frame.encode(out),
            ClientRequest::Invalid(bytes) => {
                out.extend_from_slice(bytes);
                Ok(())
            }
        }
    }
}

/// Dispatch on the tag, and fall back to
/// [`Invalid`](ClientRequest::Invalid) rather than refusing.
///
/// Each request is handed the WHOLE payload, tag included, because
/// each checks its own tag — this reads the byte to choose a decoder
/// and does not strip it.
impl<'a> Decode<'a> for ClientRequest<'a> {
    /// [`Infallible`]: a payload that cannot be read is a request all
    /// the same.
    type Error = Infallible;

    fn decode(bytes: &'a [u8]) -> Result<Self, Infallible> {
        let Some(tag) = bytes.first() else {
            return Ok(ClientRequest::Invalid(bytes));
        };
        let request = match *tag {
            0 => agents::begin::client::request::Frame::decode(bytes)
                .map(ClientRequest::AgentsBegin)
                .ok(),
            1 => tools::begin::client::request::Frame::decode(bytes)
                .map(ClientRequest::ToolsBegin)
                .ok(),
            2 => fuse::mount::client::request::Frame::decode(bytes)
                .map(ClientRequest::FuseMount)
                .ok(),
            3 => filesystem::tree::client::request::Frame::decode(bytes)
                .map(ClientRequest::FilesystemTree)
                .ok(),
            4 => filesystem::read::client::request::Frame::decode(bytes)
                .map(ClientRequest::FilesystemRead)
                .ok(),
            5 => filesystem::write::client::request::Frame::decode(bytes)
                .map(ClientRequest::FilesystemWrite)
                .ok(),
            6 => filesystem::fuse_serve::client::request::Frame::decode(bytes)
                .map(ClientRequest::FilesystemFuseServe)
                .ok(),
            _ => None,
        };
        Ok(request.unwrap_or(ClientRequest::Invalid(bytes)))
    }
}