//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `consent/approve-request`. Version: `0.1`.
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
/// Error types.
pub mod error {
/// Error from a `TryFrom` or `FromStr` implementation.
pub struct ConversionError(::std::borrow::Cow<'static, str>);
impl ::std::error::Error for ConversionError {}
impl ::std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::fmt::Debug for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Debug::fmt(&self.0, f)
}
}
impl From<&'static str> for ConversionError {
fn from(value: &'static str) -> Self {
Self(value.into())
}
}
impl From<String> for ConversionError {
fn from(value: String) -> Self {
Self(value.into())
}
}
}
///Interaction kind. Present because it changes what the approver is agreeing to: consenting to an agent reading a 1:1 conversation exposes two parties, and consenting on a group exposes everyone in it, most of whom are not being asked.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ConsentKind",
/// "description": "Interaction kind. Present because it changes what the approver is agreeing to: consenting to an agent reading a 1:1 conversation exposes two parties, and consenting on a group exposes everyone in it, most of whom are not being asked.",
/// "type": "string",
/// "enum": [
/// "dm",
/// "group",
/// "channel"
/// ],
/// "$anchor": "consentKind"
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ConsentKind {
#[serde(rename = "dm")]
Dm,
#[serde(rename = "group")]
Group,
#[serde(rename = "channel")]
Channel,
}
impl ::std::fmt::Display for ConsentKind {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Dm => f.write_str("dm"),
Self::Group => f.write_str("group"),
Self::Channel => f.write_str("channel"),
}
}
}
impl ::std::str::FromStr for ConsentKind {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"dm" => Ok(Self::Dm),
"group" => Ok(Self::Group),
"channel" => Ok(Self::Channel),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ConsentKind {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ConsentKind {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ConsentKind {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///What the agent may do. `receive` is read-only: the agent sees inbound messages. `converse` additionally lets it reply, which means it can speak to the other parties as the subject — a materially different decision, and the reason the two are distinct values rather than a boolean.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ConsentScope",
/// "description": "What the agent may do. `receive` is read-only: the agent sees inbound messages. `converse` additionally lets it reply, which means it can speak to the other parties as the subject — a materially different decision, and the reason the two are distinct values rather than a boolean.",
/// "type": "string",
/// "enum": [
/// "receive",
/// "converse"
/// ],
/// "$anchor": "consentScope"
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ConsentScope {
#[serde(rename = "receive")]
Receive,
#[serde(rename = "converse")]
Converse,
}
impl ::std::fmt::Display for ConsentScope {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Receive => f.write_str("receive"),
Self::Converse => f.write_str("converse"),
}
}
}
impl ::std::str::FromStr for ConsentScope {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"receive" => Ok(Self::Receive),
"converse" => Ok(Self::Converse),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ConsentScope {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ConsentScope {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ConsentScope {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///Platform-agnostic identifier of what consent is about. Deliberately not a platform address: a consent record that named one would be a stored directory of who the subject talks to, readable by anyone who reaches the store.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ConsentSubject",
/// "description": "Platform-agnostic identifier of what consent is about. Deliberately not a platform address: a consent record that named one would be a stored directory of who the subject talks to, readable by anyone who reaches the store.",
/// "type": "object",
/// "required": [
/// "agent",
/// "conversationRef",
/// "kind",
/// "platform"
/// ],
/// "properties": {
/// "agent": {
/// "description": "DID of the agent the decision is about. Consent is granted to one agent, not to the service that hosts it, so that revoking one agent's access does not revoke another's.",
/// "type": "string",
/// "pattern": "^did:"
/// },
/// "conversationRef": {
/// "description": "The bridge's OPAQUE handle for the conversation. MUST NOT be a raw platform address (a phone number, a handle, a group invite link). The approver identifies the conversation from displayHint; this member exists so a decision can be matched to a conversation without the matching key being personal data.",
/// "type": "string",
/// "minLength": 1
/// },
/// "kind": {
/// "$ref": "#/definitions/ConsentKind"
/// },
/// "platform": {
/// "description": "Messaging platform the conversation lives on, as the bridge names it (for example `signal`, `whatsapp`). Opaque to the framework.",
/// "type": "string",
/// "minLength": 1
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "consentSubject"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ConsentSubject {
///DID of the agent the decision is about. Consent is granted to one agent, not to the service that hosts it, so that revoking one agent's access does not revoke another's.
pub agent: ConsentSubjectAgent,
///The bridge's OPAQUE handle for the conversation. MUST NOT be a raw platform address (a phone number, a handle, a group invite link). The approver identifies the conversation from displayHint; this member exists so a decision can be matched to a conversation without the matching key being personal data.
#[serde(rename = "conversationRef")]
pub conversation_ref: ConsentSubjectConversationRef,
pub kind: ConsentKind,
///Messaging platform the conversation lives on, as the bridge names it (for example `signal`, `whatsapp`). Opaque to the framework.
pub platform: ConsentSubjectPlatform,
}
impl ConsentSubject {
pub fn builder() -> builder::ConsentSubject {
Default::default()
}
}
///DID of the agent the decision is about. Consent is granted to one agent, not to the service that hosts it, so that revoking one agent's access does not revoke another's.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "DID of the agent the decision is about. Consent is granted to one agent, not to the service that hosts it, so that revoking one agent's access does not revoke another's.",
/// "type": "string",
/// "pattern": "^did:"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ConsentSubjectAgent(::std::string::String);
impl ::std::ops::Deref for ConsentSubjectAgent {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ConsentSubjectAgent> for ::std::string::String {
fn from(value: ConsentSubjectAgent) -> Self {
value.0
}
}
impl ::std::str::FromStr for ConsentSubjectAgent {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| ::regress::Regex::new("^did:").unwrap());
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^did:\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ConsentSubjectAgent {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ConsentSubjectAgent {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ConsentSubjectAgent {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ConsentSubjectAgent {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///The bridge's OPAQUE handle for the conversation. MUST NOT be a raw platform address (a phone number, a handle, a group invite link). The approver identifies the conversation from displayHint; this member exists so a decision can be matched to a conversation without the matching key being personal data.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "The bridge's OPAQUE handle for the conversation. MUST NOT be a raw platform address (a phone number, a handle, a group invite link). The approver identifies the conversation from displayHint; this member exists so a decision can be matched to a conversation without the matching key being personal data.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ConsentSubjectConversationRef(::std::string::String);
impl ::std::ops::Deref for ConsentSubjectConversationRef {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ConsentSubjectConversationRef> for ::std::string::String {
fn from(value: ConsentSubjectConversationRef) -> Self {
value.0
}
}
impl ::std::str::FromStr for ConsentSubjectConversationRef {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ConsentSubjectConversationRef {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ConsentSubjectConversationRef {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ConsentSubjectConversationRef {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ConsentSubjectConversationRef {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Messaging platform the conversation lives on, as the bridge names it (for example `signal`, `whatsapp`). Opaque to the framework.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Messaging platform the conversation lives on, as the bridge names it (for example `signal`, `whatsapp`). Opaque to the framework.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ConsentSubjectPlatform(::std::string::String);
impl ::std::ops::Deref for ConsentSubjectPlatform {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ConsentSubjectPlatform> for ::std::string::String {
fn from(value: ConsentSubjectPlatform) -> Self {
value.0
}
}
impl ::std::str::FromStr for ConsentSubjectPlatform {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ConsentSubjectPlatform {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ConsentSubjectPlatform {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ConsentSubjectPlatform {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ConsentSubjectPlatform {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Ext",
/// "description": "Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.",
/// "type": "object",
/// "minProperties": 1,
/// "additionalProperties": true,
/// "propertyNames": {
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
/// }
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct Ext(pub ::std::collections::HashMap<ExtKey, ::serde_json::Value>);
impl ::std::ops::Deref for Ext {
type Target = ::std::collections::HashMap<ExtKey, ::serde_json::Value>;
fn deref(&self) -> &::std::collections::HashMap<ExtKey, ::serde_json::Value> {
&self.0
}
}
impl ::std::convert::From<Ext> for ::std::collections::HashMap<ExtKey, ::serde_json::Value> {
fn from(value: Ext) -> Self {
value.0
}
}
impl ::std::convert::From<::std::collections::HashMap<ExtKey, ::serde_json::Value>> for Ext {
fn from(value: ::std::collections::HashMap<ExtKey, ::serde_json::Value>) -> Self {
Self(value)
}
}
///`ExtKey`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "type": "string",
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ExtKey(::std::string::String);
impl ::std::ops::Deref for ExtKey {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ExtKey> for ::std::string::String {
fn from(value: ExtKey) -> Self {
value.0
}
}
impl ::std::str::FromStr for ExtKey {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| {
::regress::Regex::new("^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$").unwrap()
});
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ExtKey {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///The prompt an agent's home service sends to a designated approver, asking a human to decide whether an agent may act on one conversation. It carries no decision itself: the approver answers with a separate, separately-signed consent/decision. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "$id": "https://trusttasks.org/spec/consent/approve-request/0.1",
/// "title": "Payload",
/// "description": "The prompt an agent's home service sends to a designated approver, asking a human to decide whether an agent may act on one conversation. It carries no decision itself: the approver answers with a separate, separately-signed consent/decision. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.",
/// "type": "object",
/// "required": [
/// "challenge",
/// "scope",
/// "subject"
/// ],
/// "properties": {
/// "challenge": {
/// "description": "Single-use, unpredictable value the approver MUST echo in the consent/decision it signs. This is what binds a decision to this request: without it a decision is a free-floating assertion that the approver consented to something, and cannot be shown to answer this prompt rather than an earlier one. Consumers MUST reject a decision whose challenge they did not issue, and MUST NOT accept the same challenge twice.",
/// "type": "string",
/// "minLength": 1
/// },
/// "displayHint": {
/// "description": "Human-readable label for the conversation, for the approver's screen — \"Signal group 'Family'\". Present because conversationRef is deliberately opaque: without a hint the approver is asked to decide about an identifier that means nothing to them, and will either always allow or always deny. Advisory only. It is chosen by the requesting party, so a renderer MUST treat it as untrusted text — escape it, bound its length, and never let it displace the subject members above as the basis of the decision.",
/// "type": "string",
/// "maxLength": 256
/// },
/// "ext": {
/// "description": "Ecosystem-defined extension members per SPEC.md §4.5.1.",
/// "$ref": "#/definitions/Ext"
/// },
/// "firstMessageDigest": {
/// "description": "Digest of the inbound message that prompted this request, when there is one, so an approver deciding on a receive scope can confirm the decision it is being asked to make corresponds to a real arriving message. A digest rather than the content: the approver is deciding whether the agent may read the conversation, and showing them the message in order to ask would disclose what the decision is meant to gate.",
/// "type": "string",
/// "minLength": 1
/// },
/// "scope": {
/// "description": "What the agent is asking to be allowed to do.",
/// "$ref": "#/definitions/ConsentScope"
/// },
/// "subject": {
/// "description": "What the decision is about: one conversation, for one agent.",
/// "$ref": "#/definitions/ConsentSubject"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Payload {
///Single-use, unpredictable value the approver MUST echo in the consent/decision it signs. This is what binds a decision to this request: without it a decision is a free-floating assertion that the approver consented to something, and cannot be shown to answer this prompt rather than an earlier one. Consumers MUST reject a decision whose challenge they did not issue, and MUST NOT accept the same challenge twice.
pub challenge: PayloadChallenge,
///Human-readable label for the conversation, for the approver's screen — "Signal group 'Family'". Present because conversationRef is deliberately opaque: without a hint the approver is asked to decide about an identifier that means nothing to them, and will either always allow or always deny. Advisory only. It is chosen by the requesting party, so a renderer MUST treat it as untrusted text — escape it, bound its length, and never let it displace the subject members above as the basis of the decision.
#[serde(
rename = "displayHint",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub display_hint: ::std::option::Option<PayloadDisplayHint>,
///Ecosystem-defined extension members per SPEC.md §4.5.1.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
///Digest of the inbound message that prompted this request, when there is one, so an approver deciding on a receive scope can confirm the decision it is being asked to make corresponds to a real arriving message. A digest rather than the content: the approver is deciding whether the agent may read the conversation, and showing them the message in order to ask would disclose what the decision is meant to gate.
#[serde(
rename = "firstMessageDigest",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub first_message_digest: ::std::option::Option<PayloadFirstMessageDigest>,
///What the agent is asking to be allowed to do.
pub scope: ConsentScope,
///What the decision is about: one conversation, for one agent.
pub subject: ConsentSubject,
}
impl Payload {
pub fn builder() -> builder::Payload {
Default::default()
}
}
///Single-use, unpredictable value the approver MUST echo in the consent/decision it signs. This is what binds a decision to this request: without it a decision is a free-floating assertion that the approver consented to something, and cannot be shown to answer this prompt rather than an earlier one. Consumers MUST reject a decision whose challenge they did not issue, and MUST NOT accept the same challenge twice.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Single-use, unpredictable value the approver MUST echo in the consent/decision it signs. This is what binds a decision to this request: without it a decision is a free-floating assertion that the approver consented to something, and cannot be shown to answer this prompt rather than an earlier one. Consumers MUST reject a decision whose challenge they did not issue, and MUST NOT accept the same challenge twice.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadChallenge(::std::string::String);
impl ::std::ops::Deref for PayloadChallenge {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadChallenge> for ::std::string::String {
fn from(value: PayloadChallenge) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadChallenge {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadChallenge {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for PayloadChallenge {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for PayloadChallenge {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for PayloadChallenge {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Human-readable label for the conversation, for the approver's screen — "Signal group 'Family'". Present because conversationRef is deliberately opaque: without a hint the approver is asked to decide about an identifier that means nothing to them, and will either always allow or always deny. Advisory only. It is chosen by the requesting party, so a renderer MUST treat it as untrusted text — escape it, bound its length, and never let it displace the subject members above as the basis of the decision.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Human-readable label for the conversation, for the approver's screen — \"Signal group 'Family'\". Present because conversationRef is deliberately opaque: without a hint the approver is asked to decide about an identifier that means nothing to them, and will either always allow or always deny. Advisory only. It is chosen by the requesting party, so a renderer MUST treat it as untrusted text — escape it, bound its length, and never let it displace the subject members above as the basis of the decision.",
/// "type": "string",
/// "maxLength": 256
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadDisplayHint(::std::string::String);
impl ::std::ops::Deref for PayloadDisplayHint {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadDisplayHint> for ::std::string::String {
fn from(value: PayloadDisplayHint) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadDisplayHint {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() > 256usize {
return Err("longer than 256 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadDisplayHint {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for PayloadDisplayHint {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for PayloadDisplayHint {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for PayloadDisplayHint {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Digest of the inbound message that prompted this request, when there is one, so an approver deciding on a receive scope can confirm the decision it is being asked to make corresponds to a real arriving message. A digest rather than the content: the approver is deciding whether the agent may read the conversation, and showing them the message in order to ask would disclose what the decision is meant to gate.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Digest of the inbound message that prompted this request, when there is one, so an approver deciding on a receive scope can confirm the decision it is being asked to make corresponds to a real arriving message. A digest rather than the content: the approver is deciding whether the agent may read the conversation, and showing them the message in order to ask would disclose what the decision is meant to gate.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadFirstMessageDigest(::std::string::String);
impl ::std::ops::Deref for PayloadFirstMessageDigest {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadFirstMessageDigest> for ::std::string::String {
fn from(value: PayloadFirstMessageDigest) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadFirstMessageDigest {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadFirstMessageDigest {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for PayloadFirstMessageDigest {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for PayloadFirstMessageDigest {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for PayloadFirstMessageDigest {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
/// Types for composing complex structures.
pub mod builder {
#[derive(Clone, Debug)]
pub struct ConsentSubject {
agent: ::std::result::Result<super::ConsentSubjectAgent, ::std::string::String>,
conversation_ref:
::std::result::Result<super::ConsentSubjectConversationRef, ::std::string::String>,
kind: ::std::result::Result<super::ConsentKind, ::std::string::String>,
platform: ::std::result::Result<super::ConsentSubjectPlatform, ::std::string::String>,
}
impl ::std::default::Default for ConsentSubject {
fn default() -> Self {
Self {
agent: Err("no value supplied for agent".to_string()),
conversation_ref: Err("no value supplied for conversation_ref".to_string()),
kind: Err("no value supplied for kind".to_string()),
platform: Err("no value supplied for platform".to_string()),
}
}
}
impl ConsentSubject {
pub fn agent<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentSubjectAgent>,
T::Error: ::std::fmt::Display,
{
self.agent = value
.try_into()
.map_err(|e| format!("error converting supplied value for agent: {e}"));
self
}
pub fn conversation_ref<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentSubjectConversationRef>,
T::Error: ::std::fmt::Display,
{
self.conversation_ref = value
.try_into()
.map_err(|e| format!("error converting supplied value for conversation_ref: {e}"));
self
}
pub fn kind<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentKind>,
T::Error: ::std::fmt::Display,
{
self.kind = value
.try_into()
.map_err(|e| format!("error converting supplied value for kind: {e}"));
self
}
pub fn platform<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentSubjectPlatform>,
T::Error: ::std::fmt::Display,
{
self.platform = value
.try_into()
.map_err(|e| format!("error converting supplied value for platform: {e}"));
self
}
}
impl ::std::convert::TryFrom<ConsentSubject> for super::ConsentSubject {
type Error = super::error::ConversionError;
fn try_from(
value: ConsentSubject,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
agent: value.agent?,
conversation_ref: value.conversation_ref?,
kind: value.kind?,
platform: value.platform?,
})
}
}
impl ::std::convert::From<super::ConsentSubject> for ConsentSubject {
fn from(value: super::ConsentSubject) -> Self {
Self {
agent: Ok(value.agent),
conversation_ref: Ok(value.conversation_ref),
kind: Ok(value.kind),
platform: Ok(value.platform),
}
}
}
#[derive(Clone, Debug)]
pub struct Payload {
challenge: ::std::result::Result<super::PayloadChallenge, ::std::string::String>,
display_hint: ::std::result::Result<
::std::option::Option<super::PayloadDisplayHint>,
::std::string::String,
>,
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
first_message_digest: ::std::result::Result<
::std::option::Option<super::PayloadFirstMessageDigest>,
::std::string::String,
>,
scope: ::std::result::Result<super::ConsentScope, ::std::string::String>,
subject: ::std::result::Result<super::ConsentSubject, ::std::string::String>,
}
impl ::std::default::Default for Payload {
fn default() -> Self {
Self {
challenge: Err("no value supplied for challenge".to_string()),
display_hint: Ok(Default::default()),
ext: Ok(Default::default()),
first_message_digest: Ok(Default::default()),
scope: Err("no value supplied for scope".to_string()),
subject: Err("no value supplied for subject".to_string()),
}
}
}
impl Payload {
pub fn challenge<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::PayloadChallenge>,
T::Error: ::std::fmt::Display,
{
self.challenge = value
.try_into()
.map_err(|e| format!("error converting supplied value for challenge: {e}"));
self
}
pub fn display_hint<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::PayloadDisplayHint>>,
T::Error: ::std::fmt::Display,
{
self.display_hint = value
.try_into()
.map_err(|e| format!("error converting supplied value for display_hint: {e}"));
self
}
pub fn ext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::Ext>>,
T::Error: ::std::fmt::Display,
{
self.ext = value
.try_into()
.map_err(|e| format!("error converting supplied value for ext: {e}"));
self
}
pub fn first_message_digest<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::PayloadFirstMessageDigest>>,
T::Error: ::std::fmt::Display,
{
self.first_message_digest = value.try_into().map_err(|e| {
format!("error converting supplied value for first_message_digest: {e}")
});
self
}
pub fn scope<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentScope>,
T::Error: ::std::fmt::Display,
{
self.scope = value
.try_into()
.map_err(|e| format!("error converting supplied value for scope: {e}"));
self
}
pub fn subject<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ConsentSubject>,
T::Error: ::std::fmt::Display,
{
self.subject = value
.try_into()
.map_err(|e| format!("error converting supplied value for subject: {e}"));
self
}
}
impl ::std::convert::TryFrom<Payload> for super::Payload {
type Error = super::error::ConversionError;
fn try_from(value: Payload) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
challenge: value.challenge?,
display_hint: value.display_hint?,
ext: value.ext?,
first_message_digest: value.first_message_digest?,
scope: value.scope?,
subject: value.subject?,
})
}
}
impl ::std::convert::From<super::Payload> for Payload {
fn from(value: super::Payload) -> Self {
Self {
challenge: Ok(value.challenge),
display_hint: Ok(value.display_hint),
ext: Ok(value.ext),
first_message_digest: Ok(value.first_message_digest),
scope: Ok(value.scope),
subject: Ok(value.subject),
}
}
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/consent/approve-request/0.1";
const IS_PROOF_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
const PAYLOAD_SCHEMA: Option<&'static str> = Some(
"{\n \"$defs\": {\n \"ConsentKind\": {\n \"$anchor\": \"consentKind\",\n \"description\": \"Interaction kind. Present because it changes what the approver is agreeing to: consenting to an agent reading a 1:1 conversation exposes two parties, and consenting on a group exposes everyone in it, most of whom are not being asked.\",\n \"enum\": [\n \"dm\",\n \"group\",\n \"channel\"\n ],\n \"title\": \"ConsentKind\",\n \"type\": \"string\"\n },\n \"ConsentScope\": {\n \"$anchor\": \"consentScope\",\n \"description\": \"What the agent may do. `receive` is read-only: the agent sees inbound messages. `converse` additionally lets it reply, which means it can speak to the other parties as the subject — a materially different decision, and the reason the two are distinct values rather than a boolean.\",\n \"enum\": [\n \"receive\",\n \"converse\"\n ],\n \"title\": \"ConsentScope\",\n \"type\": \"string\"\n },\n \"ConsentSubject\": {\n \"$anchor\": \"consentSubject\",\n \"additionalProperties\": false,\n \"description\": \"Platform-agnostic identifier of what consent is about. Deliberately not a platform address: a consent record that named one would be a stored directory of who the subject talks to, readable by anyone who reaches the store.\",\n \"properties\": {\n \"agent\": {\n \"description\": \"DID of the agent the decision is about. Consent is granted to one agent, not to the service that hosts it, so that revoking one agent's access does not revoke another's.\",\n \"pattern\": \"^did:\",\n \"type\": \"string\"\n },\n \"conversationRef\": {\n \"description\": \"The bridge's OPAQUE handle for the conversation. MUST NOT be a raw platform address (a phone number, a handle, a group invite link). The approver identifies the conversation from displayHint; this member exists so a decision can be matched to a conversation without the matching key being personal data.\",\n \"minLength\": 1,\n \"type\": \"string\"\n },\n \"kind\": {\n \"$ref\": \"#/$defs/ConsentKind\"\n },\n \"platform\": {\n \"description\": \"Messaging platform the conversation lives on, as the bridge names it (for example `signal`, `whatsapp`). Opaque to the framework.\",\n \"minLength\": 1,\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"platform\",\n \"conversationRef\",\n \"kind\",\n \"agent\"\n ],\n \"title\": \"ConsentSubject\",\n \"type\": \"object\"\n },\n \"Ext\": {\n \"additionalProperties\": true,\n \"description\": \"Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.\",\n \"minProperties\": 1,\n \"propertyNames\": {\n \"pattern\": \"^[a-z][a-z0-9-]*(\\\\.[a-z0-9-]+)+$\"\n },\n \"title\": \"Ext\",\n \"type\": \"object\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/consent/approve-request/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"description\": \"The prompt an agent's home service sends to a designated approver, asking a human to decide whether an agent may act on one conversation. It carries no decision itself: the approver answers with a separate, separately-signed consent/decision. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.\",\n \"properties\": {\n \"challenge\": {\n \"description\": \"Single-use, unpredictable value the approver MUST echo in the consent/decision it signs. This is what binds a decision to this request: without it a decision is a free-floating assertion that the approver consented to something, and cannot be shown to answer this prompt rather than an earlier one. Consumers MUST reject a decision whose challenge they did not issue, and MUST NOT accept the same challenge twice.\",\n \"minLength\": 1,\n \"type\": \"string\"\n },\n \"displayHint\": {\n \"description\": \"Human-readable label for the conversation, for the approver's screen — \\\"Signal group 'Family'\\\". Present because conversationRef is deliberately opaque: without a hint the approver is asked to decide about an identifier that means nothing to them, and will either always allow or always deny. Advisory only. It is chosen by the requesting party, so a renderer MUST treat it as untrusted text — escape it, bound its length, and never let it displace the subject members above as the basis of the decision.\",\n \"maxLength\": 256,\n \"type\": \"string\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\",\n \"description\": \"Ecosystem-defined extension members per SPEC.md §4.5.1.\"\n },\n \"firstMessageDigest\": {\n \"description\": \"Digest of the inbound message that prompted this request, when there is one, so an approver deciding on a receive scope can confirm the decision it is being asked to make corresponds to a real arriving message. A digest rather than the content: the approver is deciding whether the agent may read the conversation, and showing them the message in order to ask would disclose what the decision is meant to gate.\",\n \"minLength\": 1,\n \"type\": \"string\"\n },\n \"scope\": {\n \"$ref\": \"#/$defs/ConsentScope\",\n \"description\": \"What the agent is asking to be allowed to do.\"\n },\n \"subject\": {\n \"$ref\": \"#/$defs/ConsentSubject\",\n \"description\": \"What the decision is about: one conversation, for one agent.\"\n }\n },\n \"required\": [\n \"subject\",\n \"scope\",\n \"challenge\"\n ],\n \"title\": \"Consent — Approve Request — payload\",\n \"type\": \"object\"\n}\n",
);
}
#[cfg(test)]
mod conformance {
//! Round-trip tests harvested from the spec's `spec.md`,
//! plus a `rejects_invalid_examples` test for any fixtures
//! in `payload.invalid-examples.json` (validate feature).
#[test]
fn request_example_1() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-000000000001\",\n \"type\": \"https://trusttasks.org/spec/consent/approve-request/0.1\",\n \"issuer\": \"did:example:producer\",\n \"recipient\": \"did:example:recipient\",\n \"issuedAt\": \"2026-01-01T00:00:00Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000000ff\",\n \"payload\": {\n \"subject\": {\n \"platform\": \"signal\",\n \"conversationRef\": \"9f2c1a7e-4d31-4a2b-9c88-0e6b1d5f3a44\",\n \"kind\": \"group\",\n \"agent\": \"did:example:agent\"\n },\n \"scope\": \"converse\",\n \"challenge\": \"0e3f5a91c7b24d8e\",\n \"displayHint\": \"Signal group 'Family'\",\n \"firstMessageDigest\": \"zQmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco\"\n },\n \"proof\": {\n \"type\": \"DataIntegrityProof\",\n \"cryptosuite\": \"eddsa-jcs-2022\",\n \"created\": \"2026-01-01T00:00:00Z\",\n \"verificationMethod\": \"did:example:producer#key-0\",\n \"proofPurpose\": \"assertionMethod\",\n \"proofValue\": \"z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz\"\n }\n}\n";
let doc: crate::TrustTask<super::Payload> =
serde_json::from_str(JSON).expect("deserialize request example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "request example failed round-trip");
}
}