Response

The server sends exactly one response, the id or an error; after the id it sends nothing until the finish, and the finish that ends a running container carries no error.

The server sends exactly one response on the scope: the id, or an error. The response finish ends the scope. No frame bearing the scope follows the finish.

[0][id JSON …]        once, when the container is running
[1][error JSON …]     once, when it is not
  • The sequence. Before the id, the server sends no response. The first response is the id or an error. An error is followed by the response finish, and the container is not running and will not be. After the id, the server sends no response until the finish, and the finish carries no error: a run that ended after its id ended by the client’s stop, by the container’s own end, or by the client’s connection ending. A response finish that no response precedes states that the request was not served, as Endpoints provides, or that the run ended before the server had an id to send.
  • The id. A payload whose first byte is 0 carries the id after that byte as JSON, an object with the member id, a string. The server sends it after every FUSE mount is complete and, for an agent container, after the agent is registered.
  • The error. A payload whose first byte is 1 carries an error after that byte, exactly one JSON value in the form defined on the volumes::list response page. This revision prescribes nothing about its content.
  • Malformed. A payload with no byte, a payload whose first byte is neither 0 nor 1, a payload whose bytes after 0 do not parse as the id, and a payload whose bytes after 1 are not one JSON value are malformed.

The response is defined by diverge-provider-sdk/src/endpoints/containers/agents/run/server/response/frame.rs:

//! What a server's response frame carries for an agent container run.

use std::fmt;

use crate::decode::Decode;
use crate::encode::{Encode, Writer};
use crate::shared::containers::response::Id;
use crate::shared::error::Error;

/// A run's answer: the container's id, and then nothing, for as long
/// as the scope lives — or a failure.
///
/// A payload leads with one byte saying which — `0` for
/// [`Id`](Self::Id), `1` for [`Error`](Self::Error) — and the rest is
/// that variant's own JSON.
///
/// # Silence is the good case
///
/// | the scope | means |
/// |-----------|-------|
/// | an id, then nothing, and stays open | the container is running |
/// | an error, then a finish | it never came up, or it is gone |
/// | a finish, with no error | the run is over — a stop, or the container's own end |
///
/// Everything a caller reads from the container — the tree, the
/// family's own exchange — is a channel it opens, not this stream.
/// Which is why this carries no readiness signal either: a provider
/// knows when a CONTAINER has started, and that is not the same fact
/// as the thing inside it having bound its port. The channels find
/// out, one exchange at a time.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// The container's id. Tag `0`.
    ///
    /// Arrives once, whenever the provider has it. See [`Id`].
    Id(Id),
    /// A failure. Tag `1`.
    ///
    /// The container is not running and will not be — the image
    /// would not pull, the container would not start, whatever the
    /// provider knows. It is the one variant that ends the scope
    /// rather than adding to it. See
    /// [`shared::error::Error`](crate::shared::error::Error) for why
    /// it says so little.
    Error(Error),
}

/// Tag for [`Frame::Id`].
const ID: u8 = 0;

/// Tag for [`Frame::Error`].
const ERROR: u8 = 1;

impl Encode for Frame {
    /// One failure per half, and both are JSON's.
    type Error = FrameEncodeError;

    // Spelled out rather than `Self::Error`: this enum has a variant
    // called `Error`, so the associated type is ambiguous by that name.
    fn encode(
        &self,
        out: &mut Writer<'_>,
    ) -> Result<(), FrameEncodeError> {
        match self {
            Frame::Id(id) => {
                out.extend_from_slice(&[ID]);
                serde_json::to_writer(out, id).map_err(FrameEncodeError::Id)
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out).map_err(FrameEncodeError::Error)
            }
        }
    }
}

/// An agent container run response that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
    /// The id did not serialize.
    Id(serde_json::Error),
    /// The error did not serialize.
    Error(serde_json::Error),
}

impl fmt::Display for FrameEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameEncodeError::Id(error) => {
                write!(f, "container id did not serialize: {error}")
            }
            FrameEncodeError::Error(error) => {
                write!(f, "agents run error did not serialize: {error}")
            }
        }
    }
}

impl std::error::Error for FrameEncodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameEncodeError::Id(error) => Some(error),
            FrameEncodeError::Error(error) => Some(error),
        }
    }
}

impl Decode<'_> for Frame {
    /// Four ways to fail, and each names which half failed.
    type Error = FrameError;

    // Spelled out for the same reason as `encode` above.
    fn decode(bytes: &[u8]) -> Result<Self, FrameError> {
        let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
        match *tag {
            ID => serde_json::from_slice(rest)
                .map(Frame::Id)
                .map_err(FrameError::Id),
            ERROR => Error::decode(rest)
                .map(Frame::Error)
                .map_err(FrameError::Error),
            tag => Err(FrameError::UnknownTag(tag)),
        }
    }
}

/// An agent container run response frame that could not be read.
#[derive(Debug)]
pub enum FrameError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is neither of this frame's two.
    UnknownTag(u8),
    /// The id did not parse.
    Id(serde_json::Error),
    /// The error did not parse.
    Error(serde_json::Error),
}

impl fmt::Display for FrameError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameError::Empty => {
                f.write_str("agents run response frame is empty")
            }
            FrameError::UnknownTag(tag) => {
                write!(f, "unknown agents run response frame tag {tag}")
            }
            FrameError::Id(error) => {
                write!(f, "container id did not parse: {error}")
            }
            FrameError::Error(error) => {
                write!(f, "agents run error did not parse: {error}")
            }
        }
    }
}

impl std::error::Error for FrameError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameError::Id(error) => Some(error),
            FrameError::Error(error) => Some(error),
            FrameError::Empty | FrameError::UnknownTag(_) => None,
        }
    }
}

and by diverge-provider-sdk/src/shared/containers/response/id.rs:

//! The container's id.

use serde::{Deserialize, Serialize};

/// What the provider decided to call this container.
///
/// An object rather than a bare string, so a provider that later has
/// something else to say about the container's identity has somewhere
/// to say it. A JSON string is a shape that can only ever be one
/// field.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct Id {
    /// The id itself.
    ///
    /// Opaque, and the provider's to mint. A caller that wants to name
    /// this container anywhere else — a
    /// [`Connect`](crate::shared::containers::request::Connect) — has
    /// this and nothing else to name it with. It is a capability:
    /// holding it is what lets a connector ask, so it has to be
    /// unguessable, and nothing a caller could choose would be.
    ///
    /// Nothing here constrains its shape. It means nothing to anyone
    /// who was not given it, and nothing outside the provider that
    /// minted it.
    pub id: String,
}