Skip to main content

adk_skill/
error.rs

1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4pub enum SkillError {
5    #[error("io error: {0}")]
6    Io(#[from] std::io::Error),
7
8    #[error("yaml parse error: {0}")]
9    Yaml(#[from] serde_yaml::Error),
10
11    #[error("invalid skill frontmatter in {path}: {message}")]
12    InvalidFrontmatter { path: PathBuf, message: String },
13
14    #[error("missing required field `{field}` in {path}")]
15    MissingField { path: PathBuf, field: &'static str },
16
17    #[error("invalid skills root, expected directory: {0}")]
18    InvalidSkillsRoot(PathBuf),
19
20    #[error("skill validation error: {0}")]
21    Validation(String),
22
23    #[error("index error: {0}")]
24    IndexError(String),
25
26    // ===== Skill Registry payloads (feature `vertex-skill-registry`) =====
27    /// A Skill Registry request failed; the inner error carries the
28    /// `skill.registry.*` code and category.
29    #[cfg(feature = "vertex-skill-registry")]
30    #[error("skill registry request failed: {0}")]
31    Registry(Box<adk_core::AdkError>),
32
33    /// The `zippedFilesystem` field was not valid standard base64.
34    #[cfg(feature = "vertex-skill-registry")]
35    #[error(
36        "skill registry payload is not valid base64: {message}. The zippedFilesystem field must be standard base64; re-fetch the skill or report the registry entry"
37    )]
38    RegistryPayloadDecode { message: String },
39
40    /// The decoded zip did not match the registry's `sha256` field.
41    #[cfg(feature = "vertex-skill-registry")]
42    #[error(
43        "skill registry payload sha256 mismatch: expected {expected}, computed {actual}. The payload may be corrupt or tampered with; re-fetch the skill before using its contents"
44    )]
45    RegistryChecksumMismatch { expected: String, actual: String },
46
47    /// The payload is not a well-formed zip archive.
48    #[cfg(feature = "vertex-skill-registry")]
49    #[error(
50        "skill archive is not a valid zip: {message}. The registry serves SKILL.md packages as zip archives; re-fetch the skill or report the registry entry"
51    )]
52    ArchiveFormat { message: String },
53
54    /// The archive exceeds the maximum accepted size.
55    #[cfg(feature = "vertex-skill-registry")]
56    #[error(
57        "skill archive is {size} bytes, exceeding the {limit}-byte limit. The registry rejects archives over this size; do not extract oversized payloads"
58    )]
59    ArchiveTooLarge { size: u64, limit: u64 },
60
61    /// The archive contains more entries than allowed.
62    #[cfg(feature = "vertex-skill-registry")]
63    #[error(
64        "skill archive contains {count} entries, exceeding the limit of {limit}. Split the skill into smaller packages or remove unneeded files"
65    )]
66    ArchiveTooManyEntries { count: usize, limit: usize },
67
68    /// An entry path contains a `..` component.
69    #[cfg(feature = "vertex-skill-registry")]
70    #[error(
71        "skill archive entry `{name}` contains a `..` path component. Path traversal is rejected; all entries must resolve inside the extraction root"
72    )]
73    ArchivePathTraversal { name: String },
74
75    /// An entry path is absolute.
76    #[cfg(feature = "vertex-skill-registry")]
77    #[error(
78        "skill archive entry `{name}` is an absolute path. Entries must be relative so extraction stays inside the target directory"
79    )]
80    ArchiveAbsolutePath { name: String },
81
82    /// An entry is a symbolic link.
83    #[cfg(feature = "vertex-skill-registry")]
84    #[error(
85        "skill archive entry `{name}` is a symlink. Symlinks are rejected because they can escape the extraction root"
86    )]
87    ArchiveSymlink { name: String },
88
89    /// Two entries share the same normalized name.
90    #[cfg(feature = "vertex-skill-registry")]
91    #[error(
92        "skill archive contains duplicate entry `{name}`. Duplicate names are rejected because later entries would silently overwrite earlier ones"
93    )]
94    ArchiveDuplicateEntry { name: String },
95
96    /// The declared uncompressed total exceeds the limit.
97    #[cfg(feature = "vertex-skill-registry")]
98    #[error(
99        "skill archive declares {total} uncompressed bytes, exceeding the {limit}-byte limit. Refusing to extract to avoid resource exhaustion"
100    )]
101    ArchiveUncompressedTooLarge { total: u64, limit: u64 },
102
103    /// An entry's compression ratio exceeds the limit (zip-bomb defense).
104    #[cfg(feature = "vertex-skill-registry")]
105    #[error(
106        "skill archive entry `{name}` has compression ratio {ratio}, exceeding the limit of {limit}. Highly compressed entries are rejected as potential zip bombs"
107    )]
108    ArchiveCompressionRatio { name: String, ratio: u64, limit: u64 },
109
110    /// An entry nests deeper than the allowed directory depth.
111    #[cfg(feature = "vertex-skill-registry")]
112    #[error(
113        "skill archive entry `{name}` nests {depth} directory levels, exceeding the limit of {limit}. Flatten the skill's directory layout"
114    )]
115    ArchiveDepthExceeded { name: String, depth: usize, limit: usize },
116}
117
118pub type SkillResult<T> = Result<T, SkillError>;
119
120#[cfg(feature = "vertex-skill-registry")]
121impl From<adk_core::AdkError> for SkillError {
122    fn from(err: adk_core::AdkError) -> Self {
123        SkillError::Registry(Box::new(err))
124    }
125}
126
127impl From<SkillError> for adk_core::AdkError {
128    fn from(err: SkillError) -> Self {
129        use adk_core::{ErrorCategory, ErrorComponent};
130        // Registry errors already carry their component, category, and
131        // `skill.registry.*` code; pass them through unchanged.
132        #[cfg(feature = "vertex-skill-registry")]
133        let err = match err {
134            SkillError::Registry(inner) => return *inner,
135            other => other,
136        };
137        let (category, code) = match &err {
138            SkillError::Io(_) => (ErrorCategory::Internal, "skill.io"),
139            SkillError::Yaml(_) => (ErrorCategory::InvalidInput, "skill.yaml_parse"),
140            SkillError::InvalidFrontmatter { .. } => {
141                (ErrorCategory::InvalidInput, "skill.invalid_frontmatter")
142            }
143            SkillError::MissingField { .. } => (ErrorCategory::InvalidInput, "skill.missing_field"),
144            SkillError::InvalidSkillsRoot(_) => {
145                (ErrorCategory::NotFound, "skill.invalid_skills_root")
146            }
147            SkillError::Validation(_) => (ErrorCategory::InvalidInput, "skill.validation"),
148            SkillError::IndexError(_) => (ErrorCategory::Internal, "skill.index"),
149            #[cfg(feature = "vertex-skill-registry")]
150            SkillError::Registry(_) => unreachable!("Registry errors return early above"),
151            // Decode and checksum failures are corrupt upstream payloads;
152            // archive-rule violations are rejected input data.
153            #[cfg(feature = "vertex-skill-registry")]
154            SkillError::RegistryPayloadDecode { .. } => {
155                (ErrorCategory::Internal, "skill.registry.payload_decode")
156            }
157            #[cfg(feature = "vertex-skill-registry")]
158            SkillError::RegistryChecksumMismatch { .. } => {
159                (ErrorCategory::Internal, "skill.registry.checksum_mismatch")
160            }
161            #[cfg(feature = "vertex-skill-registry")]
162            SkillError::ArchiveFormat { .. } => {
163                (ErrorCategory::InvalidInput, "skill.archive.invalid_format")
164            }
165            #[cfg(feature = "vertex-skill-registry")]
166            SkillError::ArchiveTooLarge { .. } => {
167                (ErrorCategory::InvalidInput, "skill.archive.too_large")
168            }
169            #[cfg(feature = "vertex-skill-registry")]
170            SkillError::ArchiveTooManyEntries { .. } => {
171                (ErrorCategory::InvalidInput, "skill.archive.too_many_entries")
172            }
173            #[cfg(feature = "vertex-skill-registry")]
174            SkillError::ArchivePathTraversal { .. } => {
175                (ErrorCategory::InvalidInput, "skill.archive.path_traversal")
176            }
177            #[cfg(feature = "vertex-skill-registry")]
178            SkillError::ArchiveAbsolutePath { .. } => {
179                (ErrorCategory::InvalidInput, "skill.archive.absolute_path")
180            }
181            #[cfg(feature = "vertex-skill-registry")]
182            SkillError::ArchiveSymlink { .. } => {
183                (ErrorCategory::InvalidInput, "skill.archive.symlink")
184            }
185            #[cfg(feature = "vertex-skill-registry")]
186            SkillError::ArchiveDuplicateEntry { .. } => {
187                (ErrorCategory::InvalidInput, "skill.archive.duplicate_entry")
188            }
189            #[cfg(feature = "vertex-skill-registry")]
190            SkillError::ArchiveUncompressedTooLarge { .. } => {
191                (ErrorCategory::InvalidInput, "skill.archive.uncompressed_too_large")
192            }
193            #[cfg(feature = "vertex-skill-registry")]
194            SkillError::ArchiveCompressionRatio { .. } => {
195                (ErrorCategory::InvalidInput, "skill.archive.compression_ratio")
196            }
197            #[cfg(feature = "vertex-skill-registry")]
198            SkillError::ArchiveDepthExceeded { .. } => {
199                (ErrorCategory::InvalidInput, "skill.archive.depth_exceeded")
200            }
201        };
202        adk_core::AdkError::new(ErrorComponent::Tool, category, code, err.to_string())
203            .with_source(err)
204    }
205}