Request
The payload is the tag byte 0 followed by the request as JSON: the image, the template id, the limits, the mounts and the arguments; no secret rides it.
The payload of the request is the tag byte followed by the container as JSON, read as Notation provides.
[0][container JSON …]
-
image. An object of two fields:{"digest":…,"references":…}.digestis<algorithm>:<hex>, the manifest digest, which identifies the image;referencesis an array of objects ofregistryandname, each a registry host and a repository path under which the bytes may be fetched, absent when empty, as images::check states them. The request names no source beyond them: the server looks in what it holds first, asks the referenced registries its policy lists, and may take the image from the client. -
template. A string: the id of the template the container is made from, as the client names it — an agent template’s, a tool template’s, or a dependency tool template’s. The server carries it verbatim in the begin scope’s request besidearguments, so the container’s program is told which template it runs as, and reads it for nothing else. -
memory,disk. Limits in bytes: the memory the container may use, and the bytes the container may write to its own filesystem. Neither governs a mount. -
volume_mounts. Ordered; the server applies them in order.volume_nameis anamein the client’s listing;volume_relative_pathandcontainer_pathare components. An emptycontainer_pathnames the root, which the server refuses. What becomes of the changes the container makes to the volume is the volume’smode, as the client’s listing reports it:persistent, every change is in the volume when the container ends;ephemeral, the volume is as it was before the run, and the container sees the volume’s content as of its start;read_only, the container cannot change it.modeis the mode the mount means the volume to have, one of the stringspersistent,ephemeralandread_only. A mount does not change the volume’s mode: a volume whose mode differs frommoderefuses the run, as the response states. A request may name one volume at several paths; it is one running container holding the volume, and it names one mode for it, a request naming it under two modes being refused the same way. -
fuse_file_mounts,fuse_directory_mounts. Each names a path and anidthe client chose. Every one is mounted before the id is sent. A directory mount is a tree the client serves: every entry beneath its root is created, written, renamed and removed by the container, each change one ask to the client, and the root alone cannot be removed or renamed. A file mount is one file, overwritten in place and never removed, renamed or replaced. Whether a change is allowed is the client’s answer to the ask; the server enforces nothing. -
Paths. No component of any mount’s path is empty,
.or... No two mounts have one path. No mount lies inside another mount: a volume mount inside a FUSE mount no more than a FUSE mount inside a volume mount. No mount’s path is the root. -
Ids. No two FUSE mounts, on either list, have one
id. -
arguments. Any JSON value. The server relays it verbatim in the begin request it opens on the proxy and does not read it. What the value may be is what the image’s/DIVERGE.mdstates, which the container’s info channel answers. -
No secret. The request carries no secret, and the server holds none. Which secrets the container’s program takes is its own answer at its registration, carried to the client on the registration channel; their values are the client’s answer there and, after every
Inactive, on the secrets channel. -
Malformed. A payload with no byte, a payload whose first byte is not
0, and a payload whose bytes after the tag do not parse as the request are malformed. Endpoints states how a server answers a request it cannot read.
The request is defined by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/client/request/frame.rs:
//! What a client's request frame carries for an agent container run.
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::containers::request::Container;
/// Ask a provider to create an agent container.
///
/// A [`Container`] and nothing else: the image, the limits, the
/// mounts, the arguments. What makes it an agent container rather
/// than the other kind is not in the request — it is the image, and
/// it is what the caller does on the channels once it runs: a message
/// for the agent, and its conversation on the scope's own main stream.
#[derive(Debug, Clone, PartialEq)]
pub struct Frame(
/// What to run.
pub Container,
);
/// This frame's tag among the scope-opening requests.
///
/// One byte at the front of the payload, which is what tells a reader
/// which request it holds. The frame layer does not discriminate them
/// — [`ClientFrame::Request`](crate::wire::frame::client::ClientFrame::Request)
/// is one type carrying bytes — so the distinction has to be in the
/// bytes, and each request owns the value that names it.
///
/// See the table in [`endpoints`](crate::provider::endpoints) for the whole
/// allocation. The values are chosen across modules that do not know
/// about each other, so the table is the only place they can be seen
/// at once.
const TAG: u8 = 0;
/// JSON, matching [`images::check`](crate::provider::endpoints::images::check)
/// rather than the postcard [`volumes`](crate::provider::endpoints::volumes)
/// uses. One of these is sent per container rather than per
/// filesystem event, so there is no throughput to optimize for — and
/// it names an image the same way a check does, which is reason
/// enough for the two to look alike on the wire. The arguments being
/// a [`Value`](serde_json::Value) settles it besides: a value cannot
/// come back out of postcard at all.
impl Encode for Frame {
/// The ordinary JSON failure. The tag cannot fail.
type Error = serde_json::Error;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Self::Error> {
out.extend_from_slice(&[TAG]);
serde_json::to_writer(out, &self.0)
}
}
impl Decode<'_> for Frame {
/// Three ways to fail, and only one of them is JSON.
type Error = FrameError;
fn decode(bytes: &[u8]) -> Result<Self, Self::Error> {
let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
if *tag != TAG {
return Err(FrameError::UnexpectedTag(*tag));
}
serde_json::from_slice(rest).map(Frame).map_err(FrameError::Body)
}
}
/// An agent container run request that could not be read.
#[derive(Debug)]
pub enum FrameError {
/// No bytes at all, so not even a tag.
Empty,
/// A tag naming some other request.
///
/// A reader that dispatched on the tag will not see this. One that
/// assumed which request it held, and was wrong, will — which is
/// the point of checking a tag rather than skipping it.
UnexpectedTag(u8),
/// The request did not parse.
Body(serde_json::Error),
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FrameError::Empty => {
f.write_str("agents run request frame is empty")
}
FrameError::UnexpectedTag(tag) => {
write!(f, "expected agents run request tag {TAG}, found {tag}")
}
FrameError::Body(error) => {
write!(f, "agents run request did not parse: {error}")
}
}
}
}
impl std::error::Error for FrameError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameError::Body(error) => Some(error),
FrameError::Empty | FrameError::UnexpectedTag(_) => None,
}
}
}
The container is defined by diverge-sdk-rs/src/shared/containers/request/container.rs:
//! Asking for a container.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{FuseMount, Image, VolumeMount};
/// Ask a provider to create a container.
///
/// Everything here is what a caller may CHOOSE, and it is the same for
/// every kind of container — the arguments included, which either
/// kind is handed once: what differs between an agent and a tool
/// server is what a caller says into it once it runs, on a channel,
/// not here. No secret is here either: which secrets a container
/// takes is its program's answer at registration, and their values
/// are fed on a channel at every activation — see
/// [`secrets`](crate::shared::containers::secrets) — so a request
/// carries none, and a provider never holds one.
/// What a caller may not choose is not here at all rather than here
/// and ignored — the container's name, its published ports, its
/// entrypoint and its environment are the provider's, because they
/// are how the provider reaches the container and how the container
/// reaches back. There is no environment: the mounts are the caller's
/// only provisioning channel, and a field that is accepted and
/// ignored is a field callers will believe in.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Container {
/// The image: a digest and its references. See [`Image`].
pub image: Image,
/// The template the container is made from, by id: the caller's
/// word for what this container is an instance of — an agent
/// template's id, a tool template's, or a dependency tool
/// template's, each the lowercase hexadecimal SHA-256 of the
/// template's canonical bytes. The provider hands it to the
/// container beside the arguments, at its registration, and reads
/// nothing of it: it is the program's to know which template it
/// runs as.
pub template: String,
/// How much memory the container may have, in BYTES.
///
/// A ceiling, not a hint. A process that exceeds what the
/// container is allowed is killed by the kernel rather than told —
/// no failed allocation to catch, no warning first — and the
/// container will not see this number in its own
/// `/proc/meminfo`, which reports the host's. An image that sizes
/// itself off what it thinks it has will size itself wrong.
///
/// Bytes rather than megabytes because a unit that has to be
/// spelled out in prose is a unit half of everyone gets wrong.
pub memory: u64,
/// How much the container may WRITE, in BYTES.
///
/// Its own filesystem only — what it adds to or changes over the
/// image it came from. The image's layers are read-only and are
/// not counted, so a container starts at nothing however large the
/// image is.
///
/// # It does not govern the mounts
///
/// A volume is storage that already existed, with a size of its
/// own that a [`volume`](crate::provider::endpoints::volumes) stated when it
/// was made. A number here that silently applied to it would be
/// this request deciding how much of somebody else's storage a
/// container may fill.
///
/// Bytes rather than megabytes, for the reason
/// [`memory`](Self::memory) gives.
pub disk: u64,
/// Volumes the provider offers, made visible inside the container.
///
/// Ordered, and a provider applies them in order. See
/// [`VolumeMount`] for how one is named without a host path. No
/// mount's path, in any list, is a prefix of another's: mounting
/// INTO a directory the image owns is the point, and mounts
/// stacking on each other is not.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub volume_mounts: Vec<VolumeMount>,
/// Files the caller serves LIVE, mounted one each over FUSE.
///
/// Each names a file by a path, an id of the caller's, and
/// whether the container may write it — see [`FuseMount`]. The
/// provider MUST mount every one before the container starts, and
/// every open and every changed close inside the container is one
/// ask back to the caller, by that id. The file is overwritten in
/// place only; a program that replaces its file by rename needs a
/// directory mount. Its path is no other mount's and lies inside
/// none, as every mount's.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fuse_file_mounts: Vec<FuseMount>,
/// Directories the caller serves LIVE, mounted one each over FUSE.
///
/// Each names a directory by a path, an id of the caller's, and
/// whether the container may change it — see [`FuseMount`]. The
/// whole tree under the path is the caller's: every listing,
/// read, write, creation, removal and rename inside the container
/// is one ask back to the caller, by that id. No other mount may
/// lie inside it.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fuse_directory_mounts: Vec<FuseMount>,
/// What the image is told once, as the image defines it, for the
/// container's life.
///
/// A JSON value, because this crate does not know what an image
/// takes — an agent's model and tools, a tool server's own knobs
/// — and a wire that typed it would have to be revised for every
/// image that ever ran. The provider hands it to the container
/// and does not read it; what the value MAY be is what the image's
/// [`info`](crate::shared::containers::info) answers — its
/// DIVERGE.md — for either kind of container.
pub arguments: Value,
}
and by diverge-sdk-rs/src/shared/containers/request/image.rs:
//! What image a container is made from: a digest, and where its
//! bytes may be fetched.
use serde::{Deserialize, Serialize};
use super::Reference;
/// The image, by its manifest digest and the references under which
/// its bytes may be fetched — what
/// [`images::check`](crate::provider::endpoints::images::check::client::request::Frame)
/// asks about, so a check that came back available names an image a
/// run can ask for, with nothing to translate between them.
///
/// # The digest is the image
///
/// A digest cannot be repointed at different content, so an image
/// means the same bytes every time, wherever they come from. That is
/// the whole of what the wire asserts: the provider runs the image
/// the digest names and no other. The references are not the image:
/// the same bytes sit under different names on different registries,
/// and an id that hashes an image hashes it with none — see
/// [`hashed`](Self::hashed).
///
/// # Who supplies the bytes is the provider's
///
/// A provider looks in what it holds first, by digest alone — among
/// which are the images the caller
/// [built](crate::provider::endpoints::images::build) on it, and no
/// other caller's. Failing that it may ask any referenced registry its own policy lists, with
/// the credential it holds for it, and ignores a reference naming one
/// it does not; and it may take the image from the caller — asking on
/// the run scope whether the caller holds it and, when it does, for
/// its manifest and blobs, see [`oci`](crate::shared::containers::oci).
/// Which of those, in what order, is the provider's policy, and a
/// caller learns only whether the run happened. A caller need not
/// hold what it names; one that does answers so when asked.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct Image {
/// The manifest digest, `<algorithm>:<hex>`.
///
/// What identifies the image: a runtime hashes what it pulls, so
/// a source serving other bytes under it fails before anything
/// runs.
pub digest: String,
/// Where the bytes may be fetched, each a registry and a path
/// there: see [`Reference`]. Absent when there are none, and then
/// the provider has only what it holds and what the caller holds.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub references: Vec<Reference>,
}
impl Image {
/// The image as an id hashes it: the digest with no references,
/// so that one image under any names hashes the same.
pub fn hashed(&self) -> Image {
Image {
digest: self.digest.clone(),
references: Vec::new(),
}
}
}
and by diverge-sdk-rs/src/shared/containers/request/volume_mount.rs:
//! One volume made visible inside a container.
use serde::{Deserialize, Serialize};
use crate::provider::endpoints::volumes::Mode;
/// A volume from a listing, mounted into the container.
///
/// Host side first, then the container side — source before
/// destination, the order a mount reads in everywhere else.
///
/// # Why the host side is a name and an offset
///
/// Because a host path is not something a caller is allowed to state.
/// [`volume_name`](Self::volume_name) is a
/// [`Volume::name`](crate::provider::endpoints::volumes::list::server::response::Volume::name)
/// the provider published, and
/// [`volume_relative_path`](Self::volume_relative_path) descends from
/// wherever that maps to — so a caller reaches a subdirectory of
/// something it was offered, and nothing else.
///
/// That is the same access model as every
/// [`volumes`](crate::provider::endpoints::volumes) endpoint, and it holds for
/// the same reason: a provider never validates a path, it resolves a name it
/// chose and then descends. A caller cannot escape upward, because
/// there is no component it can write that means "up" — the offset is
/// components, and `..` is a name, not an instruction.
///
/// # The mode is the volume's, and the mount states it
///
/// Whether what a container writes into the volume is in the volume
/// when the container ends, discarded with the container, or refused
/// inside it is the volume's [`Mode`], as its
/// [listing](crate::provider::endpoints::volumes::list::server::response::Volume::mode)
/// reports it and as a [`create`](crate::provider::endpoints::volumes::create)
/// stated or an [`edit`](crate::provider::endpoints::volumes::edit) changed it
/// — a fact of the volume, the same for every container that mounts
/// it. A mount does not change it: it STATES the mode it means the
/// volume to have, [`mode`](Self::mode), and a volume in another mode
/// refuses the whole run,
/// [`VolumeMode`](crate::shared::containers::response::VolumeMode),
/// before anything is fetched or deployed, so that a container never
/// runs on a volume behaving otherwise than its caller meant. A
/// request naming one volume at several paths names one mode for it.
/// How many may mount a volume is the volume's too, as far as the
/// provider's [`info`](crate::provider::endpoints::info) capability
/// for the mode goes: up to any number of the caller's containers at
/// once, whatever the mode, each of which may name it at several
/// paths — a persistent volume being one filesystem they share, an
/// ephemeral one a volume each starts from afresh. A second holder
/// the provider does not allow is
/// refused with
/// [`VolumeMounted`](crate::shared::containers::response::VolumeMounted)
/// or
/// [`VolumeFuseServed`](crate::shared::containers::response::VolumeFuseServed),
/// a mode the provider does not mount with
/// [`VolumeMountUnsupported`](crate::shared::containers::response::VolumeMountUnsupported).
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct VolumeMount {
/// Which offered volume, by the name a listing gave it.
///
/// Names come from
/// [`Volume::name`](crate::provider::endpoints::volumes::list::server::response::Volume::name)
/// and mean nothing outside the provider that published them.
pub volume_name: String,
/// How far into that volume to start, as path components
/// relative to it.
///
/// Empty mounts the volume itself, which is the common case;
/// anything else mounts a subdirectory of it.
pub volume_relative_path: Vec<String>,
/// The mode the mount means the volume to have: `persistent`,
/// `ephemeral` or `read_only`. A volume in another mode refuses
/// the run; nothing changes a volume's mode but an edit.
pub mode: Mode,
/// Where it appears inside the container, as path components from
/// the container's root.
///
/// Empty means the root itself, which a provider will almost
/// certainly refuse — the image's own filesystem is there.
pub container_path: Vec<String>,
}
and by diverge-sdk-rs/src/shared/containers/request/fuse_mount.rs:
//! One file, or one directory, the caller serves live into the
//! container.
use serde::{Deserialize, Serialize};
/// A file or a directory the caller keeps, mounted into the container
/// over FUSE: where, and under what id. Which of the two it is, is
/// which list of the [`Container`](super::Container) it is on.
///
/// The provider MUST mount every one of the request before it
/// answers the run's id — one `fuse::mount` scope on the proxy for
/// each, see
/// [`fuse::mount`](crate::container_proxy::outside::endpoints::fuse::mount), each
/// complete before the next and before any filetree is opened — and
/// one more whenever the caller asks for it on the run's `fuse_mount`
/// channel while the container runs, as
/// [`fuse_mount`](crate::shared::containers::fuse_mount) states, each
/// as a filesystem the caller serves: the mount point is the file or the
/// directory itself, made if absent with every missing parent
/// directory made too, and the directory around it stays whatever
/// the image or another mount made it. Nothing in the container or
/// the provider copies the contents in or reads them back: every
/// read, write, listing, removal, rename and new directory is one
/// exchange in [`fuse`](super::super::fuse), carrying the id, and the
/// caller serves it from wherever it keeps the thing. It is for the
/// credential files vendor CLIs rewrite when they refresh a login.
///
/// A FILE mount is one regular file that can be read and overwritten
/// in place — opened, truncated, written, closed — but never deleted
/// or moved, and
/// never replaced by a rename: the mount point is the file itself,
/// and the kernel refuses to unlink or rename a mount point, so a
/// program that saves by writing a temporary beside the file and
/// renaming it over the file fails at the rename. A DIRECTORY mount
/// is for that program: a whole tree the caller serves, whose every
/// entry can be created, overwritten by either method, renamed and
/// deleted, and whose root alone is fixed.
///
/// Nothing here says a mount may not change. A caller that wants one
/// unchangeable refuses the mutation asks it receives — a `write`, a
/// `remove`, a `rename`, a `mkdir` — one at a time, with the error of
/// its answer, and the program in the container sees that operation
/// fail. The provider enforces nothing on the caller's behalf.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FuseMount {
/// Where the mount appears inside the container, as path
/// components from the container's root — the shape every path in
/// this crate takes, as
/// [`container_path`](super::VolumeMount::container_path) does
/// for a volume.
///
/// No component is empty, `.` or `..`. The path is not the root,
/// is no other mount's, and lies inside no other mount's — a
/// volume's or a FUSE one's — and no other mount's lies inside
/// it.
pub container_path: Vec<String>,
/// The caller's own id for the mount: opaque, minted by the caller
/// when it named the mount, and echoed back on every ask the
/// provider sends for it. Two mounts on one request, on either
/// list, may not share an id.
pub id: String,
}