Skip to main content

forge_workspace/
error.rs

1//! Error types surfaced by `Workspace::new` and friends.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7/// Reasons `Workspace::new` may fail.
8#[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    /// The parse failure as a reader acts on it: where it is, and why.
17    /// `message` is the error's own words with any quoted value scrubbed
18    /// and `at` its line and column (empty when it has none) - a value on
19    /// the offending line can be a token, so neither the snippet nor the
20    /// toml error itself (whose Debug carries the whole document) rides
21    /// along (#2267).
22    #[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    /// A recovery spawn found the seat closed: the reader stopped it while
255    /// the spawn was being prepared, and only an opening door may undo that.
256    #[error("session '{slot}' was closed while its recovery spawn was in flight; the close stands")]
257    SeatClosedDuringSpawn { slot: String },
258    /// The stated spawn role and the target slot name different
259    /// sessions. Both are caller statements, so a disagreement is a
260    /// caller bug rather than something to resolve.
261    #[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}