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.
| Tag | Endpoint |
|---|---|
0 | agents::begin |
1 | tools::begin |
2 | fuse::mount |
3 | filesystem::tree |
4 | filesystem::read |
5 | filesystem::write |
6 | filesystem::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
clientand the server’s trafficserverunder 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)))
}
}