[eric] health: boot-time bundled-CLI integrity check rides the login-health pill with repair guidance

This commit is contained in:
ciregenz
2026-07-28 15:49:59 -07:00
parent 0d9e80d502
commit ac0f575d34
5 changed files with 93 additions and 12 deletions
+6 -3
View File
@@ -531,15 +531,18 @@ async def subscriptions_health():
so the frontend can retry once instead of reading 'all healthy' off a cold boot."""
from backend.apps.nine_router import is_running, get_providers
from backend.apps.nine_router.subscription_health import probe_subscription_health
from backend.apps.agents.core.bundled_cli_missing import bundled_cli_missing
# Independent of the router: the bundled-CLI integrity check rides the same boot fetch so an AV-quarantined runtime surfaces as a pill instead of dead turns.
p_cli_missing = bundled_cli_missing() is not None
if not is_running():
return {"dead": [], "skipped": True}
return {"dead": [], "skipped": True, "cli_missing": p_cli_missing}
try:
connections = await get_providers()
dead = await probe_subscription_health(connections)
return {"dead": dead, "skipped": False}
return {"dead": dead, "skipped": False, "cli_missing": p_cli_missing}
except Exception as e:
logger.debug(f"subscription health probe failed: {e}")
return {"dead": [], "skipped": True}
return {"dead": [], "skipped": True, "cli_missing": p_cli_missing}
@agents.router.get("/subscriptions/models")
@@ -0,0 +1,26 @@
"""Detect the AV-quarantine failure class at boot: the claude-agent-sdk's bundled
CLI binary deleted out from under an installed app (Windows-only in field data;
22 of 25 affected installs never produced another agent reply). Returns the
expected path when the binary is gone so the health pill can tell the user how
to repair, None when it's present or there is nothing to verify."""
import platform
from pathlib import Path
from typing import Optional
from typeguard import typechecked
@typechecked
def bundled_cli_missing() -> Optional[str]:
try:
import claude_agent_sdk
p_pkg = Path(claude_agent_sdk.__file__).parent
except Exception:
return None
p_name = "claude.exe" if platform.system() == "Windows" else "claude"
p_bundled = p_pkg / "_bundled" / p_name
# No _bundled dir at all = a source install running the PATH CLI; nothing to verify (quarantine removes the file, not the dir).
if not p_bundled.parent.is_dir():
return None
if p_bundled.is_file():
return None
return str(p_bundled)
+40
View File
@@ -0,0 +1,40 @@
"""Pins the boot-time AV-quarantine detector: binary present = quiet, binary
deleted out of an intact _bundled dir = flagged with the expected path, no
_bundled dir at all (source installs) = quiet, so it can never cry wolf on dev."""
import platform
import sys
import types
from backend.apps.agents.core.bundled_cli_missing import bundled_cli_missing
P_CLI_NAME = "claude.exe" if platform.system() == "Windows" else "claude"
def p_fake_sdk(monkeypatch, tmp_path, with_dir=True, with_binary=True):
pkg = tmp_path / "claude_agent_sdk"
pkg.mkdir()
(pkg / "__init__.py").write_text("")
if with_dir:
(pkg / "_bundled").mkdir()
if with_binary:
(pkg / "_bundled" / P_CLI_NAME).write_text("stub")
mod = types.ModuleType("claude_agent_sdk")
mod.__file__ = str(pkg / "__init__.py")
monkeypatch.setitem(sys.modules, "claude_agent_sdk", mod)
def test_present_binary_is_quiet(monkeypatch, tmp_path):
p_fake_sdk(monkeypatch, tmp_path)
assert bundled_cli_missing() is None
def test_deleted_binary_is_flagged_with_path(monkeypatch, tmp_path):
p_fake_sdk(monkeypatch, tmp_path, with_binary=False)
result = bundled_cli_missing()
assert result is not None
assert result.endswith(P_CLI_NAME)
def test_no_bundled_dir_is_quiet(monkeypatch, tmp_path):
p_fake_sdk(monkeypatch, tmp_path, with_dir=False)
assert bundled_cli_missing() is None
@@ -16,6 +16,7 @@ export default function ProviderHealthToast() {
const dispatch = useAppDispatch();
const open = useAppSelector((s) => s.subscriptions.healthToastOpen);
const dead = useAppSelector((s) => s.subscriptions.healthDead);
const cliMissing = useAppSelector((s) => s.subscriptions.healthCliMissing);
const onReconnect = React.useCallback(() => {
dispatch(openSettingsModal('models'));
@@ -26,7 +27,7 @@ export default function ProviderHealthToast() {
return (
<Snackbar
open={open && dead.length > 0}
open={open && (dead.length > 0 || cliMissing)}
autoHideDuration={null}
// Clickaway would kill the pill on the user's first canvas click, before they read it; only the X or Reconnect dismisses.
onClose={(event, reason) => { if (reason !== 'clickaway') dispatch(hideProviderHealthToast()); }}
@@ -43,9 +44,11 @@ export default function ProviderHealthToast() {
}}
action={
<>
<Button size="small" onClick={onReconnect} sx={{ color: c.accent.primary, fontWeight: 700 }}>
Reconnect
</Button>
{dead.length > 0 && (
<Button size="small" onClick={onReconnect} sx={{ color: c.accent.primary, fontWeight: 700 }}>
Reconnect
</Button>
)}
<IconButton
size="small"
aria-label="Dismiss"
@@ -57,7 +60,9 @@ export default function ProviderHealthToast() {
</>
}
>
Your {labels} login{dead.length > 1 ? 's have' : ' has'} expired; chats on {dead.length > 1 ? 'them' : 'it'} will fail until you reconnect.
{cliMissing
? 'A core OpenSwarm component is missing, usually antivirus quarantine. Restore it from quarantine and add an exclusion for OpenSwarm, or reinstall from openswarm.com; agents cannot run until then.'
: `Your ${labels} login${dead.length > 1 ? 's have' : ' has'} expired; chats on ${dead.length > 1 ? 'them' : 'it'} will fail until you reconnect.`}
</Alert>
</Snackbar>
);
@@ -25,6 +25,7 @@ export interface DeadProvider {
export interface SubscriptionsState {
status: SubscriptionStatus | null;
healthDead: DeadProvider[];
healthCliMissing: boolean;
healthToastOpen: boolean;
}
@@ -34,6 +35,7 @@ type WithSubscriptions = { subscriptions: SubscriptionsState };
const initialState: SubscriptionsState = {
status: null,
healthDead: [],
healthCliMissing: false,
healthToastOpen: false,
};
@@ -56,9 +58,9 @@ export const fetchSubscriptionStatus = createAsyncThunk(
/** Boot-time login-health check; `skipped` means the router wasn't up yet, caller may retry once. */
export const fetchProviderHealth = createAsyncThunk(
'subscriptions/fetchHealth',
async (): Promise<{ dead: DeadProvider[]; skipped: boolean }> => {
async (): Promise<{ dead: DeadProvider[]; skipped: boolean; cli_missing?: boolean }> => {
const r = await fetch(`${API_BASE}/agents/subscriptions/health`);
return (await r.json()) as { dead: DeadProvider[]; skipped: boolean };
return (await r.json()) as { dead: DeadProvider[]; skipped: boolean; cli_missing?: boolean };
},
);
@@ -99,9 +101,14 @@ const subscriptionsSlice = createSlice({
state.status = action.payload;
});
builder.addCase(fetchProviderHealth.fulfilled, (state, action) => {
if (action.payload.skipped) return;
// cli_missing is filesystem truth, valid even when the router probe was skipped.
state.healthCliMissing = action.payload.cli_missing ?? false;
if (action.payload.skipped) {
state.healthToastOpen = state.healthToastOpen || state.healthCliMissing;
return;
}
state.healthDead = action.payload.dead ?? [];
state.healthToastOpen = state.healthDead.length > 0;
state.healthToastOpen = state.healthDead.length > 0 || state.healthCliMissing;
});
},
});