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 #[cfg(feature = "vertex-skill-registry")]
30 #[error("skill registry request failed: {0}")]
31 Registry(Box<adk_core::AdkError>),
32
33 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}