Response
The server sends Begun once, carrying the secrets the container takes, or an error and the finish; after Begun it sends Active whenever an exchange is in flight where none was and Inactive, carrying the rotating secrets, whenever the last in flight is answered, and the finish when the server ends or after the one error it sends when it cannot collect them.
[0][registration JSON …] begun: the container holds its arguments, and these are its secrets
[1][error JSON …] not begun: the connection had begun, or the arguments were refused; or, after begun, the rotating secrets could not be collected
[2] active: an exchange is in flight where none was
[3][values JSON …] inactive: the last exchange in flight was answered, and these are the rotating secrets
- The sequence. Exactly one response whose first byte is
0, followed by zero or more periods of activity, followed by the response finish when the server ends; or exactly one response whose first byte is1, followed by the response finish. A period of activity is exactly one response whose payload is the byte2and nothing else, followed by exactly one response whose first byte is3, or, where the server cannot collect the rotating secrets, by exactly one response whose first byte is1and the response finish. The server sends no response whose first byte is2or3beforeBegun, no byte2while a period is open, no byte3while none is, and none after the finish. - Active and inactive. An exchange is one
mcp-list-tools,
mcp-list-resources,
mcp-call-tool or
mcp-read-resource
channel, from its request to its answer; an
mcp-notifications
channel is no exchange. The server sends the byte
2when the number of exchanges in flight goes from none to one, before that exchange is delivered to the program, and the byte3when the number goes from one to none, after the last exchange is answered and after the program has handed back the rotating secrets. The server produces both itself and relays neither from the program. The byte2carries nothing after it. - The registration. The bytes after the byte
0are a JSON object of one member, absent when empty:secrets, an 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. A tool container declares no dependencies: a list its program answers its registration with is not carried, and the object has nodependenciesmember. The server sends the declaration exactly as the program produced it and reads none of it. - The values. The bytes after the byte
3are a JSON object whose every member is the name of a secret the program declaredrotating, to the value the program leaves in it, any JSON value: everyrotatingname the program declared, and no other; the empty object for a program that declared nonerotating. The server collects them from the program after the last exchange is answered, and sends them as the program handed them back. From the byte3the program holds no values, and the server delivers no exchange to a program that declared a secret until the next secrets channel is answeredFed. - The error. After the byte
1, exactly one JSON value in the form defined on the volumes::list response page. Refused arguments carry the container’s own words. AfterBegun, the byte1states that the program did not hand back everyrotatingsecret the server asked for at the end of a period of activity; the error names the secret and carries no value, and the finish follows it. A value the program did not hand back is not written anywhere by the server. - A finish with nothing. A response finish that no response precedes states that the server could not read the request.
The response is defined by diverge-sdk-rs/src/container_proxy/outside/endpoints/tools/begin/server/response/frame.rs:
//! What a server's response frame carries for a tool container begin.
use std::fmt;
use super::Registered;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::containers::secrets::Values;
use crate::shared::error::Error;
/// A begin's answer: that the connection has begun, then the proxy's
/// word whenever the container goes active and inactive, for as long
/// as the connection lives — or a failure.
///
/// A payload leads with one byte saying which — `0` for
/// [`Begun`](Self::Begun), `1` for [`Error`](Self::Error), `2` for
/// [`Active`](Self::Active), `3` for [`Inactive`](Self::Inactive) —
/// and the rest is that variant's own JSON, or nothing: the one bare
/// word carries nothing after the byte.
///
/// # Begun, then the activity
///
/// | the scope | means |
/// |-----------|-------|
/// | a begun, then quiet, and stays open | the connection has begun, what its program declared is known, and no exchange has been in flight |
/// | a begun, an active, and stays open | one or more MCP exchanges are in flight: the container is serving |
/// | …an active, an inactive, and stays open | the last in flight has been answered, its rotating secrets collected; the container is idle until the next |
/// | an error, then a finish | it has not begun — this connection had already begun, or the arguments were refused; or, after `Begun`, the secrets could not be collected |
/// | a finish, with no error | the proxy is ending |
///
/// # The proxy marks the activity, and only the proxy can
///
/// An exchange is one of the four MCP calls the server opens into
/// the container — `tools/list`, `resources/list`, `tools/call`,
/// `resources/read`; notifications are no exchange. The proxy counts
/// them in flight: the count going from none to one is
/// [`Active`](Self::Active), sent before that exchange is delivered,
/// and the count going from one to none is
/// [`Inactive`](Self::Inactive), sent after the last is answered and
/// the program has handed back its rotating secrets, which ride it.
/// Nothing the program says is read as either word.
///
/// # Nothing is delivered before a feed
///
/// After `Begun`, and after every `Inactive`, the proxy delivers
/// nothing to a program that declared a secret until the server has
/// fed the values on a
/// [`Secrets`](super::super::super::client::channel_request::Frame::Secrets)
/// channel and the proxy has answered it `Fed`: an exchange opened
/// meanwhile is held, its `Active` already sent. A program that
/// declared none is never gated.
///
/// Everything else the server reads from the container — the
/// family's own exchange, the asks the container makes — is a
/// channel, not this stream. It carries no readiness signal beyond
/// `Begun`: the proxy is here, the arguments are held, and channels
/// may be opened.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
/// The connection has begun, the container holds its arguments
/// for its life, and this is what its program declared. Tag `0`.
///
/// Arrives once, when the container's server has taken the
/// arguments and answered its registration — as JSON, a
/// [`Registered`]: the secrets it takes, a map, empty for a
/// program that takes none; a tool container declares no
/// dependencies, and a list its program answers with is ignored.
/// See [`registration`](crate::shared::containers::registration)
/// for what the provider does with it. A channel on this scope is
/// opened only after it.
Begun(Registered),
/// A failure. Tag `1`.
///
/// A second begin on a connection that had one, or arguments the
/// container refused — the container's server's own words: a
/// value the image will not take — or, after `Begun`, rotating
/// secrets the program would not hand back, named and never
/// valued. 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),
/// An exchange is in flight where none was. Tag `2`, and nothing
/// after it.
///
/// Sent by the proxy before the exchange is delivered, never
/// relayed from the program; never twice without an
/// [`Inactive`](Self::Inactive) between.
Active,
/// The last exchange in flight has been answered, and these are
/// the rotating secrets. Tag `3`.
///
/// The program handed back every `rotating` secret it declared,
/// each by name to the value it leaves in it, as JSON after the
/// byte: the [`Values`] the caller writes back. Empty for a
/// program that declared none rotating. Sent by the proxy, never
/// relayed from the program; never without an
/// [`Active`](Self::Active) before it.
Inactive(Values),
}
/// Tag for [`Frame::Begun`].
const BEGUN: u8 = 0;
/// Tag for [`Frame::Error`].
const ERROR: u8 = 1;
/// Tag for [`Frame::Active`]. Public, so the proxy writes the one
/// byte that is the whole frame.
pub const ACTIVE: u8 = 2;
/// Tag for [`Frame::Inactive`]. Public, so a relay that carries the
/// values' JSON without reading it can frame it.
pub const INACTIVE: u8 = 3;
/// A tag, and that variant's own JSON — or the tag alone.
impl Encode for Frame {
/// The ordinary JSON failure; `Active` cannot fail.
type Error = serde_json::Error;
// 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<(), serde_json::Error> {
match self {
Frame::Begun(registered) => {
out.extend_from_slice(&[BEGUN]);
serde_json::to_writer(out, registered)
}
Frame::Error(error) => {
out.extend_from_slice(&[ERROR]);
error.encode(out)
}
Frame::Active => {
out.extend_from_slice(&[ACTIVE]);
Ok(())
}
Frame::Inactive(values) => {
out.extend_from_slice(&[INACTIVE]);
serde_json::to_writer(out, values)
}
}
}
}
impl Decode<'_> for Frame {
/// Five ways to fail, and three of them are JSON.
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 {
BEGUN => serde_json::from_slice(rest).map(Frame::Begun).map_err(FrameError::Begun),
ERROR => {
Error::decode(rest).map(Frame::Error).map_err(FrameError::Error)
}
ACTIVE => Ok(Frame::Active),
INACTIVE => serde_json::from_slice(rest)
.map(Frame::Inactive)
.map_err(FrameError::Inactive),
tag => Err(FrameError::UnknownTag(tag)),
}
}
}
/// A tools begin 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 none of this frame's four.
UnknownTag(u8),
/// What the program declared did not parse.
Begun(serde_json::Error),
/// The error did not parse.
Error(serde_json::Error),
/// The rotating secrets did not parse. The message is the
/// parser's, which quotes no value.
Inactive(serde_json::Error),
}
impl fmt::Display for FrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameError::Empty => f.write_str("tools begin response frame is empty"),
FrameError::UnknownTag(tag) => {
write!(f, "unknown tools begin response frame tag {tag}")
}
FrameError::Begun(error) => {
write!(f, "tools begin registration did not parse: {error}")
}
FrameError::Error(error) => {
write!(f, "tools begin error did not parse: {error}")
}
FrameError::Inactive(error) => {
write!(f, "tools begin inactive secrets did not parse: {error}")
}
}
}
}
impl std::error::Error for FrameError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FrameError::Begun(error) | FrameError::Error(error) | FrameError::Inactive(error) => Some(error),
FrameError::Empty | FrameError::UnknownTag(_) => None,
}
}
}
and by diverge-sdk-rs/src/container_proxy/outside/endpoints/tools/begin/server/response/registered.rs:
//! What the container's program declared when it registered.
use serde::{Deserialize, Serialize};
use crate::shared::containers::secrets::Declaration;
/// The program's answer to its registration, as `Begun` carries it:
/// the secrets it takes, each by name to its mode, the container's
/// for its life. A tool container declares no dependencies — a list
/// its program answers with is ignored by the proxy, and none rides
/// here. The provider carries the declaration to the caller in one
/// [`registration`](crate::shared::containers::registration) ask when
/// it is non-empty, and reads nothing of it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Registered {
/// The secrets the program takes, each by name to its mode, in
/// the order the program declared them: see
/// [`secrets`](crate::shared::containers::secrets). Absent when
/// empty.
#[serde(default, skip_serializing_if = "Declaration::is_empty")]
pub secrets: Declaration,
}
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/containers/secrets/values.rs:
//! The secrets' values, by name.
use indexmap::IndexMap;
use serde_json::Value;
/// Secrets by name to value, any JSON: what the caller feeds a
/// container, and what the program hands back. Fed, it carries every
/// name the program declared, in the order declared — a `fixed` one
/// a value read from its key, a `rotating` one the value locked to
/// the container. Collected at an inactive, it carries every
/// `rotating` name the program declared, each with the value the
/// program leaves in it; a `fixed` name in it is ignored, and a
/// `rotating` name missing from it is the collect's error, never a
/// write-back. A value is never read by anything between the vault
/// and the program.
pub type Values = IndexMap<String, Value>;