fix(host_agent): gate "deployed" on agent check-in; fail no-op runs
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 9s
CI / integration (push) Successful in 2m18s
CI / publish (push) Successful in 58s

A failed provision looked successful in two ways:

1. The host panel showed the agent as deployed (metrics + Update/Rotate/Remove)
   because provision/deploy mint the registration row BEFORE the playbook runs
   and the panel keyed "installed" on that row. Now gated on the agent actually
   checking in (reg.last_seen_at). Three states: reporting (metrics + lifecycle),
   pending (token minted but no check-in → "no metrics yet, deploy may be
   running/failed" banner + retry + Clear pending registration), and none
   (install path).

2. The run reported success though nothing ran — ansible-playbook exits 0 on
   "no hosts matched"/empty inventory. The executor now treats an empty PLAY
   RECAP (returncode 0 but no hosts executed) as failed, with a clear failure
   note. Non-zero exits and recap failed/unreachable were already caught.

Scribe issue #887.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 09:50:11 -04:00
parent a92d1995d5
commit bb90411f00
3 changed files with 43 additions and 9 deletions
+6 -3
View File
@@ -548,13 +548,16 @@ async def host_panel(host_id: str):
hostlvl = latest.get(host.name, {})
ls = reg.last_seen_at if reg else None
stale = bool(reg) and (
ls is None or (datetime.now(timezone.utc) - ls).total_seconds() > _stale_after_seconds())
# "Deployed" is gated on the agent actually checking in (last_seen_at), NOT on
# the registration row — that row is minted before the playbook runs, so a
# failed provision would otherwise look deployed.
reporting = bool(reg) and ls is not None
stale = reporting and (datetime.now(timezone.utc) - ls).total_seconds() > _stale_after_seconds()
ansible_cfg = current_app.config.get("ANSIBLE", {})
return await render_template(
"panel.html",
host=host, reg=reg, hostlvl=hostlvl, stale=stale, target=target,
host=host, reg=reg, hostlvl=hostlvl, reporting=reporting, stale=stale, target=target,
ansible_available=has_capability("ansible.run_playbook"),
managed_key_set=bool((ansible_cfg.get("ssh_public_key") or "").strip()),
)
+25 -5
View File
@@ -3,16 +3,18 @@
<div class="card">
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.75rem;gap:1rem;flex-wrap:wrap;">
<h3 class="section-title" style="margin-bottom:0;">Agent</h3>
{% if reg %}
{% if reporting %}
<span style="font-size:0.78rem;color:{{ 'var(--yellow)' if stale else 'var(--green)' }};">
{{ 'stale' if stale else 'reporting' }}{% if reg.agent_version %} · v{{ reg.agent_version }}{% endif %}
{% if reg.last_seen_at %} · last seen {{ reg.last_seen_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}
· last seen {{ reg.last_seen_at.strftime('%Y-%m-%d %H:%M') }}
</span>
{% elif reg %}
<span style="font-size:0.78rem;color:var(--text-muted);">pending — no check-in yet</span>
{% endif %}
</div>
{% if reg %}
{# ── Installed: at-a-glance metrics + lifecycle ── #}
{% if reporting %}
{# ── Reporting: at-a-glance metrics + lifecycle ── #}
<div style="display:flex;gap:2rem;flex-wrap:wrap;margin-bottom:0.9rem;">
{% set cpu = hostlvl.get('cpu_pct') %}
{% set mem = hostlvl.get('mem_used_pct') %}
@@ -54,10 +56,21 @@
{% endif %}
{% else %}
{# ── Not installed: provisioning path ── #}
{# ── Not reporting: pending (token minted, no check-in) or never installed ── #}
{% if reg %}
<div style="background:color-mix(in srgb,var(--yellow) 10%,var(--bg-elevated));
border:1px solid color-mix(in srgb,var(--yellow) 30%,var(--border));
border-radius:6px;padding:0.7rem 0.9rem;margin-bottom:0.75rem;font-size:0.84rem;">
A token was minted for this host but <strong>no metrics have arrived yet</strong> — the
deploy may still be running, or it failed. Open the latest
<a href="/ansible/">Ansible run</a> to check, then retry below. Live metrics appear here
once the agent checks in.
</div>
{% else %}
<p style="color:var(--text-muted);font-size:0.88rem;margin-bottom:0.75rem;">
No agent installed. The agent reports CPU, memory, disk, network and more back to Steward.
</p>
{% endif %}
{% if session.user_role != 'admin' %}
<p style="color:var(--text-dim);font-size:0.85rem;">An admin can install the agent here.</p>
@@ -113,5 +126,12 @@
<button type="submit" class="btn btn-sm btn-ghost">Install agent (managed key)</button>
</form>
{% endif %}
{% if reg and session.user_role == 'admin' %}
<form method="post" action="/plugins/host_agent/fleet/{{ host.id }}/delete" style="margin:0.75rem 0 0;"
onsubmit="return confirm('Clear this pending registration? Its token is discarded.');">
<button type="submit" class="btn btn-sm btn-ghost">Clear pending registration</button>
</form>
{% endif %}
{% endif %}
</div>