fix(#2018): surface permission error in hivectl agent lookup instead of misleading 'no such agent'
This commit is contained in:
parent
b0c89af817
commit
49067ad83e
1 changed files with 24 additions and 6 deletions
|
|
@ -933,8 +933,26 @@ fn human_bytes(n: u64) -> String {
|
||||||
/// state dir (not the live container list) so kept-state tombstones
|
/// state dir (not the live container list) so kept-state tombstones
|
||||||
/// still resolve as agents — re-provisioning a destroyed-but-kept agent
|
/// still resolve as agents — re-provisioning a destroyed-but-kept agent
|
||||||
/// should still drop its token in the existing state tree.
|
/// should still drop its token in the existing state tree.
|
||||||
fn is_agent(name: &str) -> bool {
|
///
|
||||||
Coordinator::agent_state_root(name).exists()
|
/// Uses `try_exists()` rather than `Path::exists()` so a permission
|
||||||
|
/// error reaching the agents root is surfaced, not collapsed into
|
||||||
|
/// `false`. The agents root is `0700 hive-core`, so running hivectl
|
||||||
|
/// without root yields EACCES on traversal — `Path::exists()` would
|
||||||
|
/// silently report `false`, which callers turn into a misleading "no
|
||||||
|
/// such agent" (or, for the create-user paths, a silent misclassify of
|
||||||
|
/// a real agent as a non-agent account). Mapping EACCES to an explicit
|
||||||
|
/// "needs root" error fixes that first-run footgun (#2018).
|
||||||
|
fn agent_exists(name: &str) -> Result<bool> {
|
||||||
|
let root = Coordinator::agent_state_root(name);
|
||||||
|
match root.try_exists() {
|
||||||
|
Ok(found) => Ok(found),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!(
|
||||||
|
"cannot read the agents root {} (permission denied) - this command needs root; \
|
||||||
|
re-run with sudo",
|
||||||
|
root.parent().unwrap_or(&root).display()
|
||||||
|
),
|
||||||
|
Err(e) => Err(e).with_context(|| format!("check agent state dir {}", root.display())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop into an interactive Claude session in the agent container.
|
/// Drop into an interactive Claude session in the agent container.
|
||||||
|
|
@ -973,7 +991,7 @@ fn is_agent(name: &str) -> bool {
|
||||||
/// `machinectl shell` inherits the caller's PTY, so the session is
|
/// `machinectl shell` inherits the caller's PTY, so the session is
|
||||||
/// fully interactive. Requires root and a running container.
|
/// fully interactive. Requires root and a running container.
|
||||||
fn choom(name: &str, fresh: bool) -> Result<()> {
|
fn choom(name: &str, fresh: bool) -> Result<()> {
|
||||||
if !is_agent(name) {
|
if !agent_exists(name)? {
|
||||||
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
|
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
|
||||||
}
|
}
|
||||||
let container = hive_c0re::lifecycle::container_name(name);
|
let container = hive_c0re::lifecycle::container_name(name);
|
||||||
|
|
@ -1023,7 +1041,7 @@ async fn forge_create_user(name: &str, password: Option<&str>, password_stdin: b
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let user_password = resolve_password(password, password_stdin)?;
|
let user_password = resolve_password(password, password_stdin)?;
|
||||||
if is_agent(name) {
|
if agent_exists(name)? {
|
||||||
if user_password.is_some() {
|
if user_password.is_some() {
|
||||||
bail!(
|
bail!(
|
||||||
"forge create-user: --password / --password-stdin is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token"
|
"forge create-user: --password / --password-stdin is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token"
|
||||||
|
|
@ -1095,7 +1113,7 @@ async fn matrix_create_user(
|
||||||
.build()
|
.build()
|
||||||
.context("build reqwest client")?;
|
.context("build reqwest client")?;
|
||||||
let user_password = resolve_password(password, password_stdin)?;
|
let user_password = resolve_password(password, password_stdin)?;
|
||||||
if is_agent(name) {
|
if agent_exists(name)? {
|
||||||
if user_password.is_some() {
|
if user_password.is_some() {
|
||||||
// The boot-sweep / approval-time agent provisioning path
|
// The boot-sweep / approval-time agent provisioning path
|
||||||
// doesn't accept a password — agents auth by access_token,
|
// doesn't accept a password — agents auth by access_token,
|
||||||
|
|
@ -1421,7 +1439,7 @@ fn single_agent_scope(name: &str) -> hive_sh4re::LifecycleScope {
|
||||||
/// regardless of the migration outcome so a failed migration never leaves
|
/// regardless of the migration outcome so a failed migration never leaves
|
||||||
/// the agent down; the migration error (if any) is surfaced afterwards.
|
/// the agent down; the migration error (if any) is surfaced afterwards.
|
||||||
async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
||||||
if !is_agent(name) {
|
if !agent_exists(name)? {
|
||||||
bail!("no agent named {name:?} (no state dir under the agents root)");
|
bail!("no agent named {name:?} (no state dir under the agents root)");
|
||||||
}
|
}
|
||||||
if !yes {
|
if !yes {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue