agentmux_srv/identity/resolver/errors.rs
1// Copyright 2025-2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Resolver error types: [`SpawnGateError`] and [`ResolverError`].
5//!
6//! Split out of the single ~2193-line `resolver.rs` (pure relocation, no
7//! behavior change).
8
9use crate::backend::storage::error::StoreError;
10
11/// Blocking spawn-gate error — layer 3 of
12/// SPEC_ACCOUNT_DELETE_DEAUTH_LAYERS_2_4_2026_07_14.md (§2.2).
13///
14/// Returned by the injection entry points when an **oauth-class** provider
15/// the agent is supposed to have credentials for (a binding exists, or the
16/// provider is the agent definition's own CLI provider) has no resolvable
17/// account. There is no per-agent opt-out. The spawn callers surface
18/// `Display` verbatim in the agent pane (same `error_during_execution`
19/// frame other spawn failures use) — the wording is the spec's.
20///
21/// Api-key-class bindings keep the historical log-and-skip behavior; this
22/// error exists only for the oauth class, where silent fallback meant the
23/// CLI would read the user's global login (`~/.claude`) after an account
24/// was deleted.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum SpawnGateError {
27 /// The gate's credentials verdict: no resolvable account.
28 MissingCredentials { provider: String },
29 /// The injection task itself could not run to completion (task-join
30 /// failure — e.g. a panic inside the blocking closure, which also
31 /// poisons the `Store` mutex for every later call). The gate FAILS
32 /// CLOSED on this: an open fallback would silently convert one panic
33 /// anywhere in the store into a permanent, systemic bypass of the
34 /// oauth gate (#2164). A
35 /// blocked spawn is retryable and visible; a silent ambient launch
36 /// is neither.
37 InjectionUnavailable { detail: String },
38 /// The bound account's `SecretRef::OAuthConfigDir` resolves to the
39 /// provider's own literal ambient home directory (e.g. `~/.claude`)
40 /// instead of an AgentMux-isolated dir. Blocked unconditionally — a
41 /// real, currently-live account
42 /// configured exactly this way was found in this repo's own data
43 /// (`docs/status/STATUS_IDENTITY_ISOLATION_GATE_NOT_ENFORCING_2026_08_20.md`
44 /// §8); this variant is the enforcement that closes that gap. See
45 /// `docs/specs/SPEC_BLOCK_AMBIENT_HOME_DIR_IDENTITY_BINDING_2026_08_25.md`.
46 AmbientHomeDirNotAllowed { provider: String, dir: String },
47 /// Failed to seed the isolated Claude Code config dir's `CLAUDE.md`
48 /// placeholder (`providers::seed_claude_md_placeholder_if_missing`).
49 /// Blocked rather than warn-and-continue: an isolated dir with no
50 /// `CLAUDE.md` of its own is exactly the condition where Claude Code
51 /// CLI falls through to the operator's real `~/.claude/CLAUDE.md` —
52 /// continuing to spawn would launch the agent with the leak this
53 /// fix exists to close. See
54 /// `docs/specs/SPEC_ISOLATE_HOST_CLAUDE_MD_2026_08_31.md`.
55 ClaudeMdSeedFailed { provider: String, dir: String, error: String },
56 /// The block names an agent that was deleted (its row is gone and the
57 /// shared definition registry holds only a retired record for it). The
58 /// pane survived the delete — deleted from another window, or mid-spawn
59 /// — and used to spawn on the shared login with no identity at all; it
60 /// is refused instead (#3577).
61 AgentDeleted { agent_id: String },
62}
63
64impl std::fmt::Display for SpawnGateError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 // Starts "this agent's" so muxspect's `classify_last_error_source`
68 // files it under "identity" with the other gate refusals, and
69 // carries "was deleted, so this pane cannot start" — the phrase
70 // `agents/failure.rs` classifies it by (no Retry: a retry is
71 // refused identically).
72 SpawnGateError::AgentDeleted { agent_id } => write!(
73 f,
74 "this agent's definition ({agent_id}) was deleted, so this pane cannot \
75 start. Close the pane, or launch another agent in it.",
76 ),
77 // "Bind an account in Connectors" is now the ONLY path — the
78 // ambient/"use global CLI login" opt-in this used to also
79 // suggest was retired (PLAN_LOGIN_SINGLE_PATH_CONSOLIDATION_
80 // 2026_07_20.md §7): it let a credential the app couldn't
81 // attribute to any account run indefinitely, invisible to
82 // Connectors. Don't point users at a toggle that no longer works.
83 SpawnGateError::MissingCredentials { provider } => write!(
84 f,
85 "no credentials for {}: the bound account was deleted or is \
86 unresolvable. Bind an account for this provider in Connectors → Accounts.",
87 provider,
88 ),
89 SpawnGateError::InjectionUnavailable { detail } => write!(
90 f,
91 "credential injection could not run ({detail}); the spawn was \
92 refused rather than falling back to the global CLI login. \
93 Retry, and check `muxlog auth` if it persists.",
94 ),
95 SpawnGateError::AmbientHomeDirNotAllowed { provider, dir } => write!(
96 f,
97 "this agent's {provider} identity points directly at your personal \
98 {provider} config directory ({dir}) instead of an isolated AgentMux \
99 account — AgentMux no longer allows spawning an agent against your \
100 own global CLI login. Re-bind this identity to an isolated account \
101 in Connectors → Accounts (delete the current {provider} account and log \
102 in again to create a fresh, isolated one), then retry.",
103 ),
104 SpawnGateError::ClaudeMdSeedFailed { provider, dir, error } => write!(
105 f,
106 "could not isolate this agent's {provider} config directory ({dir}): \
107 {error}. Refusing to spawn with an unprotected config dir — retry, \
108 and check the directory's permissions if it persists.",
109 ),
110 }
111 }
112}
113
114impl std::error::Error for SpawnGateError {}
115
116/// Errors specific to the resolver. Every variant is recoverable
117/// (the spawn proceeds with whatever env vars resolved successfully)
118/// — they exist for tracing visibility, not control flow.
119#[derive(Debug, thiserror::Error)]
120pub enum ResolverError {
121 #[error("env var not set in srv environment: {0}")]
122 EnvVarMissing(String),
123
124 #[error("AWS Secrets Manager backend not yet supported (Phase 3)")]
125 SecretsManagerUnsupported,
126
127 #[error("PlaintextDev secrets are disabled in release builds")]
128 #[cfg(not(debug_assertions))]
129 PlaintextDevDisabledInRelease,
130
131 /// `OAuthConfigDir` is a filesystem pointer, not a secret string —
132 /// `resolve_secret` cannot turn it into a credential value because
133 /// the credential lives in a CLI-managed token file inside the dir.
134 /// Oauth-class providers must be routed through the config-dir
135 /// injection path that PR B adds to `inject_identity_env`. Seeing
136 /// this error from `resolve_secret` means the caller forgot to
137 /// dispatch by provider class first.
138 #[error("OAuthConfigDir is a config-dir pointer, not a resolvable secret — routed via the oauth-class injection path, not resolve_secret")]
139 OAuthConfigDirNotASecret,
140
141 /// The OS keychain read failed (no entry, locked store, or no Secret
142 /// Service agent). Account API keys (`SecretRef::Keychain`) live
143 /// in the OS keychain; this surfaces a resolution failure at spawn.
144 #[error("keychain error: {0}")]
145 KeychainError(String),
146
147 #[error("storage error: {0}")]
148 Storage(#[from] StoreError),
149}