pub enum CoreError {
Show 45 variants
Internal(String),
ConfirmationRequired {
operation: String,
},
InvalidImportFile {
reason: String,
},
ImportDenied {
project: String,
problem: String,
endpoint: Option<String>,
},
InvalidInput {
reason: String,
},
ProfileNotFound {
name: String,
known: Vec<String>,
},
NoActiveProfile,
SecretUnavailable {
profile: String,
},
PasswordUnavailable {
profile: String,
},
ConfigInvalid {
reason: String,
},
PollIntervalTooSmall {
profile: String,
},
Network {
url: String,
source: Option<Error>,
observation: Option<String>,
},
Auth {
status: u16,
endpoint: Option<String>,
},
GatewayTooOld {
found: String,
minimum: String,
endpoint: Option<String>,
},
GatewayNotCommissioned {
endpoint: Option<String>,
},
GatewayRestarting {
endpoint: Option<String>,
},
NotFound {
endpoint: Option<String>,
},
ProjectExists {
name: String,
endpoint: Option<String>,
},
ResourceBinary {
path: String,
endpoint: Option<String>,
},
TrialNotExpired {
remaining_s: i64,
endpoint: Option<String>,
},
RoutesNotDeployed {
project: String,
route: String,
endpoint: Option<String>,
},
WebdevUnlicensed {
endpoint: Option<String>,
},
RouteVersionMismatch {
route: String,
deployed: String,
expected: String,
endpoint: Option<String>,
},
ProviderNotFound {
name: String,
endpoint: Option<String>,
},
WebdevRouteError {
code: String,
message: String,
endpoint: Option<String>,
},
TagCollision {
provider: String,
names: Vec<String>,
endpoint: Option<String>,
},
AlarmJournalMissing {
endpoint: Option<String>,
},
SessionNotPrunable {
id: String,
endpoint: Option<String>,
},
Rig(String),
EamNotController {
endpoint: Option<String>,
},
ProviderRootUnsupported {
endpoint: Option<String>,
},
EamTaskTypeRefused {
task_type: String,
},
EamTaskInFlight {
task: String,
detail: String,
endpoint: Option<String>,
},
ScriptExecNotConfigured {
profile: String,
},
LintToolAbsent,
NodeToolAbsent {
tool: String,
},
GatewayClientError {
status: u16,
endpoint: String,
body: String,
},
BundleNotAvailable {
state: String,
},
ModuleFeedUnreachable {
url: String,
source: Option<Error>,
},
ModuleReleaseNotFound {
module: String,
version: String,
url: String,
detail: String,
},
ModuleDigestMismatch(Box<ModuleDigestMismatchDetails>),
ModuleFeedUnusable {
url: String,
detail: String,
},
ModuleDigestChanged(Box<ModuleDigestChangedDetails>),
ModuleNotRegistered {
id: String,
known: Vec<String>,
},
ModuleUninstallDenied {
module_id: String,
gateway_module_id: String,
failed: Vec<String>,
endpoint: Option<String>,
},
}Expand description
Every failure ign can report. One variant per contract class; code(),
exit_code(), hint() are total functions over it.
Variants§
Internal(String)
Unexpected runtime failure — the catch-all; report as a bug. Exit 1.
ConfirmationRequired
Destructive operation invoked without --yes. Exit 2 (same class as
usage: it names a flag the caller must add; clap renders its own
usage errors with its exit 2 — never hook clap).
InvalidImportFile
An import byte source the caller must fix (wrong file, too big,
unreadable). Exit 2 — usage class: it names what the CALLER must
change, like Self::ConfirmationRequired (03-02).
ImportDenied
The gateway ANSWERED the import POST with HTTP 200 but the
body says {"success": false, "problem": "…"} — the
denial-rides-200 class the WebDev family handles (05-01)
applied to the project-import family (05-07, UAT Gap 1):
without this check the import caller reports ok while nothing
landed. Exit 6 — target state: the gateway refused the
import (the problem text names why, verbatim).
Fields
InvalidInput
A command input the caller must fix (unreadable --file, failed
stdin read). Exit 2 — usage class, the generic sibling of
Self::InvalidImportFile (03-03: resource put’s byte
source).
ProfileNotFound
Named profile absent from config. Exit 3.
NoActiveProfile
No --profile, no IGNITION_PROFILE, no active profile in config.
Exit 3. Constructible from the CLI once config resolution lands
(01-03); the taxonomy is complete on day one.
No credential resolvable for the profile (env, token_env, keyring all missed or failed). Exit 3.
The adopt bootstrap’s login rung needs IGNITION_PASSWORD and it
was absent/empty (ADOPT-05 review round: the generic
Self::SecretUnavailable hint names the TOKEN paths, which is
exactly wrong for the PASSWORD gate — same exit class, own slug,
never a new exit code; the PollIntervalTooSmall precedent).
ConfigInvalid
Config file unreadable or wrong shape. Exit 3.
PollIntervalTooSmall
A profile’s poll_interval_secs is below the 1-second floor
(08-01, TUIX-05 clamp): sub-second gateway polling is refused at
load time — the TUI’s background refresh cadence may not hammer
the gateway. Exit 3 — the CONFIG class (the Phase-7 additive-slug
precedent, e.g. eam_not_controller: same exit class, own slug,
never a new exit code — the 1–7 taxonomy is frozen).
Network
Gateway unreachable / timeout / TLS failure. Exit 4.
source: None marks a POLL deadline expiry (02-04 poll.rs):
same class, same slug (network_error), no new variant — the
transport-error source a real failure carries is simply
absent, and url describes what was being waited on instead
(the poll’s subject). The deadline’s last observation rides
observation (09-07): when it is Some the gateway ANSWERED
with a concrete state, and the message leads with “no terminal
state” — it NEVER claims unreachability for an observed
answer; None keeps the plain unreachability wording.
Fields
Auth
Gateway reachable but rejected credentials (401/403). Exit 5.
GatewayTooOld
Gateway reachable but the command is invalid for its current state — version below minimum, uncommissioned, mid-restart, or a missing resource. Exit 6.
Fields
GatewayNotCommissioned
Gateway reachable but uncommissioned — every /data route 302s to
/welcome (verified on a fresh 8.3.6 container; 02-RESEARCH
§Error-Body Sniffing). Exit 6.
GatewayRestarting
Gateway restarting — webserver answers (503) but services are down (verified restart lifecycle: webserver never drops the connection). Exit 6.
NotFound
Named resource absent (404) — terminating a nonexistent session id,
an unknown path, or a pre-8.3 gateway’s JSON
{"message": "No route match for path: …"}. Exit 6.
ProjectExists
A project of this name already exists and the import’s collision policy is abort — the CLI-side pre-check refused BEFORE any upload (the server’s own answer remains the backstop). Exit 6 — target state: the command is invalid for the gateway’s current state (03-02, the GatewayTooOld action-built-variant precedent: constructed by the actions layer, not classify).
Fields
ResourceBinary
A binary (data.bin-class) resource met the surgical JSON/text loop — REFUSED rather than corrupted through it. Exit 6 — target state: the command is invalid for that resource’s nature; the export/import family owns binary resources (Pitfall 7).
Fields
TrialNotExpired
The gateway refuses trial resets while the trial is still active — live-discovered on 8.3.3 during 04-03’s spike: the reset POST answers 403 on a NON-expired trial (verified from the browser page itself with the exact UI headers), and 200 + the flip on an expired one. The action layer’s expiry pre-check turns that misleading auth-shaped 403 into the honest target-state refusal. Exit 6 (the ProjectExists precedent: action-constructed, not classify).
Fields
RoutesNotDeployed
The WebDev route family a command depends on is not deployed —
the presence probe answered 405, the live-proven 8.3 absent
marker (missing routes AND missing projects both answer 405,
NOT 404; 05-RESEARCH Pitfall 1). Exit 6 — target state: the
command is invalid until ign webdev deploy installs the
routes (the TrialNotExpired precedent: action-constructed, not
classify).
Fields
WebdevUnlicensed
The WebDev module answered 402 — installed but unlicensed (a
trial-expired gateway; live-verified cross-version on 8.3.6,
05-RESEARCH §Servlet). Exit 6 — no /system/webdev route can
answer until the gateway is licensed.
RouteVersionMismatch
A deployed route’s handshake version differs from the embedded bundle’s — the CLI refuses rather than auto-upgrading either side (roadmap-locked: actionable error, no auto-upgrade magic). Exit 6.
Fields
expected: StringThe embedded bundle’s version
(crate::webdev::ROUTE_BUNDLE_VERSION).
ProviderNotFound
A tag provider of this name does not exist — the find→signature→delete chain’s find half missed (05-04, TAGS-01). Exit 6 — target state: the named thing is absent (the ProjectExists precedent family: action-constructed, not classify — the honest, family-specific refusal over a bare 404).
Fields
WebdevRouteError
A WebDev route answered HTTP 200 with a body denial
({ok:false, error{code,message}}) whose machine code this CLI
does not specifically map — code + message ride verbatim so
agents can branch on the stable route contract (05-01). Exit 6
— target state: the deployed route refused the action.
Fields
TagCollision
A tag import under abort policy found EXISTING tags at the target provider (05-05, TAGS-09) — the browse pre-check refuses BEFORE any route write (the LOCKED Phase-3 collision matrix mapped onto configure’s ‘a’/‘o’). Exit 6 — target state: the named tags exist; overwrite is the explicit, guarded opt-in.
Fields
AlarmJournalMissing
The gateway has no alarm-journal profile configured — alarm
history has nowhere to read from. The alarms route’s
structured no_alarm_journal denial maps here (the
denial_to_error seam, 05-06 TAGS-07): DEFAULT rigs hit this
ALWAYS, because the journal is a config-resource chain —
database connection + ignition/alarm-journal profile + the
general-alarm-settings singleton pointing at it. Exit 6 —
target state: the command is invalid for the gateway’s
current state until that chain is provisioned (the honest,
actionable refusal over a bare route error).
SessionNotPrunable
Pruning a LIVE Designer session entry — the gateway’s prune route answers 409 (empty body, wire-verified on 8.3.3, 06-UAT test 6): prune removes STALE entries only, so the command is invalid while the Designer is still open. Exit 6 — target state (additive slug in the frozen taxonomy’s established growth pattern; classify()’s ROUTE-SCOPED 409 arm constructs this — the first classify-constructed refusal added since the 02-01 set).
Fields
Rig(String)
Docker/compose rig failure. Exit 7. Reserved — first used in Phase 4; trivially constructible so the taxonomy enumerates completely today.
EamNotController
The gateway’s EAM module is not configured as a controller —
every /data/eam/api/v1/* runtime endpoint answers 403 with
“This operation can only be performed when EAM is configured
as a controller” on a stock gateway (live-proven 8.3.3,
07-RESEARCH): a STATE refusal, not auth — the token is fine,
the module’s role is not. Message-classified at the classify
seam, path-scoped to /data/eam/ so generic 403s elsewhere
cannot shift (the trial_not_expired pattern, classify
edition). Exit 6 — target state.
ProviderRootUnsupported
A provider-ROOT tag path ([default] alone, or a bare first
segment that resolves to a provider) met the tagConfig route
— system.tag.getConfiguration/exportTags need an RPC
context WebDev threads don’t carry (live-proven 8.3.3
b2026012009, both gateways; 07-UAT test 12). The route
refuses honestly (pre-call bracket detection + RpcContext
translation for the bare form) instead of surfacing the
IllegalStateException as a generic route error; subtree
paths ([default]folder) are the supported form. Exit 6 —
target state: a platform limitation, not a bug.
EamTaskTypeRefused
An eam task new whose type is in the REFUSED set —
eam_restoreBackup, eam_installModules, eam_remoteUpgrade
are fleet-destructive (they push backups/modules/upgrades to
every AGENT target), and the CLI refuses them outright over
guard-everything: honest refusal with the v2 scope pointer
(the planner-locked create ladder’s top rung). Exit 6 —
target state (additive slug alongside Task 2’s).
EamTaskInFlight
An eam task force whose slot a leftover run occupies — the
force route answers 409 with the gateway’s own Jetty page
(“Task ‘X (forced)’ already exists! It must be completed or
deleted before another task of this type can be force
executed.”; live-captured 8.3.3, 07-UAT test 7). Exit 6 —
target state (the session_not_prunable precedent,
force-route edition): the gateway’s state refused the command,
not a bug; classify()’s path-scoped 409 arm constructs this.
Fields
ScriptExecNotConfigured
The scriptExec route is not configured for this profile — no
webdev secret is persisted, which can only mean ign webdev deploy --with-script-exec has never run (deploy persists the
secret 0600 BEFORE upload, so a deployed route without a
stored secret is not a reachable state). ign script run’s
opt-in is STRUCTURAL — the deploy flag — so the verb carries
no --yes guard and refuses here instead (07-03, SCRPT-01:
the TrialNotExpired precedent — action-constructed, not
classify). Exit 6 — target state.
LintToolAbsent
ign lint found no ignition-lint executable on PATH — the
delegation has nothing to delegate to (07-04, INTR-02). The
hint carries the install command + repo. Exit 6 — target
state (additive slug; the environment lacks the tool, the
command is fine).
NodeToolAbsent
ign e2e init found no node executable on PATH — the scaffold
it would write is unusable without one (QUICK-tg4).
Exit 6, mirroring Self::LintToolAbsent line for line: this
is the SAME shape — a local external tool a verb delegates to is
absent, with an install hint. It deliberately does NOT join
exit 7, whose sole slug is rig_error for docker/compose rig
failures; putting a missing Node there would force a relabel of
a locked exit class.
GatewayClientError
The gateway answered the api call with a 4xx this CLI does not
curate — the caller’s request is the problem, and the body is
theirs to read. Exit 2 (usage class; additive slug — the
poll_interval_too_small precedent: same class, own slug, no new
exit code). Constructed ONLY by the api-call-scoped classify arm
(09-01: the api_call parameter) — a curated command’s 4xx keeps
its existing classification (the catch-all cannot fire without
the parameter). The body is the gateway’s answer VERBATIM,
truncated at GATEWAY_CLIENT_BODY_CAP_BYTES with the explicit
GATEWAY_CLIENT_BODY_TRUNCATION_MARKER via truncate_api_body
at construction.
Fields
body: StringThe response body VERBATIM, truncated at
GATEWAY_CLIENT_BODY_CAP_BYTES with an explicit marker.
BundleNotAvailable
No diagnostics bundle is available — the status poll ANSWERED
with a captured TERMINAL steady state meaning “no current
bundle” (Invalid: live-proven on 8.3.6 rig ign-p9-836,
2026-09-07 UAT / 09-UAT.md Gap 3 — a Valid bundle decays to
Invalid within ~2 minutes UNPROMPTED and stays Invalid;
only a fresh generate changes it, polling cannot). Exit 6 —
target state (the ImportDenied precedent: a gateway-answered
refusal riding its own class, action-constructed by bundle wait’s probe, not classify).
ModuleFeedUnreachable
Transport failure reaching a third-party module release feed or
its asset CDN — DNS/connect/timeout/TLS (Phase 15, D-06). Exit 4:
same network class as Self::Network, but a SEPARATE slug and
wording — Network’s Display hard-codes “gateway unreachable”,
which is actively wrong here (api.github.com is not a gateway,
and the user’s own gateway may be perfectly reachable). This is
the offline signal fetch_and_verify returns when the cache is
empty (SC-4’s negative half).
ModuleReleaseNotFound
No release exists for the pinned tag, or the release carries no asset of the expected name (Phase 15, SC-1’s “unknown version” refusal). Exit 6 — target state: names BOTH the requested version and the resolved URL, and (for a missing asset) both the expected asset name and the names actually present, so the caller never has to guess which lookup failed or falls back to “latest”.
Fields
module: StringThe crate::module::ModuleSpec::id being fetched.
ModuleDigestMismatch(Box<ModuleDigestMismatchDetails>)
Downloaded bytes hash to something other than the release’s published digest — SC-2’s hard refusal (Phase 15; this variant is defined in 15-01, the branch is exercised starting 15-02). Exit 6 — target state: the feed answered and served bytes, but what it served cannot be trusted. The bytes are discarded (never persisted to the cache) and BOTH digests are named.
Payload is BOXED: five inline Strings put CoreError at
exactly clippy’s 128-byte result_large_err threshold, which
fails -D warnings in every crate returning Result<_, CoreError> — ignition-tui first. Error paths are cold, so one
allocation on construction is the cheap side of that trade.
ModuleFeedUnusable
The module release feed ANSWERED but not usably — rate-limited,
unauthenticated 401/403, 5xx, an unparseable body, an asset with
no published digest, or an insecure downgrade of the asset URL
(Phase 15). Exit 6 — target state: distinct from
Self::ModuleFeedUnreachable (exit 4 — transport never reached
the feed at all); here the feed DID answer, just not with
something ign can safely act on.
Fields
ModuleDigestChanged(Box<ModuleDigestChangedDetails>)
A pinned version’s upstream digest differs from what’s already
cached — D-02’s default refusal on FetchPolicy::Refresh /
AcceptUpstreamChange (Phase 15, 15-02): the feed answered
fine, but what it currently publishes for this version no
longer matches what was verified and cached before. Exit 6 —
target state: names BOTH full digests and the path of the
cached artifact, which is left on disk UNTOUCHED (no path in
module/ deletes, truncates, or overwrites a cache entry).
FetchPolicy::AcceptUpstreamChange is the explicit, documented
override.
Payload is BOXED for the same reason as
Self::ModuleDigestMismatch — see that variant.
ModuleNotRegistered
A declared module id has no matching registry entry (Phase 16,
D-13). A well-formed id that simply isn’t registered is a CONFIG
problem, not malformed input (exit 2 belongs to
validate_module_id/validate_version) and not a Docker
failure (exit 7 is Self::Rig) — every OTHER module
provisioning refusal (missing artifact, non-file artifact,
relative path, underivable service) stays CoreError::Rig.
Exit 3, structurally the Self::ProfileNotFound shape: names
both the bad id and every id that IS registered.
Fields
id: StringThe declared id with no matching crate::module::ModuleSpec.
known: Vec<String>Every currently registered crate::module::ModuleSpec::id.
ModuleUninstallDenied
The gateway ANSWERED DELETE /data/api/v1/modules/uninstall with
HTTP 200 but the body says {"success": false, "failedUninstalls": {"uninstall": […]}} — the denial-rides-200 class
(Self::ImportDenied’s precedent) applied to module uninstall
(Phase 19, D-19-03): live-verified that a stock module, a bogus
id, and a still-mounted module ALL answer 200 with
success:false. An unparseable body, a missing success, or an
absent failedUninstalls all map here too — absence of detail
must never degrade into a reported success. Exit 6 — target
state: names both the registry id and the gateway id that was
sent, plus every failed id the gateway reported (or the sent id
itself when the gateway gave no detail).
Implementations§
Source§impl CoreError
impl CoreError
Sourcepub fn exit_code(&self) -> u8
pub fn exit_code(&self) -> u8
The LOCKED exit-code mapping — the only place exit codes are decided.
Sourcepub fn endpoint(&self) -> Option<String>
pub fn endpoint(&self) -> Option<String>
URL/path of the request involved, when one was — populated for the network, auth, and target-state classes (CORE-05).
Sourcepub fn tui_tty_refusal() -> Self
pub fn tui_tty_refusal() -> Self
The ign tui TTY refusal (06-07): InvalidInput carrying the
ONE reason whose hint is terminal-contextual — see
TUI_TTY_REFUSAL_REASON. Same slug, same exit 2 (frozen
taxonomy; only the hint differs).
Sourcepub fn envelope<'a>(&self, profile: Option<&'a str>) -> ErrorEnvelope<'a>
pub fn envelope<'a>(&self, profile: Option<&'a str>) -> ErrorEnvelope<'a>
Build the LOCKED failure envelope for this error (field order is part
of the golden contract: ok, profile, error then code,
message, endpoint, hint).
Trait Implementations§
Source§impl Error for CoreError
impl Error for CoreError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()