Skip to main content

treeship_core/
agent.rs

1//! Agent Identity Certificate schema.
2//!
3//! An Agent Identity Certificate is a signed credential that proves who an
4//! agent is and what it is authorized to do. Produced once when an agent
5//! registers, lives permanently with the agent. The TLS certificate
6//! equivalent for AI agents.
7
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
9use ed25519_dalek::{Signature as DalekSignature, VerifyingKey};
10use serde::{Deserialize, Serialize};
11
12/// Agent identity: who the agent is.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AgentIdentity {
15    pub agent_name: String,
16    pub ship_id: String,
17    pub public_key: String,
18    pub issuer: String,
19    pub issued_at: String,
20    pub valid_until: String,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub model: Option<String>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub description: Option<String>,
25}
26
27/// Agent capabilities: what tools and services the agent is authorized to use.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct AgentCapabilities {
30    /// Authorized MCP tool names.
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub tools: Vec<ToolCapability>,
33    /// Authorized API endpoints.
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub api_endpoints: Vec<String>,
36    /// Authorized MCP server names.
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub mcp_servers: Vec<String>,
39}
40
41/// A single authorized tool with optional description.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ToolCapability {
44    pub name: String,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub description: Option<String>,
47}
48
49/// Agent declaration: scope constraints.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AgentDeclaration {
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub bounded_actions: Vec<String>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub forbidden: Vec<String>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub escalation_required: Vec<String>,
58    /// Network destinations the agent may reach: exact hosts or `*.suffix`
59    /// patterns. Empty means no network scope is declared, not "no network".
60    /// Omitted from the canonical bytes when empty, so every certificate
61    /// signed before this field existed keeps its exact signature.
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub network: Vec<String>,
64}
65
66/// The complete Agent Certificate -- identity + capabilities + declaration
67/// with a signature over the canonical JSON of all three.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AgentCertificate {
70    pub r#type: String, // "treeship/agent-certificate/v1"
71    /// Schema version. Absent on pre-v0.9.0 certificates (treated as "0").
72    /// Set to "1" for v0.9.0+. Informational only in v0.9.0; future versions
73    /// may use this to gate verification rule selection.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub schema_version: Option<String>,
76    pub identity: AgentIdentity,
77    pub capabilities: AgentCapabilities,
78    pub declaration: AgentDeclaration,
79    pub signature: CertificateSignature,
80}
81
82/// Signature over the certificate content.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct CertificateSignature {
85    pub algorithm: String, // "ed25519"
86    pub key_id: String,
87    pub public_key: String,    // base64url-encoded Ed25519 public key
88    pub signature: String,     // base64url-encoded Ed25519 signature
89    pub signed_fields: String, // "identity+capabilities+declaration"
90}
91
92pub const CERTIFICATE_TYPE: &str = "treeship/agent-certificate/v1";
93
94/// Current certificate schema version. Certificates without this field are
95/// treated as schema "0" and verified under legacy rules (pre-v0.9.0 shape).
96pub const CERTIFICATE_SCHEMA_VERSION: &str = "1";
97
98/// Resolve a schema_version Option to its effective string, defaulting to
99/// "0" when absent. Centralizing this avoids the legacy default leaking out
100/// across call sites.
101pub fn effective_schema_version(field: Option<&str>) -> &str {
102    field.unwrap_or("0")
103}
104
105/// Errors verifying an `AgentCertificate` signature.
106#[derive(Debug)]
107pub enum CertificateVerifyError {
108    /// Public key in `signature.public_key` was not valid base64url or wrong length.
109    BadPublicKey(String),
110    /// Signature bytes were not valid base64url or wrong length.
111    BadSignature(String),
112    /// Could not reconstruct canonical signed payload.
113    PayloadEncode(String),
114    /// Signature did not verify against the embedded public key.
115    InvalidSignature,
116    /// Signature algorithm is not supported (only `ed25519` is recognized).
117    UnsupportedAlgorithm(String),
118    /// `signed_fields` does not name the expected payload composition.
119    UnsupportedSignedFields(String),
120    /// The embedded `signature.public_key` is not pinned in the
121    /// operator's trust root store under kind `AgentCert`. The signature
122    /// math may be internally consistent, but the issuer is unknown.
123    /// Self-signed certificates an attacker mints to authorize their own
124    /// agent's tool calls land here.
125    UntrustedIssuer { key_id: String },
126    /// No trust roots configured at all (or none for kind `AgentCert`).
127    /// Distinct from `UntrustedIssuer` so the CLI can render the
128    /// "configure trust" remediation rather than "key not in store".
129    NoTrustConfigured,
130}
131
132impl std::fmt::Display for CertificateVerifyError {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            Self::BadPublicKey(s) => write!(f, "certificate public key: {s}"),
136            Self::BadSignature(s) => write!(f, "certificate signature bytes: {s}"),
137            Self::PayloadEncode(s) => write!(f, "certificate canonical encoding: {s}"),
138            Self::InvalidSignature => write!(f, "certificate signature did not verify"),
139            Self::UnsupportedAlgorithm(s) => write!(f, "certificate algorithm '{s}' not supported"),
140            Self::UnsupportedSignedFields(s) => {
141                write!(f, "certificate signed_fields '{s}' not recognized")
142            }
143            Self::UntrustedIssuer { key_id } => write!(
144                f,
145                "certificate issuer (key_id={key_id}) is not in the trust root store. \
146                 Run `treeship trust add <key_id> <pubkey> --kind agent_cert` if you trust this issuer.",
147            ),
148            Self::NoTrustConfigured => write!(
149                f,
150                "no trust roots configured for agent certificates. \
151                 Run `treeship trust add <key_id> <pubkey> --kind agent_cert` \
152                 or sync from your hub via `treeship hub sync-trust`.",
153            ),
154        }
155    }
156}
157
158impl std::error::Error for CertificateVerifyError {}
159
160/// The signed payload composition used by v0.x agent certificates.
161const SIGNED_FIELDS_V1: &str = "identity+capabilities+declaration";
162
163/// Verify the Ed25519 signature on an `AgentCertificate`. Requires the
164/// embedded `signature.public_key` to be present in `trust` under kind
165/// `AgentCert`, then reconstructs the canonical JSON the issuer signed
166/// and verifies the signature bytes match.
167///
168/// Trust pinning is mandatory. Before this, the function trusted whichever
169/// public key the certificate happened to carry -- making the certificate
170/// self-signed. With it, an operator pins which issuer keys are allowed to
171/// vouch for agents, and any other issuer's certificate is rejected with
172/// `UntrustedIssuer` (or `NoTrustConfigured` when the store has nothing
173/// for kind `AgentCert`).
174///
175/// This does NOT check certificate validity windows (issued_at /
176/// valid_until). That's the cross-verifier's job.
177pub fn verify_certificate(
178    cert: &AgentCertificate,
179    trust: &crate::trust::TrustRootStore,
180) -> Result<(), CertificateVerifyError> {
181    use crate::trust::TrustRootKind;
182
183    if cert.signature.algorithm != "ed25519" {
184        return Err(CertificateVerifyError::UnsupportedAlgorithm(
185            cert.signature.algorithm.clone(),
186        ));
187    }
188    if cert.signature.signed_fields != SIGNED_FIELDS_V1 {
189        return Err(CertificateVerifyError::UnsupportedSignedFields(
190            cert.signature.signed_fields.clone(),
191        ));
192    }
193
194    let pk_bytes = URL_SAFE_NO_PAD
195        .decode(&cert.signature.public_key)
196        .map_err(|e| CertificateVerifyError::BadPublicKey(e.to_string()))?;
197    let pk_arr: [u8; 32] = pk_bytes.as_slice().try_into().map_err(|_| {
198        CertificateVerifyError::BadPublicKey(format!("expected 32 bytes, got {}", pk_bytes.len()))
199    })?;
200    let verifying_key = VerifyingKey::from_bytes(&pk_arr)
201        .map_err(|e| CertificateVerifyError::BadPublicKey(e.to_string()))?;
202
203    // Trust pin -- fail-closed before signature math runs. We
204    // distinguish "no trust configured at all (for this kind)" from
205    // "trust configured but this issuer isn't in it" so the CLI can
206    // render a more useful remediation.
207    if !trust.contains(&verifying_key, TrustRootKind::AgentCert) {
208        if trust.is_empty_for_kind(TrustRootKind::AgentCert) {
209            return Err(CertificateVerifyError::NoTrustConfigured);
210        }
211        return Err(CertificateVerifyError::UntrustedIssuer {
212            key_id: cert.signature.key_id.clone(),
213        });
214    }
215
216    let sig_bytes = URL_SAFE_NO_PAD
217        .decode(&cert.signature.signature)
218        .map_err(|e| CertificateVerifyError::BadSignature(e.to_string()))?;
219    let sig_arr: [u8; 64] = sig_bytes.as_slice().try_into().map_err(|_| {
220        CertificateVerifyError::BadSignature(format!("expected 64 bytes, got {}", sig_bytes.len()))
221    })?;
222    let signature = DalekSignature::from_bytes(&sig_arr);
223
224    // Reconstruct the canonical signed payload exactly as the issuer did:
225    // {identity, capabilities, declaration} serialized with serde_json (which
226    // preserves struct field declaration order).
227    let payload = serde_json::json!({
228        "identity": cert.identity,
229        "capabilities": cert.capabilities,
230        "declaration": cert.declaration,
231    });
232    let canonical = serde_json::to_vec(&payload)
233        .map_err(|e| CertificateVerifyError::PayloadEncode(e.to_string()))?;
234
235    verifying_key
236        .verify_strict(&canonical, &signature)
237        .map_err(|_| CertificateVerifyError::InvalidSignature)
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    fn sample_certificate(schema_version: Option<&str>) -> AgentCertificate {
245        AgentCertificate {
246            r#type: CERTIFICATE_TYPE.into(),
247            schema_version: schema_version.map(|s| s.to_string()),
248            identity: AgentIdentity {
249                agent_name: "agent-007".into(),
250                ship_id: "ship_demo".into(),
251                public_key: "pk_b64".into(),
252                issuer: "ship://ship_demo".into(),
253                issued_at: "2026-04-15T00:00:00Z".into(),
254                valid_until: "2026-10-15T00:00:00Z".into(),
255                model: None,
256                description: None,
257            },
258            capabilities: AgentCapabilities {
259                tools: vec![ToolCapability {
260                    name: "Bash".into(),
261                    description: None,
262                }],
263                api_endpoints: vec![],
264                mcp_servers: vec![],
265            },
266            declaration: AgentDeclaration {
267                bounded_actions: vec!["Bash".into()],
268                forbidden: vec![],
269                escalation_required: vec![],
270                network: Vec::new(),
271            },
272            signature: CertificateSignature {
273                algorithm: "ed25519".into(),
274                key_id: "key_demo".into(),
275                public_key: "pk_b64".into(),
276                signature: "sig_b64".into(),
277                signed_fields: "identity+capabilities+declaration".into(),
278            },
279        }
280    }
281
282    #[test]
283    fn legacy_certificate_round_trips_byte_identical() {
284        // schema_version=None mimics a pre-v0.9.0 certificate. Re-serializing
285        // must skip the field entirely so the original bytes (and therefore
286        // any signature over those bytes if a future format binds them) is
287        // preserved.
288        let cert = sample_certificate(None);
289        let bytes = serde_json::to_vec(&cert).unwrap();
290        let s = std::str::from_utf8(&bytes).unwrap();
291        assert!(
292            !s.contains("schema_version"),
293            "legacy cert must omit schema_version, got: {s}"
294        );
295
296        let parsed: AgentCertificate = serde_json::from_slice(&bytes).unwrap();
297        assert!(parsed.schema_version.is_none());
298        let reserialized = serde_json::to_vec(&parsed).unwrap();
299        assert_eq!(bytes, reserialized);
300        assert_eq!(
301            effective_schema_version(parsed.schema_version.as_deref()),
302            "0"
303        );
304    }
305
306    /// Build a single-entry trust store pinning `pk_b64` for kind
307    /// `AgentCert`. Tests that exercise valid signatures use this so the
308    /// trust pin doesn't short-circuit before signature verification.
309    fn trust_with(pk_b64: &str) -> crate::trust::TrustRootStore {
310        use crate::trust::{TrustRoot, TrustRootKind, TrustRootStore};
311        TrustRootStore::with_roots(vec![TrustRoot {
312            key_id: "key_demo".into(),
313            public_key: format!("ed25519:{pk_b64}"),
314            kind: TrustRootKind::AgentCert,
315            label: "test issuer".into(),
316            added_at: "2026-05-15T00:00:00Z".into(),
317        }])
318    }
319
320    #[test]
321    fn verify_certificate_round_trip() {
322        // Mint a cert, sign it the way the CLI does, then call verify.
323        use crate::attestation::{Ed25519Signer, Signer};
324        let signer = Ed25519Signer::generate("key_demo").unwrap();
325        let pk_b64 = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
326
327        let identity = AgentIdentity {
328            agent_name: "agent-007".into(),
329            ship_id: "ship_x".into(),
330            public_key: pk_b64.clone(),
331            issuer: "ship://ship_x".into(),
332            issued_at: "2026-04-15T00:00:00Z".into(),
333            valid_until: "2027-04-15T00:00:00Z".into(),
334            model: None,
335            description: None,
336        };
337        let capabilities = AgentCapabilities {
338            tools: vec![ToolCapability {
339                name: "Bash".into(),
340                description: None,
341            }],
342            api_endpoints: vec![],
343            mcp_servers: vec![],
344        };
345        let declaration = AgentDeclaration {
346            bounded_actions: vec!["Bash".into()],
347            forbidden: vec![],
348            escalation_required: vec![],
349            network: Vec::new(),
350        };
351        let payload = serde_json::json!({
352            "identity": identity, "capabilities": capabilities, "declaration": declaration,
353        });
354        let canonical = serde_json::to_vec(&payload).unwrap();
355        let sig = signer.sign(&canonical).unwrap();
356
357        let cert = AgentCertificate {
358            r#type: CERTIFICATE_TYPE.into(),
359            schema_version: Some(CERTIFICATE_SCHEMA_VERSION.into()),
360            identity,
361            capabilities,
362            declaration,
363            signature: CertificateSignature {
364                algorithm: "ed25519".into(),
365                key_id: "key_demo".into(),
366                public_key: pk_b64.clone(),
367                signature: URL_SAFE_NO_PAD.encode(sig),
368                signed_fields: "identity+capabilities+declaration".into(),
369            },
370        };
371
372        let trust = trust_with(&pk_b64);
373        verify_certificate(&cert, &trust).expect("freshly-signed cert must verify");
374    }
375
376    #[test]
377    fn verify_certificate_detects_tampered_payload() {
378        use crate::attestation::{Ed25519Signer, Signer};
379        let signer = Ed25519Signer::generate("key_demo").unwrap();
380        let pk_b64 = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
381
382        let identity = AgentIdentity {
383            agent_name: "agent-007".into(),
384            ship_id: "ship_x".into(),
385            public_key: pk_b64.clone(),
386            issuer: "ship://ship_x".into(),
387            issued_at: "2026-04-15T00:00:00Z".into(),
388            valid_until: "2027-04-15T00:00:00Z".into(),
389            model: None,
390            description: None,
391        };
392        let capabilities = AgentCapabilities {
393            tools: vec![ToolCapability {
394                name: "Bash".into(),
395                description: None,
396            }],
397            api_endpoints: vec![],
398            mcp_servers: vec![],
399        };
400        let declaration = AgentDeclaration {
401            bounded_actions: vec!["Bash".into()],
402            forbidden: vec![],
403            escalation_required: vec![],
404            network: Vec::new(),
405        };
406        let payload = serde_json::json!({
407            "identity": identity, "capabilities": capabilities, "declaration": declaration,
408        });
409        let canonical = serde_json::to_vec(&payload).unwrap();
410        let sig = signer.sign(&canonical).unwrap();
411
412        // Tamper: expand the tools list AFTER signing. Signature was computed
413        // over the smaller list so it should no longer verify.
414        let evil_caps = AgentCapabilities {
415            tools: vec![
416                ToolCapability {
417                    name: "Bash".into(),
418                    description: None,
419                },
420                ToolCapability {
421                    name: "DropDatabase".into(),
422                    description: None,
423                },
424            ],
425            api_endpoints: vec![],
426            mcp_servers: vec![],
427        };
428
429        let cert = AgentCertificate {
430            r#type: CERTIFICATE_TYPE.into(),
431            schema_version: Some(CERTIFICATE_SCHEMA_VERSION.into()),
432            identity,
433            capabilities: evil_caps,
434            declaration,
435            signature: CertificateSignature {
436                algorithm: "ed25519".into(),
437                key_id: "key_demo".into(),
438                public_key: pk_b64.clone(),
439                signature: URL_SAFE_NO_PAD.encode(sig),
440                signed_fields: "identity+capabilities+declaration".into(),
441            },
442        };
443
444        let trust = trust_with(&pk_b64);
445        let err = verify_certificate(&cert, &trust).unwrap_err();
446        assert!(
447            matches!(err, CertificateVerifyError::InvalidSignature),
448            "expected InvalidSignature, got: {err}"
449        );
450    }
451
452    #[test]
453    fn verify_certificate_rejects_unsupported_algorithm() {
454        let mut cert = sample_certificate(Some(CERTIFICATE_SCHEMA_VERSION));
455        cert.signature.algorithm = "rsa-pss-sha256".into();
456        let err = verify_certificate(&cert, &crate::trust::TrustRootStore::empty()).unwrap_err();
457        assert!(matches!(
458            err,
459            CertificateVerifyError::UnsupportedAlgorithm(_)
460        ));
461    }
462
463    /// Trust pin headline: a freshly-signed cert whose issuer key is
464    /// NOT in the operator's trust store must be rejected with
465    /// `UntrustedIssuer` -- even though the signature math is fine.
466    #[test]
467    fn verify_certificate_rejects_unknown_issuer() {
468        use crate::attestation::{Ed25519Signer, Signer};
469        let signer = Ed25519Signer::generate("key_attacker").unwrap();
470        let pk_b64 = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
471
472        let identity = AgentIdentity {
473            agent_name: "agent-007".into(),
474            ship_id: "ship_x".into(),
475            public_key: pk_b64.clone(),
476            issuer: "ship://attacker-claims-zerker".into(),
477            issued_at: "2026-04-15T00:00:00Z".into(),
478            valid_until: "2027-04-15T00:00:00Z".into(),
479            model: None,
480            description: None,
481        };
482        let capabilities = AgentCapabilities {
483            tools: vec![ToolCapability {
484                name: "Bash".into(),
485                description: None,
486            }],
487            api_endpoints: vec![],
488            mcp_servers: vec![],
489        };
490        let declaration = AgentDeclaration {
491            bounded_actions: vec!["Bash".into()],
492            forbidden: vec![],
493            escalation_required: vec![],
494            network: Vec::new(),
495        };
496        let payload = serde_json::json!({
497            "identity": identity, "capabilities": capabilities, "declaration": declaration,
498        });
499        let sig = signer.sign(&serde_json::to_vec(&payload).unwrap()).unwrap();
500        let cert = AgentCertificate {
501            r#type: CERTIFICATE_TYPE.into(),
502            schema_version: Some(CERTIFICATE_SCHEMA_VERSION.into()),
503            identity,
504            capabilities,
505            declaration,
506            signature: CertificateSignature {
507                algorithm: "ed25519".into(),
508                key_id: "key_attacker".into(),
509                public_key: pk_b64,
510                signature: URL_SAFE_NO_PAD.encode(sig),
511                signed_fields: "identity+capabilities+declaration".into(),
512            },
513        };
514
515        // Trust an unrelated issuer.
516        let honest = Ed25519Signer::generate("honest_issuer").unwrap();
517        let honest_pk = URL_SAFE_NO_PAD.encode(honest.public_key_bytes());
518        let trust = trust_with(&honest_pk);
519
520        let err = verify_certificate(&cert, &trust).unwrap_err();
521        assert!(
522            matches!(err, CertificateVerifyError::UntrustedIssuer { .. }),
523            "expected UntrustedIssuer, got: {err}"
524        );
525    }
526
527    /// Empty trust store yields `NoTrustConfigured` so the CLI can
528    /// render the install-time remediation distinct from "this
529    /// particular key is wrong".
530    #[test]
531    fn verify_certificate_rejects_with_no_trust_configured() {
532        use crate::attestation::{Ed25519Signer, Signer};
533        let signer = Ed25519Signer::generate("key_demo").unwrap();
534        let pk_b64 = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
535
536        let identity = AgentIdentity {
537            agent_name: "agent-007".into(),
538            ship_id: "ship_x".into(),
539            public_key: pk_b64.clone(),
540            issuer: "ship://ship_x".into(),
541            issued_at: "2026-04-15T00:00:00Z".into(),
542            valid_until: "2027-04-15T00:00:00Z".into(),
543            model: None,
544            description: None,
545        };
546        let capabilities = AgentCapabilities {
547            tools: vec![ToolCapability {
548                name: "Bash".into(),
549                description: None,
550            }],
551            api_endpoints: vec![],
552            mcp_servers: vec![],
553        };
554        let declaration = AgentDeclaration {
555            bounded_actions: vec!["Bash".into()],
556            forbidden: vec![],
557            escalation_required: vec![],
558            network: Vec::new(),
559        };
560        let payload = serde_json::json!({
561            "identity": identity, "capabilities": capabilities, "declaration": declaration,
562        });
563        let sig = signer.sign(&serde_json::to_vec(&payload).unwrap()).unwrap();
564        let cert = AgentCertificate {
565            r#type: CERTIFICATE_TYPE.into(),
566            schema_version: Some(CERTIFICATE_SCHEMA_VERSION.into()),
567            identity,
568            capabilities,
569            declaration,
570            signature: CertificateSignature {
571                algorithm: "ed25519".into(),
572                key_id: "key_demo".into(),
573                public_key: pk_b64,
574                signature: URL_SAFE_NO_PAD.encode(sig),
575                signed_fields: "identity+capabilities+declaration".into(),
576            },
577        };
578
579        let err = verify_certificate(&cert, &crate::trust::TrustRootStore::empty()).unwrap_err();
580        assert!(
581            matches!(err, CertificateVerifyError::NoTrustConfigured),
582            "expected NoTrustConfigured, got: {err}"
583        );
584        // And the error must reference the CLI remediation.
585        let msg = format!("{err}");
586        assert!(
587            msg.contains("treeship trust add"),
588            "remediation must mention treeship trust add: {msg}"
589        );
590    }
591
592    #[test]
593    fn current_certificate_carries_schema_version_one() {
594        let cert = sample_certificate(Some(CERTIFICATE_SCHEMA_VERSION));
595        let bytes = serde_json::to_vec(&cert).unwrap();
596        let s = std::str::from_utf8(&bytes).unwrap();
597        assert!(
598            s.contains(r#""schema_version":"1""#),
599            "current cert must include schema_version=1, got: {s}"
600        );
601        let parsed: AgentCertificate = serde_json::from_slice(&bytes).unwrap();
602        assert_eq!(
603            effective_schema_version(parsed.schema_version.as_deref()),
604            "1"
605        );
606    }
607}