1use std::path::PathBuf;
4
5use thiserror::Error;
6
7#[derive(Debug, Error)]
9pub enum WorkspaceError {
10 #[error(
11 "forge.toml not found at {}; create it with at least one [[orgs]] entry containing one [[orgs.projects]] entry",
12 path.display()
13 )]
14 ConfigMissing { path: PathBuf },
15
16 #[error("forge.toml at {} failed to parse{at}: {message}", path.display())]
23 ConfigParse { path: PathBuf, at: String, message: String },
24
25 #[error("forge.toml at {} is otherwise invalid: {message}", path.display())]
26 ConfigInvalid { path: PathBuf, message: String },
27
28 #[error("no [[orgs]] entries in forge.toml at {}", path.display())]
29 NoOrgsConfigured { path: PathBuf },
30
31 #[error("no [[orgs.projects]] entries across any org in forge.toml at {}", path.display())]
32 NoProjectsConfigured { path: PathBuf },
33
34 #[error("no [[accounts]] entries in forge.toml at {}", path.display())]
35 NoAccountsConfigured { path: PathBuf },
36
37 #[error("duplicate account display_name '{name}' in forge.toml at {}", path.display())]
38 DuplicateAccount { path: PathBuf, name: String },
39
40 #[error(
41 "accounts missing provider in forge.toml at {}: {}. Each needs one of {}. If a line is \
42 present, check it sits above that account's [accounts.env] table - below it TOML reads \
43 it as an env key",
44 path.display(),
45 names.join(", "),
46 forge_primitives::account::Provider::ACCEPTED,
47 )]
48 AccountsMissingProvider { path: PathBuf, names: Vec<String> },
49
50 #[error(
51 "account '{name}' in forge.toml at {} sets provider = \"openrouter\" with an \
52 ANTHROPIC_BASE_URL that is not the API root; it must end in /api, as in \
53 https://openrouter.ai/api",
54 path.display()
55 )]
56 OpenrouterBaseUrlNotApiRoot { path: PathBuf, name: String },
57
58 #[error(
59 "project '{name}' in forge.toml at {} sets permission_mode = \"{value}\"; accepted values: {}",
60 path.display(),
61 forge_primitives::permission::PermissionMode::ACCEPTED
62 )]
63 ProjectInvalidPermissionMode { path: PathBuf, name: String, value: String },
64
65 #[error(
66 "project '{name}' in forge.toml at {} sets document_roots entry '{root}', which is not absolute; every entry must be an absolute path, ~ or start with ~/",
67 path.display()
68 )]
69 ProjectDocumentRootRelative { path: PathBuf, name: String, root: String },
70
71 #[error(
72 "gateway port 0 in forge.toml at {} is not usable; the port is fixed, not OS-assigned",
73 path.display()
74 )]
75 GatewayPortInvalid { path: PathBuf },
76
77 #[error(
78 "gateway key '{key}' = 0 in forge.toml at {} is not usable; 0 would silently disable the mechanism it configures",
79 path.display()
80 )]
81 GatewayRotationInvalid { path: PathBuf, key: &'static str },
82
83 #[error(
84 "models \"{model}\" in forge.toml at {} sets auto_compact_window = {value}, which is not usable; it must name a positive window in tokens, so remove the key to leave the CLI's own resolution in place",
85 path.display()
86 )]
87 ModelAutoCompactWindowInvalid { path: PathBuf, model: String, value: i64 },
88
89 #[error(
90 "the handoff table at {scope} in forge.toml at {} sets at_percent = {value}, which is not usable; it must name a share of the context window between 1 and 100, so remove the key for the {}% default",
91 path.display(),
92 crate::config::DEFAULT_HANDOFF_AT_PERCENT
93 )]
94 HandoffAtPercentInvalid { path: PathBuf, scope: String, value: u32 },
95
96 #[error(
97 "the handoff table at {scope} in forge.toml at {} sets after_compactions = 0, which is not usable; it must name a count of at least 1, so remove the key for the {} default",
98 path.display(),
99 crate::config::DEFAULT_HANDOFF_AFTER_COMPACTIONS
100 )]
101 HandoffAfterCompactionsInvalid { path: PathBuf, scope: String },
102
103 #[error(
104 "documents max_size_mib = {value} in forge.toml at {} is not usable; it must name a positive ceiling in MiB, so remove the key for the {} MiB default",
105 path.display(),
106 crate::config::DEFAULT_DOCUMENT_MAX_MIB
107 )]
108 DocumentCeilingInvalid { path: PathBuf, value: u64 },
109
110 #[error(
111 "documents max_size_mib = {value} in forge.toml at {} is over the wire's ceiling; a document crosses the socket base64-encoded and the desktop client's frame cap is 256 MiB, so the most a ceiling can name is {} MiB - remove the key for the {} MiB default",
112 path.display(),
113 crate::config::DOCUMENT_MAX_MIB_LIMIT,
114 crate::config::DEFAULT_DOCUMENT_MAX_MIB
115 )]
116 DocumentCeilingTooLarge { path: PathBuf, value: u64 },
117
118 #[error(
119 "[usage] poll_seconds = 0 in forge.toml at {} is not usable; it must name a positive interval in seconds, so remove the key for the {} second default",
120 path.display(),
121 crate::config::DEFAULT_USAGE_POLL_SECONDS
122 )]
123 UsagePollIntervalInvalid { path: PathBuf },
124
125 #[error(
126 "sessions.heal_attempts = 0 in forge.toml at {} is not usable; it must name at least one restart, so remove the key for the {} default",
127 path.display(),
128 crate::config::DEFAULT_HEAL_ATTEMPTS
129 )]
130 SessionsHealAttemptsInvalid { path: PathBuf },
131
132 #[error(
133 "sessions.{key} = \"{value}\" in forge.toml at {} is not a duration; write a number with its unit, as in \"5s\", \"30s\" or \"2m\"",
134 path.display()
135 )]
136 SessionsDurationInvalid { path: PathBuf, key: &'static str, value: String },
137
138 #[error(
139 "[sessions] heal_backoff in forge.toml at {} is empty; list at least one interval, or remove the key for the defaults",
140 path.display()
141 )]
142 SessionsBackoffEmpty { path: PathBuf },
143
144 #[error(
145 "server port 0 in forge.toml at {} is not usable; the socket needs a port a client can be pointed at",
146 path.display()
147 )]
148 ServerPortInvalid { path: PathBuf },
149
150 #[error(
151 "server port {port} in forge.toml at {} is the port the gateway binds; the two listeners need a port each",
152 path.display()
153 )]
154 ServerPortTakenByGateway { path: PathBuf, port: u16 },
155
156 #[error(
157 "client {key} = \"{value}\" in forge.toml at {} names nothing forge ships; this key picks by name, never by path",
158 path.display()
159 )]
160 ClientNameUnknown { path: PathBuf, key: &'static str, value: String },
161
162 #[error(
163 "account '{name}' in forge.toml at {} declares a base-url provider but has no base_url key",
164 path.display()
165 )]
166 AccountProviderNeedsBaseUrl { path: PathBuf, name: String },
167
168 #[error(
169 "account '{name}' in forge.toml at {} is missing the required token key",
170 path.display()
171 )]
172 AccountTokenRequired { path: PathBuf, name: String },
173
174 #[error(
175 "account '{name}' in forge.toml at {} declares no models; every account must list the models it serves",
176 path.display()
177 )]
178 AccountModelsRequired { path: PathBuf, name: String },
179
180 #[error(
181 "account in forge.toml at {} maps slug '{slug}' that is not in its models list",
182 path.display()
183 )]
184 AccountSlugUndeclared { path: PathBuf, slug: String },
185
186 #[error(
187 "account in forge.toml at {} maps a blank slug for '{slug}'",
188 path.display()
189 )]
190 AccountSlugBlank { path: PathBuf, slug: String },
191
192 #[error(
193 "account in forge.toml at {} declares model_aliases for '{alias}', which is not one of its models",
194 path.display()
195 )]
196 AccountAliasUndeclared { path: PathBuf, alias: String },
197
198 #[error(
199 "account in forge.toml at {} declares an empty alias list for '{alias}'",
200 path.display()
201 )]
202 AccountAliasEmpty { path: PathBuf, alias: String },
203
204 #[error(
205 "project '{name}' in forge.toml at {} sets model '{model}' that no account in its org declares; every session's first request would fail",
206 path.display()
207 )]
208 ProjectModelUndeclared { path: PathBuf, name: String, model: String },
209
210 #[error("duplicate org name '{name}' in forge.toml at {}", path.display())]
211 DuplicateOrg { path: PathBuf, name: String },
212
213 #[error(
214 "duplicate project name '{name}' in forge.toml at {} (project names must be unique across all orgs)",
215 path.display()
216 )]
217 DuplicateProject { path: PathBuf, name: String },
218
219 #[error(
220 "org '{org}' in forge.toml at {} has no [[orgs.projects]] entries",
221 path.display()
222 )]
223 EmptyOrg { path: PathBuf, org: String },
224
225 #[error(
226 "org '{org}' in forge.toml at {} has an empty `accounts = []` list; list at least one account",
227 path.display()
228 )]
229 EmptyOrgAccounts { path: PathBuf, org: String },
230
231 #[error(
232 "no account in org '{org}' serves project '{project}' model '{model}'; \
233 accounts considered: {accounts}"
234 )]
235 NoAccountServesProjectModel { project: String, org: String, model: String, accounts: String },
236
237 #[error(
238 "every account in org '{org}' that serves project '{project}' model '{model}' is cooling; \
239 soonest reset in {seconds}s"
240 )]
241 AllAccountsCooling { project: String, org: String, model: String, seconds: u64 },
242
243 #[error(
244 "project '{project}' in org '{org}' declares no model; a session's account is selected by \
245 the model it serves"
246 )]
247 ProjectModelMissing { project: String, org: String },
248
249 #[error(
250 "session '{slot}' resolves to no project: it names no project in forge.toml. Add that \
251 project, or start the session from one that is configured"
252 )]
253 SpawnResolvesToNoProject { slot: String },
254 #[error("session '{slot}' was closed while its recovery spawn was in flight; the close stands")]
257 SeatClosedDuringSpawn { slot: String },
258 #[error("spawn role {role:?} and target slot '{slot}' name different sessions")]
262 SpawnRoleSlotDisagree { role: crate::protocol::SpawnRole, slot: String },
263
264 #[error(
265 "org '{org}' in forge.toml at {} references unknown account '{account}'; valid accounts: {valid}",
266 path.display()
267 )]
268 UnknownOrgAccount { path: PathBuf, org: String, account: String, valid: String },
269
270 #[error("no project named '{name}' in forge.toml at {}", path.display())]
271 ProjectNotFound { name: String, path: PathBuf },
272
273 #[error(
274 "the session store could not be read for ({org}, {project}, {label}); the spawn is refused \
275 rather than starting a fresh session over an id that may already exist"
276 )]
277 SessionStoreUnreadable { org: String, project: String, label: String },
278
279 #[error(
280 "failed to create the forge config directory at {}: {source}. forge cannot persist state, crons, or the single-instance lock without a writable config dir",
281 path.display()
282 )]
283 DataDirUnavailable {
284 path: PathBuf,
285 #[source]
286 source: std::io::Error,
287 },
288
289 #[error(
290 "a forge instance is already running on this config dir{}",
291 pid.map_or(String::new(), |p| format!(" (PID {p})"))
292 )]
293 AlreadyRunning { pid: Option<u32> },
294}