Request
The payload of the channel request the server sends: the tag byte 3 and the ask, the run’s id, the dependency tool templates and the declaration of secrets as JSON.
[3][request JSON …]
id. A string: the id of the run the container is registered for, as the server minted it and as the response carries it after the channel has ended and the values have been fed. The server has entered the container among those it runs when it sends this, so a containers::fuse_serve naming the id is answered while the channel is open.dependencies. An array of dependency tool templates, in the order the program declared them; absent when empty. Each is an object of five members, the members of its mode, and two that may be absent:image, an object ofdigestandreferencesas the request of a tools run names one;memoryanddisk, limits in bytes as that request states them;arguments, any JSON value, theargumentsof that request;secrets, an array of strings, the names of the secrets the tool’s image needs, each a name the agent’s template declares, whose values the client supplies when it answers the tool’s own registration — absent when empty;mode, the stringtransientorservice, which mode the dependency is in, with that mode’s own members flat beside it; andpermissions, an array of grants in the formsshared::permissiondefines — each one object with one member named for a kind, whose value is a bare array of making actions, an object ofactionsandwithin, or an object ofactions,withinandtags, wherewithinis the stringanyor an object ofall_tagsandany_tagsand names nothing, and, over agents alone, may be the stringself, the agents the dependency is a dependency of — absent when empty. The volumes of a tools run are not here: the client chooses them. The server relays every member and reads none.secrets. A JSON object whose every member is a secret’s name, as the container’s template declares it, to the mode the program takes it in, the stringfixedorrotating, in the order the program declared them; absent when empty. Afixedsecret is read: the client hands the container one value at every activation and takes nothing back. Arotatingsecret is held: the client locks one value to the container at every activation, and the container hands it back, as the program leaves it, on theInactivethat ends the loop. A name the template does not declare is the registration’s error, in the client’s words. A name the template declares and the declaration omits is a secret the program does not take, and is never fed. The server relays the declaration as the proxy sent it and reads none of it.- A transient tool’s members.
fuse_file_mountsandfuse_directory_mounts, arrays of mounts, each an object ofagent_pathandtool_path, both arrays of path components, a file or a directory of the agent container the client serves live at a path of the tool container, each absent when empty. A transient tool’s database scope is the agent’s it is deployed for, and the template says so by its mode alone. - A service’s members.
scope, the stringinstance,templateorglobal: how far the service is shared, and its database scope with it — one service and one scope per agent that declares it, one per agent template whose agents declare it, or one for every agent that declares it, from whatever template. A service names no mount. - The id of a template. A template has no name. The client names
a dependency by the template’s id: the lowercase hexadecimal
SHA-256 of the template as compact JSON with absent members
omitted, the image’s
referencesabsent too, the keys of every object sorted bytewise at every depth, theargumentsand every object inside them included, and thesecretsincluded with their names sorted bytewise. One image under different names is one template. Two templates with one id are one dependency, and an array that holds one id twice is refused by the client as its error. The container calls the tool’s tools by whatever names the client’s merged list shows. - Malformed. A channel request whose payload does not decode as stated is answered by a channel response finish that no channel response precedes, and the server does nothing else for it.
The channel request is defined by diverge-sdk-rs/src/provider/endpoints/containers/agents/run/server/channel_request/frame.rs:
//! What a server's channel request frame carries for an agent container
//! run.
use std::fmt;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::containers::{fuse, oci, postgres, registration, write_bytes};
use crate::shared::mcp;
/// What a provider asks a caller for while an agent container runs.
///
/// A payload leads with one byte saying which, and the rest is that
/// variant's own bytes.
///
/// | tag | asks for |
/// |-----|----------|
/// | `0` | [`OciManifest`](Self::OciManifest) |
/// | `1` | [`OciBlob`](Self::OciBlob) |
/// | `2` | [`OciHas`](Self::OciHas) |
/// | `3` | [`Registration`](Self::Registration) |
/// | `4` | [`Write`](Self::Write) |
/// | `5` | [`Postgres`](Self::Postgres) |
/// | `6` | [`McpListTools`](Self::McpListTools) |
/// | `7` | [`McpListResources`](Self::McpListResources) |
/// | `8` | [`McpCallTool`](Self::McpCallTool) |
/// | `9` | [`McpReadResource`](Self::McpReadResource) |
/// | `10` | [`McpNotifications`](Self::McpNotifications) |
/// | `11` | [`FuseRead`](Self::FuseRead) |
/// | `12` | [`FuseWrite`](Self::FuseWrite) |
/// | `13` | [`FuseList`](Self::FuseList) |
/// | `14` | [`FuseRemove`](Self::FuseRemove) |
/// | `15` | [`FuseRename`](Self::FuseRename) |
/// | `16` | [`FuseMkdir`](Self::FuseMkdir) |
/// | `17` | [`FuseStat`](Self::FuseStat) |
/// | `18` | [`FuseTruncate`](Self::FuseTruncate) |
/// | `19` | [`FuseSetattr`](Self::FuseSetattr) |
///
/// Twenty. The first five are the provider's own asks — whether the
/// caller holds an image, its manifest and blobs, the container's
/// registration, a write's content — and the rest are the
/// CONTAINER's, relayed: its database connections, its tool calls
/// outward to the caller's MCP servers, and the files the caller
/// mounted live. No daemon connection: an agent
/// container holds no account and asks the daemon nothing, and only
/// a tool container asks the caller for one.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame<'a> {
/// A manifest of an image the caller holds, by digest. Tag `0`.
///
/// Opened when the provider takes the image from the caller, and
/// opened by the container RUNTIME's appetite rather
/// than the provider's: the provider's registry serves the pull,
/// and a manifest its store does not hold becomes one of these.
/// See [`oci`](crate::shared::containers::oci).
OciManifest(oci::manifest::request::Request),
/// A blob of an image the caller holds, by digest. Tag `1`.
///
/// The other half of a pull; see
/// [`oci`](crate::shared::containers::oci).
OciBlob(oci::blob::request::Request),
/// Whether the caller holds an image, by digest, and under what
/// name. Tag `2`.
///
/// Opened by a provider that would take the image from the caller,
/// before it asks for anything of it; see
/// [`oci`](crate::shared::containers::oci).
OciHas(oci::has::request::Request),
/// Register the container: deploy the dependencies it declared,
/// and resolve the secrets it takes. Tag `3`.
///
/// Opened once, after the proxy's `Begun` carried a non-empty
/// list or a non-empty declaration, and before the id; answered
/// with the secrets' first values, which the provider feeds to
/// the container before it sends the id. See
/// [`registration`](crate::shared::containers::registration).
Registration(registration::request::Request<'a>),
/// Send the content for a write. Tag `4`.
///
/// Opened in answer to a write the caller started. A write cannot
/// carry its own content — only a responder can finish a channel —
/// so the bytes travel as responses on this one. See
/// [`write_bytes`](crate::shared::containers::write_bytes).
Write(write_bytes::request::Request),
/// The provider's half of a database connection the container
/// opened. Tag `5`.
///
/// What comes back is everything the database says; the caller
/// opens the other half, or declines. See
/// [`postgres`](crate::shared::containers::postgres).
Postgres(postgres::request::Postgres),
/// What tools the caller's servers have. Tag `6`.
///
/// The container asking OUTWARD: an agent's tool calls go to
/// servers that live with the caller, so the five exchanges in
/// [`shared::mcp`](crate::shared::mcp) travel this direction too.
McpListTools(mcp::list_tools::request::Request),
/// What resources they have. Tag `7`.
McpListResources(mcp::list_resources::request::Request),
/// Run one of their tools. Tag `8`.
McpCallTool(mcp::call_tool::request::Request),
/// Read one of their resources. Tag `9`.
McpReadResource(mcp::read_resource::request::Request),
/// Everything they say on their own account. Tag `10`.
McpNotifications(mcp::notifications::request::Request),
/// Read a piece of a file the caller mounted live, by the mount's
/// id, the file's path in it, an offset and a length. Tag `11`.
///
/// The container's proxy asking on behalf of a FUSE mount: every
/// `read(2)` of the file. See
/// [`fuse`](crate::shared::containers::fuse).
FuseRead(fuse::read::request::Request<'a>),
/// Write a piece of a file the caller mounted live, in place at
/// an offset. Tag `12`.
///
/// Every `write(2)` of the file.
FuseWrite(fuse::write::request::Request<'a>),
/// List a directory of a tree the caller mounted live. Tag `13`.
///
/// Every listing in a directory mount: names and kinds.
FuseList(fuse::list::request::Request<'a>),
/// Remove a file or an empty directory of such a tree. Tag `14`.
FuseRemove(fuse::remove::request::Request<'a>),
/// Rename an entry within such a tree. Tag `15`.
FuseRename(fuse::rename::request::Request<'a>),
/// Make a directory in such a tree. Tag `16`.
FuseMkdir(fuse::mkdir::request::Request<'a>),
/// What an entry the caller mounted live is: kind, size, mode,
/// owner, group and times. Tag `17`.
///
/// Every attribute of a file mount, and every lookup and
/// attribute of an entry in a directory mount: a `stat` costs
/// fifty-seven bytes back, not the file.
FuseStat(fuse::stat::request::Request<'a>),
/// Set a file the caller mounted live to a length. Tag `18`.
///
/// Every `truncate(2)`, `ftruncate(2)` and `O_TRUNC` open.
FuseTruncate(fuse::truncate::request::Request<'a>),
/// Set some attributes of an entry the caller mounted live. Tag
/// `19`.
///
/// Every `chmod(2)`, `chown(2)` and `utimensat(2)`.
FuseSetattr(fuse::setattr::request::Request<'a>),
}
/// Tag for [`Frame::OciManifest`].
const OCI_MANIFEST: u8 = 0;
/// Tag for [`Frame::OciBlob`].
const OCI_BLOB: u8 = 1;
/// Tag for [`Frame::OciHas`].
const OCI_HAS: u8 = 2;
/// Tag for [`Frame::Registration`].
const REGISTRATION: u8 = 3;
/// Tag for [`Frame::Write`].
const WRITE: u8 = 4;
/// Tag for [`Frame::Postgres`].
const POSTGRES: u8 = 5;
/// Tag for [`Frame::McpListTools`].
const MCP_LIST_TOOLS: u8 = 6;
/// Tag for [`Frame::McpListResources`].
const MCP_LIST_RESOURCES: u8 = 7;
/// Tag for [`Frame::McpCallTool`].
const MCP_CALL_TOOL: u8 = 8;
/// Tag for [`Frame::McpReadResource`].
const MCP_READ_RESOURCE: u8 = 9;
/// Tag for [`Frame::McpNotifications`].
const MCP_NOTIFICATIONS: u8 = 10;
/// Tag for [`Frame::FuseRead`].
const FUSE_READ: u8 = 11;
/// Tag for [`Frame::FuseWrite`].
const FUSE_WRITE: u8 = 12;
/// Tag for [`Frame::FuseList`].
const FUSE_LIST: u8 = 13;
/// Tag for [`Frame::FuseRemove`].
const FUSE_REMOVE: u8 = 14;
/// Tag for [`Frame::FuseRename`].
const FUSE_RENAME: u8 = 15;
/// Tag for [`Frame::FuseMkdir`].
const FUSE_MKDIR: u8 = 16;
/// Tag for [`Frame::FuseStat`].
const FUSE_STAT: u8 = 17;
/// Tag for [`Frame::FuseTruncate`].
const FUSE_TRUNCATE: u8 = 18;
/// Tag for [`Frame::FuseSetattr`].
const FUSE_SETATTR: u8 = 19;
impl Encode for Frame<'_> {
/// The JSON failure from the asks that are JSON, or a fuse id or
/// path too long for its prefix; everything else is bytes copied.
type Error = FrameEncodeError;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), FrameEncodeError> {
match self {
Frame::OciManifest(request) => {
out.extend_from_slice(&[OCI_MANIFEST]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::OciBlob(request) => {
out.extend_from_slice(&[OCI_BLOB]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::OciHas(request) => {
out.extend_from_slice(&[OCI_HAS]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::Registration(request) => {
out.extend_from_slice(&[REGISTRATION]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::Write(request) => {
out.extend_from_slice(&[WRITE]);
// Its error is `Infallible`, and an empty match on one
// is how you say so: there is no value to handle.
request.encode(out).map_err(|error| match error {})
}
Frame::Postgres(request) => {
out.extend_from_slice(&[POSTGRES]);
request.encode(out).map_err(|error| match error {})
}
Frame::McpListTools(request) => {
out.extend_from_slice(&[MCP_LIST_TOOLS]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::McpListResources(request) => {
out.extend_from_slice(&[MCP_LIST_RESOURCES]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::McpCallTool(request) => {
out.extend_from_slice(&[MCP_CALL_TOOL]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::McpReadResource(request) => {
out.extend_from_slice(&[MCP_READ_RESOURCE]);
request.encode(out).map_err(FrameEncodeError::Json)
}
Frame::McpNotifications(request) => {
out.extend_from_slice(&[MCP_NOTIFICATIONS]);
request.encode(out).map_err(|error| match error {})
}
Frame::FuseRead(request) => {
out.extend_from_slice(&[FUSE_READ]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseWrite(request) => {
out.extend_from_slice(&[FUSE_WRITE]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseList(request) => {
out.extend_from_slice(&[FUSE_LIST]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseRemove(request) => {
out.extend_from_slice(&[FUSE_REMOVE]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseRename(request) => {
out.extend_from_slice(&[FUSE_RENAME]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseMkdir(request) => {
out.extend_from_slice(&[FUSE_MKDIR]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseStat(request) => {
out.extend_from_slice(&[FUSE_STAT]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseTruncate(request) => {
out.extend_from_slice(&[FUSE_TRUNCATE]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
Frame::FuseSetattr(request) => {
out.extend_from_slice(&[FUSE_SETATTR]);
request.encode(out).map_err(FrameEncodeError::Fuse)
}
}
}
}
/// An agent container run channel request that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
/// A JSON ask did not serialize.
Json(serde_json::Error),
/// A fuse ask would not encode: an id or a path too long for its
/// prefix.
Fuse(fuse::RequestEncodeError),
}
impl fmt::Display for FrameEncodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameEncodeError::Json(error) => {
write!(f, "channel request did not serialize: {error}")
}
FrameEncodeError::Fuse(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for FrameEncodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameEncodeError::Json(error) => Some(error),
FrameEncodeError::Fuse(error) => Some(error),
}
}
}
impl<'a> Decode<'a> for Frame<'a> {
/// Several ways to fail, and each names which ask it was.
type Error = FrameError;
fn decode(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let (tag, rest) = bytes.split_first().ok_or(FrameError::Empty)?;
match *tag {
OCI_MANIFEST => oci::manifest::request::Request::decode(rest)
.map(Frame::OciManifest)
.map_err(FrameError::Oci),
OCI_BLOB => oci::blob::request::Request::decode(rest)
.map(Frame::OciBlob)
.map_err(FrameError::Oci),
OCI_HAS => oci::has::request::Request::decode(rest)
.map(Frame::OciHas)
.map_err(FrameError::Oci),
REGISTRATION => registration::request::Request::decode(rest)
.map(Frame::Registration)
.map_err(FrameError::Registration),
WRITE => write_bytes::request::Request::decode(rest)
.map(Frame::Write)
.map_err(FrameError::Write),
POSTGRES => postgres::request::Postgres::decode(rest)
.map(Frame::Postgres)
.map_err(FrameError::Postgres),
MCP_LIST_TOOLS => mcp::list_tools::request::Request::decode(rest)
.map(Frame::McpListTools)
.map_err(FrameError::McpParams),
MCP_LIST_RESOURCES => {
mcp::list_resources::request::Request::decode(rest)
.map(Frame::McpListResources)
.map_err(FrameError::McpParams)
}
MCP_CALL_TOOL => mcp::call_tool::request::Request::decode(rest)
.map(Frame::McpCallTool)
.map_err(FrameError::McpParams),
MCP_READ_RESOURCE => {
mcp::read_resource::request::Request::decode(rest)
.map(Frame::McpReadResource)
.map_err(FrameError::McpParams)
}
MCP_NOTIFICATIONS => Ok(Frame::McpNotifications(
mcp::notifications::request::Request::decode(rest)
.unwrap_or_else(|error| match error {}),
)),
FUSE_READ => fuse::read::request::Request::decode(rest)
.map(Frame::FuseRead)
.map_err(FrameError::Fuse),
FUSE_WRITE => fuse::write::request::Request::decode(rest)
.map(Frame::FuseWrite)
.map_err(FrameError::Fuse),
FUSE_LIST => fuse::list::request::Request::decode(rest)
.map(Frame::FuseList)
.map_err(FrameError::Fuse),
FUSE_REMOVE => fuse::remove::request::Request::decode(rest)
.map(Frame::FuseRemove)
.map_err(FrameError::Fuse),
FUSE_RENAME => fuse::rename::request::Request::decode(rest)
.map(Frame::FuseRename)
.map_err(FrameError::Fuse),
FUSE_MKDIR => fuse::mkdir::request::Request::decode(rest)
.map(Frame::FuseMkdir)
.map_err(FrameError::Fuse),
FUSE_STAT => fuse::stat::request::Request::decode(rest)
.map(Frame::FuseStat)
.map_err(FrameError::Fuse),
FUSE_TRUNCATE => fuse::truncate::request::Request::decode(rest)
.map(Frame::FuseTruncate)
.map_err(FrameError::Fuse),
FUSE_SETATTR => fuse::setattr::request::Request::decode(rest)
.map(Frame::FuseSetattr)
.map_err(FrameError::Fuse),
tag => Err(FrameError::UnknownTag(tag)),
}
}
}
/// An agent container run channel request that could not be read.
#[derive(Debug)]
pub enum FrameError {
/// No bytes at all, so not even a tag.
Empty,
/// A tag that is none of this frame's twenty.
UnknownTag(u8),
/// An image ask did not parse.
Oci(serde_json::Error),
/// The registration did not parse.
Registration(serde_json::Error),
/// The write content request did not decode.
Write(write_bytes::request::RequestError),
/// The connection id was not four bytes.
Postgres(postgres::request::PostgresError),
/// A fuse ask did not decode.
Fuse(fuse::RequestError),
/// One of the five MCP exchanges' params did not parse.
///
/// One variant for five tags, because they fail the same way and
/// the tag already said which was meant.
McpParams(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 channel request frame is empty")
}
FrameError::UnknownTag(tag) => {
write!(f, "unknown agents run channel request tag {tag}")
}
FrameError::Oci(error) => {
write!(f, "image ask did not parse: {error}")
}
FrameError::Registration(error) => {
write!(f, "registration did not parse: {error}")
}
FrameError::Write(error) => {
write!(f, "write content request did not decode: {error}")
}
FrameError::Postgres(error) => write!(f, "{error}"),
FrameError::Fuse(error) => write!(f, "{error}"),
FrameError::McpParams(error) => {
write!(f, "mcp request params did not parse: {error}")
}
}
}
}
impl std::error::Error for FrameError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameError::Oci(error)
| FrameError::Registration(error)
| FrameError::McpParams(error) => Some(error),
FrameError::Write(error) => Some(error),
FrameError::Postgres(error) => Some(error),
FrameError::Fuse(error) => Some(error),
FrameError::Empty | FrameError::UnknownTag(_) => None,
}
}
}
and by diverge-sdk-rs/src/shared/containers/registration/request/request.rs:
//! What a container declared at registration, as the provider relays
//! it.
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use serde_json::Error;
use super::super::super::dependencies::Template;
use super::super::super::secrets::Declaration;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
/// The dependencies and the secrets' declaration, as the container's
/// proxy answered `Begun` with them, and the run's id.
///
/// Borrowed where the provider encodes it — both are the `Begun`'s
/// for the run's life, see the agents begin's
/// [`Registered`](crate::container_proxy::outside::endpoints::agents::begin::server::response::Registered)
/// and the tools begin's
/// [`Registered`](crate::container_proxy::outside::endpoints::tools::begin::server::response::Registered)
/// — and owned where the caller decodes it.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Request<'a> {
/// The run's id: the one the `Id` response carries after the
/// registration, told here first so that the caller can serve the
/// container's own paths into the dependencies — a
/// `containers::fuse_serve` names a container by it — before the
/// id is answered. The provider has entered the container among
/// those it runs by the time it asks.
pub id: Cow<'a, str>,
/// The dependencies, in the order the program declared them.
/// Absent when empty, which a tool container's always is.
#[serde(default, skip_serializing_if = "no_dependencies")]
pub dependencies: Cow<'a, [Template]>,
/// The secrets the program takes, each by name to its mode, in the
/// order the program declared them: see
/// [`Declaration`]. Absent when empty.
#[serde(default, skip_serializing_if = "no_secrets")]
pub secrets: Cow<'a, Declaration>,
}
/// Whether the list is empty, for serde.
fn no_dependencies(dependencies: &Cow<'_, [Template]>) -> bool {
dependencies.is_empty()
}
/// Whether the declaration is empty, for serde.
fn no_secrets(secrets: &Cow<'_, Declaration>) -> bool {
secrets.is_empty()
}
/// Its JSON, and nothing in front of it. The tag that says which
/// request this is belongs to whichever frame carries it.
impl Encode for Request<'_> {
/// The ordinary JSON failure.
type Error = Error;
fn encode(&self, out: &mut Writer<'_>) -> Result<(), Error> {
serde_json::to_writer(out, self)
}
}
impl Decode<'_> for Request<'_> {
/// The ordinary JSON failure.
type Error = Error;
fn decode(bytes: &[u8]) -> Result<Self, Error> {
serde_json::from_slice(bytes)
}
}
and by diverge-sdk-rs/src/shared/containers/dependencies/template.rs:
//! One dependency, as an image declares it.
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::Mode;
use super::super::request::Image;
use crate::shared::permission::Grant;
/// One tool container an agent's program depends on: everything the
/// caller needs to deploy it, as fields and not as words. A tool
/// container depends on nothing.
///
/// A [`Container`](crate::shared::containers::request::Container) is an
/// image, two limits, mounts and arguments. The image, the limits and
/// the arguments are here, member for member; beside them the secrets
/// the image needs, by name alone — which of them the tool's program
/// takes, and in what mode, is its own answer at registration, and
/// their values are the caller's, each resolved through the vault key
/// the agent that spawns the tool binds the same name to, fed at every
/// activation. The mounts are not the caller's volumes — a volume is
/// named in the caller's own listing, and a tool that named one would
/// be dictating the caller's storage — but, for a transient tool alone,
/// the AGENT's own files and directories, each a path in the agent
/// container served live at a path in the tool container: the caller
/// serves the agent container, a `containers::fuse_serve` of the path,
/// and mounts the tool, a `fuse::mount` answered from it.
/// [`mode`](Self::mode) says which of the two modes the dependency is —
/// a transient tool of the agent's run, with those mounts, or a service
/// the caller keeps, with a scope — see [`Mode`]; and
/// [`permissions`](Self::permissions) what its account may do — grants
/// that name nothing, so the template is the same on every daemon it
/// reaches, and among them the word `self`, which reaches the agent a
/// transient tool is the dependency of and every agent attached to a
/// service.
///
/// There are no instructions. What a template cannot say — the provider
/// the tool runs on, the volumes it is given, the account's name — is
/// the caller's, and the caller needs no prose to choose it. The
/// daemon's own tool templates are another thing: a record the daemon
/// keeps, named by a hash, made by a request; this is what an image
/// declares and a caller deploys.
///
/// There is no name. A dependency is named by its ID: the lowercase
/// hexadecimal SHA-256 of its [`canonical`](crate::shared::canonical)
/// bytes — this template as compact JSON, absent members omitted, the
/// image's `references` absent too, every object key sorted at every
/// depth, the `arguments` and the `secrets` included, the names sorted
/// bytewise, the `mode` and
/// its members among the keys — sixty-four characters, which the caller
/// computes over [`hashed`](Self::hashed). Two declarations with one id
/// are one dependency, and a list that declares one twice is refused;
/// the same template deployed for the same agent, on whatever provider,
/// is the same dependency, and the same service template declared under
/// the same scope key, by whatever agent, is the same service. The
/// caller labels the tool's MCP tools by the id when it merges lists,
/// and the program knows which tool serves each by the keys under
/// `_meta`, see [`shared::mcp`](crate::shared::mcp).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct Template {
/// The image: a digest and its references, as a
/// `containers::tools::run` request names one.
pub image: Image,
/// The memory the tool container needs, in bytes.
pub memory: u64,
/// The bytes the tool container may write, in bytes.
pub disk: u64,
/// The arguments the tool container is registered with — the
/// `arguments` of the run request that makes it, verbatim.
pub arguments: Value,
/// The secrets the image needs, by name, in the order the image
/// states them: names only. Which of them the tool's program takes
/// and in what mode is the program's answer at its registration —
/// see [`secrets`](crate::shared::containers::secrets) — and the
/// value of each is resolved through the vault key the agent the
/// tool is spawned for binds the same name to, fed at every
/// activation. Inside the hash, sorted bytewise there. A name is
/// not empty, no two are alike, and every one is a name the
/// registering agent's template declares; a template that says
/// otherwise is the registration's error. Absent, the image needs
/// none.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<String>,
/// Which mode of dependency this is, with that mode's own members
/// flat beside the `mode` that names it: a transient tool's mounts,
/// or a service's scope. See [`Mode`].
#[serde(flatten)]
pub mode: Mode,
/// What the tool's account may do, as grants that name nothing: see
/// [`Grant`]. The caller gives the tool an account holding exactly
/// these. Absent when empty, and then the tool may do nothing
/// through the daemon.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub permissions: Vec<Grant>,
}
impl Template {
/// The template as its id hashes it: the same, with the image's
/// references absent, since where the bytes may be fetched is not
/// what the tool is, and the secrets' names sorted bytewise, since
/// the order an image states them in is not what it needs.
pub fn hashed(&self) -> Template {
let mut secrets = self.secrets.clone();
secrets.sort_unstable();
Template {
image: self.image.hashed(),
secrets,
..self.clone()
}
}
}
and by diverge-sdk-rs/src/shared/containers/dependencies/mount.rs:
//! One file, or one directory, of the agent container served live
//! into the tool container.
use serde::{Deserialize, Serialize};
/// A path of the agent container, mounted at a path of the tool
/// container over FUSE: source before destination, the order a mount
/// reads in everywhere else. Which of the two it is — a file or a
/// directory — is which list of the [`Template`](super::Template) it
/// is on.
///
/// The caller serves the agent container's path live — a
/// `containers::fuse_serve` of it, or of the directory holding a file —
/// and makes the mount in the tool container as a FUSE mount answered
/// from that FUSE serve, so every read, write, listing, removal, rename and
/// new directory the tool makes is one ask answered from the agent's
/// own filesystem, where the agent sees it at once. Nothing is copied:
/// the two containers share the path for as long as both run.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
pub struct Mount {
/// Where it is in the agent container, as path components from the
/// agent container's root — the shape every path in this crate
/// takes. No component is empty, `.` or `..`, and the path is not
/// the root: a file on the file list, a directory on the directory
/// list.
pub agent_path: Vec<String>,
/// Where it appears in the tool container, as path components from
/// the tool container's root, under the rules a
/// [`Container`](crate::shared::containers::request::Container)
/// states for its mounts: not the root, no other mount's, inside
/// no other mount's, and no other mount's inside it.
pub tool_path: Vec<String>,
}
and by diverge-sdk-rs/src/shared/containers/dependencies/mode.rs:
//! Which of the two modes a dependency template declares.
use serde::{Deserialize, Serialize};
use super::{Mount, Scope};
/// A transient tool, or a service: the two things a dependency can be,
/// told apart on the wire by the `mode` member beside the template's
/// other members — `"transient"` or `"service"` — with the members of
/// that mode flat beside it, and no member of the other.
///
/// # A transient tool is the agent's run's
///
/// Deployed when the agent's container starts, from the template as
/// the program declared it, and stopped with the agent's run: it
/// lives exactly as long as one run of one agent, and the next run
/// deploys it again. Its database scope is the agent's — one per
/// parent agent, which it has without saying so — and it alone may
/// mount the agent's own files and directories, since there is one
/// agent whose files they are.
///
/// # A service is the caller's
///
/// Made once, when the first agent that declares it registers, under
/// the scope key its [`Scope`] names — the agent, the agent's
/// template, or the template alone — and kept by the caller from then
/// on: started again whenever its container ends, and at every start
/// of the caller's, for as long as any agent is attached to it. An
/// agent is attached by a registration that declares the template and
/// detached by one that no longer does, and a service with no agent
/// attached goes down for good after the caller's idle time, an
/// attachment within that time keeping it. Nothing else makes a
/// service inactive: it serves no agent's run in particular, so no
/// run's end is its end. It mounts nothing of any agent's, since it
/// is many agents'. Its database scope is shared as far as it is, and
/// lives as long as its scope's owner: the agent, the agent template,
/// or — for a global service — the last agent template whose agents
/// ever attached.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum Mode {
/// A tool for the agent's run: `"mode":"transient"`.
Transient {
/// Files of the agent container served live into the tool
/// container, one each over FUSE: see [`Mount`]. The agent's
/// path names a file; the tool's path is where it appears.
/// Absent when empty.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fuse_file_mounts: Vec<Mount>,
/// Directories of the agent container served live into the
/// tool container, one each over FUSE: see [`Mount`]. The
/// agent's path names a directory, whose whole tree the tool
/// sees; the tool's path is where it appears. Absent when
/// empty.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
fuse_directory_mounts: Vec<Mount>,
},
/// A service the caller keeps: `"mode":"service"`.
Service {
/// How far the service is shared, and its database with it:
/// see [`Scope`].
scope: Scope,
},
}
and by diverge-sdk-rs/src/shared/containers/dependencies/scope.rs:
//! How far a service is shared.
use serde::{Deserialize, Serialize};
/// What a service tool is one of: one per parent agent, one per
/// parent agent template, or one for every agent that declares it,
/// whatever template they are made from. The scope names the service
/// — the same template declared under the same scope key is the same
/// service, joined and not made again — and names its database scope
/// with it, so what a service holds is shared exactly as far as the
/// service is. Snake case on the wire: `"instance"`,
/// `"template"`, `"global"`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
/// One service per parent agent: the service deployed for one
/// agent is its own, with a database of its own, and two agents'
/// services of this template never see each other's. The scope a
/// transient tool has without saying so.
Instance,
/// One service per parent agent template: every agent made from
/// the same agent template that declares this template is
/// attached to one service, which holds one database, so what one
/// writes the others read.
Template,
/// One service for the template, full stop: every agent that
/// declares it, from whatever agent template, is attached to the
/// one service, and its database is one for all of them.
Global,
}
and by diverge-sdk-rs/src/shared/containers/secrets/declaration.rs:
//! Which secrets a program takes, and how.
use indexmap::IndexMap;
use super::Mode;
/// The program's answer about its secrets: each name it takes, as
/// its template declares it, to the [`Mode`] it takes it in, in the
/// order the program names them. A name the template does not
/// declare is the registration's error; a name the template declares
/// and this omits is a secret the program does not want, and is never
/// fed. Empty, the program takes none: nothing is ever withheld from
/// it for want of a feed, and nothing is collected from it.
pub type Declaration = IndexMap<String, Mode>;
and by diverge-sdk-rs/src/shared/containers/secrets/mode.rs:
//! How a program takes one of its secrets.
use serde::{Deserialize, Serialize};
/// What a program does with a secret, declared per name when it
/// registers: read it, or hold and change it. Snake case on the
/// wire: `"fixed"`, `"rotating"`.
///
/// The mode is the program's to declare and the caller's to honour:
/// a template names the secret, a record binds the name to a vault
/// key, and this says whether the value the container is handed is
/// one of the key's to read or one of the key's to keep.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
/// Read, never locked: at every activation the caller hands the
/// container a random value among the key's, and takes nothing
/// back at the inactive. Several containers may read the same
/// value at once.
Fixed,
/// Held and written back: at every activation the caller locks
/// one free value of the key's to the container and hands it
/// over; at the inactive the caller collects what the program
/// made of it, writes that into the same place and drops the
/// lock. No two containers hold one value at once, and a key
/// with every value held makes the next activation fail.
Rotating,
}
and by diverge-sdk-rs/src/shared/permission/grant.rs:
//! One grant, by the kind it is over.
use serde::{Deserialize, Serialize};
use super::{accounts, agents, agents_templates, dependency_tools, events, postgres, providers_daemons, providers_incoming, providers_outgoing, roles, standalone_tools, standalone_tools_templates, vault, volumes};
/// One grant that names nothing: a permission over one kind of thing
/// the daemon holds, reaching by tags alone. Externally tagged by the
/// kind's name — one object with one member, `{"agents":…}`,
/// `{"volumes":…}` — whose value is that kind's
/// [`Permission`](agents::Permission), in one of the shapes
/// [`permission`](crate::shared::permission) states. Snake case on the
/// wire: `"agents"`, `"agents_templates"`, `"standalone_tools"`,
/// `"standalone_tools_templates"`, `"dependency_tools"`,
/// `"providers_outgoing"`,
/// `"providers_incoming"`, `"providers_daemons"`, `"accounts"`,
/// `"roles"`, `"volumes"`, `"vault"`, `"postgres"`, `"events"` — the
/// kinds of the daemon's [`Grant`](crate::daemon::grant::Grant), which
/// is made of these.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Grant {
/// Over agents: see [`agents::Permission`].
Agents(agents::Permission),
/// Over agent templates: see [`agents_templates::Permission`].
AgentsTemplates(agents_templates::Permission),
/// Over standalone tools: see [`standalone_tools::Permission`].
StandaloneTools(standalone_tools::Permission),
/// Over standalone tool templates: see
/// [`standalone_tools_templates::Permission`].
StandaloneToolsTemplates(standalone_tools_templates::Permission),
/// Over dependency tools: see [`dependency_tools::Permission`].
DependencyTools(dependency_tools::Permission),
/// Over outgoing providers: see [`providers_outgoing::Permission`].
ProvidersOutgoing(providers_outgoing::Permission),
/// Over incoming credentials: see [`providers_incoming::Permission`].
ProvidersIncoming(providers_incoming::Permission),
/// Over daemons: see [`providers_daemons::Permission`].
ProvidersDaemons(providers_daemons::Permission),
/// Over accounts: see [`accounts::Permission`].
Accounts(accounts::Permission),
/// Over roles: see [`roles::Permission`].
Roles(roles::Permission),
/// Over volumes: see [`volumes::Permission`].
Volumes(volumes::Permission),
/// Over the vault's keys: see [`vault::Permission`].
Vault(vault::Permission),
/// Over the database: see [`postgres::Permission`].
Postgres(postgres::Permission),
/// Over the events: see [`events::Permission`].
Events(events::Permission),
}
and by diverge-sdk-rs/src/shared/permission/within.rs:
//! Every one, or only these.
use std::borrow::Cow;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
/// How far a grant reaches, or which tags it may touch: every one there
/// is, or only what `T` names. Flat on the wire: the string `"any"`, or
/// `T` as it is — a filter object, a list of tags. `"any"` is no filter
/// and no list, so the two shapes never collide; a string that is not
/// `"any"` does not decode.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Within<T> {
/// Every one: the string `"any"`.
Any,
/// Only what `T` names, as `T` is on the wire.
Only(T),
}
impl<T> Within<T> {
/// Whether this is `"any"`: what a member left absent means.
pub fn is_any(&self) -> bool {
matches!(self, Within::Any)
}
}
impl<T> Default for Within<T> {
/// `"any"`: a member left absent reaches everything.
fn default() -> Self {
Within::Any
}
}
impl<T: Serialize> Serialize for Within<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Within::Any => serializer.serialize_str("any"),
Within::Only(only) => only.serialize(serializer),
}
}
}
/// The one word, so that it is read as a word and not as a `T`.
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum Word {
Any,
}
/// The two shapes, tried in order: the word, then `T`.
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr<T> {
Word(Word),
Only(T),
}
impl<'de, T: Deserialize<'de>> Deserialize<'de> for Within<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match Repr::deserialize(deserializer)? {
Repr::Word(Word::Any) => Within::Any,
Repr::Only(only) => Within::Only(only),
})
}
}
/// The two shapes as a schema: the one word, or `T`'s own. Written by
/// hand because the derive follows a derived serde and this type's
/// serde is its own.
impl<T: JsonSchema> JsonSchema for Within<T> {
fn schema_name() -> Cow<'static, str> {
format!("Within_{}", T::schema_name()).into()
}
fn schema_id() -> Cow<'static, str> {
format!("diverge_sdk::shared::permission::Within<{}>", T::schema_id()).into()
}
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
let only = generator.subschema_for::<T>();
json_schema!({
"anyOf": [
{ "type": "string", "const": "any" },
only
]
})
}
}
and, for the grant over agents, by
diverge-sdk-rs/src/shared/permission/agents/within.rs:
//! Every agent, the dependency's own, or only these.
use std::borrow::Cow;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use crate::shared::permission::Tags;
/// How far a grant over agents reaches: every agent there is, the
/// dependency's own agents, or only those the [`Tags`] let through.
/// Flat on the wire: the string `"any"`, the string `"self"`, or the
/// tags object as it is. Neither word is a tags object, so the three
/// shapes never collide; a string that is neither does not decode.
///
/// # `self` is the agents the dependency is a dependency of
///
/// The one word the agents kind has that no other has, and that no
/// grant of the daemon's own has: it names nothing and reaches, for a
/// transient tool, the one agent it was deployed for, and, for a
/// service, every agent attached to it — attached by a registration
/// that declared the service's template, and still so — whether or
/// not any of them is active. A tool that may `message` within
/// `self` may message its own agents and no other, whatever their
/// tags, which is what a tool that answers to its agents asks for.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Within {
/// Every agent: the string `"any"`.
Any,
/// The dependency's own agents: the string `"self"`.
Self_,
/// Only the agents the tags let through, as a test.
Only(Tags),
}
impl Serialize for Within {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Within::Any => serializer.serialize_str("any"),
Within::Self_ => serializer.serialize_str("self"),
Within::Only(only) => only.serialize(serializer),
}
}
}
/// The two words, so that each is read as a word and not as tags.
#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum Word {
Any,
#[serde(rename = "self")]
Self_,
}
/// The two shapes, tried in order: a word, then the tags.
#[derive(Deserialize)]
#[serde(untagged)]
enum Repr {
Word(Word),
Only(Tags),
}
impl<'de> Deserialize<'de> for Within {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match Repr::deserialize(deserializer)? {
Repr::Word(Word::Any) => Within::Any,
Repr::Word(Word::Self_) => Within::Self_,
Repr::Only(only) => Within::Only(only),
})
}
}
/// The three shapes as a schema: either word, or the tags' own.
/// Written by hand because the derive follows a derived serde and
/// this type's serde is its own.
impl JsonSchema for Within {
fn schema_name() -> Cow<'static, str> {
"AgentsWithin".into()
}
fn schema_id() -> Cow<'static, str> {
"diverge_sdk::shared::permission::agents::Within".into()
}
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
let only = generator.subschema_for::<Tags>();
json_schema!({
"anyOf": [
{ "type": "string", "const": "any" },
{ "type": "string", "const": "self" },
only
]
})
}
}
and by diverge-sdk-rs/src/shared/permission/tags.rs:
//! The one test a grant that names nothing reaches by.
use serde::{Deserialize, Serialize};
/// Which of a kind a grant reaches, by their tags: those carrying
/// every one of `all_tags` and any one of `any_tags`. A member that
/// is empty is absent, and passes everything; both empty is every one
/// of the kind, which `"any"` says more plainly. Read as a TEST, as
/// the daemon reads a `within`: a thing passes when it passes every
/// member given. The two words are the ones every list filter of the
/// daemon's already has, so a grant here and a grant of the daemon's
/// read tags the same way. No other member decodes: a `within` that
/// named something — `names`, `templates`, `creators` — would
/// otherwise read as no tags at all, which is everything, and a grant
/// must never widen by being misread.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[derive(schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Tags {
/// Every one of these among the thing's tags. Absent when empty,
/// and then any tags.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub all_tags: Vec<String>,
/// Any one of these among the thing's tags; with `all_tags`, both
/// hold. Absent when empty, and then any tags.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub any_tags: Vec<String>,
}