Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/core/api/src/admin/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ export const AdminConnectionHealth = Schema.Struct({
/** Epoch ms the check ran, so an operator can tell a fresh verdict from a
* stale one. */
checkedAt: Schema.Number,
/** Enumerable failure mechanism — a `HealthCheckReason` literal. Carried so
* the view can tell a tool-sync stamp (`tool_sync_failed` — catalog state,
* not credential health) from a genuine probe verdict without the free-text
* `detail` this plane deliberately strips. Typed as a plain string ON
* PURPOSE: the reason enum grows, and a closed set here would make a client
* built today fail the WHOLE admin response the first time one connection
* carries a literal it doesn't know. Clients treat unrecognized values as
* unclassified. */
reason: Schema.optional(Schema.String),
});

/**
Expand Down
13 changes: 10 additions & 3 deletions packages/core/api/src/admin/reads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,19 @@ const toUser = (subject: AdminSubject, identities: ReadonlyMap<string, AdminUser
* prevent — the column allowlist below would still have "passed" while every
* upstream field rode along inside it.
*
* Two fields survive, both of them verdicts ABOUT the connection rather than
* content FROM it: `status` and `checkedAt`. Written as an explicit
* Three fields survive, all of them verdicts ABOUT the connection rather than
* content FROM it: `status`, `checkedAt`, and the enumerable `reason` (a
* closed literal set — never upstream text). Written as an explicit
* construction, never a spread, for the same reason the row mappings are.
*/
const toHealth = (health: AdminConnection["lastHealth"]) =>
health === null ? null : { status: health.status, checkedAt: health.checkedAt };
health === null
? null
: {
status: health.status,
checkedAt: health.checkedAt,
...(health.reason !== undefined ? { reason: health.reason } : {}),
};

/**
* `AdminConnection` → the public `AdminUserConnection` shape.
Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export {
pathNamesASecret,
REDACTED_SAMPLE_VALUE,
identityPathTier,
isToolSyncHealth,
rankResponseSample,
} from "./health-check";

Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export {
candidateIdentityTier,
sortHealthCheckCandidatesByIdentity,
identityPathTier,
isToolSyncHealth,
rankResponseSample,
} from "./health-check";

Expand Down
12 changes: 12 additions & 0 deletions packages/react/src/components/accounts-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import * as Exit from "effect/Exit";
import {
IntegrationSlug,
isToolSyncHealth,
type Connection,
type OAuthClientSummary,
type Owner,
Expand Down Expand Up @@ -172,6 +173,12 @@ function AccountRow(props: {
const misconfigured = status === "misconfigured";
const needsHealthAttention = status === "expired" || status === "degraded";
const healthDetail = needsHealthAttention ? probe?.detail : undefined;
// A "Tool sync failing" stamp is about the CATALOG (tools may be stale or
// missing), not the credential — the health hook excludes it from `status`,
// so surface it as its own muted note instead of an unhealthy row.
const syncFailureDetail = isToolSyncHealth(connection.lastHealth)
? connection.lastHealth?.detail
: undefined;
const missingOAuthScopes = connection.missingOAuthScopes ?? [];

const handleCheck = async () => {
Expand Down Expand Up @@ -252,6 +259,11 @@ function AccountRow(props: {
{healthDetail}
</CardStackEntryDescription>
) : null}
{syncFailureDetail ? (
<CardStackEntryDescription className="mt-1 overflow-visible whitespace-normal text-clip text-xs text-muted-foreground">
{syncFailureDetail}
</CardStackEntryDescription>
) : null}
{needsReconsent ? (
<CardStackEntryDescription className="mt-1 text-xs text-muted-foreground">
This connection wasn't granted all the access this integration now needs.
Expand Down
18 changes: 18 additions & 0 deletions packages/react/src/lib/admin-users-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,24 @@ describe("connectionHealthStatus", () => {
it("a never-probed connection is unchecked, not healthy", () => {
expect(connectionHealthStatus(connection({ lastHealth: null }))).toBe("unknown");
});

it("a tool-sync stamp is catalog state, not a degraded connection", () => {
// Same rule as the console's `presentableHealth`: one failed sync sweep
// must not paint an operator's view of a user's connections amber.
expect(
connectionHealthStatus(
connection({ lastHealth: { status: "degraded", reason: "tool_sync_failed" } }),
),
).toBe("unknown");
});

it("a genuine degraded probe verdict still reads degraded", () => {
expect(
connectionHealthStatus(
connection({ lastHealth: { status: "degraded", reason: "probe_timeout" } }),
),
).toBe("degraded");
});
});

describe("isConnectableIntegration", () => {
Expand Down
21 changes: 17 additions & 4 deletions packages/react/src/lib/admin-users-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ export interface AdminConnectionRow {
readonly integration: IntegrationSlug;
readonly name: string;
readonly oauthScope: string | null;
readonly lastHealth: { readonly status: HealthStatus } | null;
readonly lastHealth: {
readonly status: HealthStatus;
/** A `HealthCheckReason` literal, tolerant of future additions (the admin
* wire types it as a plain string); unrecognized values read as
* unclassified. */
readonly reason?: string;
} | null;
}

// ── Last seen ───────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -142,9 +148,16 @@ export const adminUserCopyableEmail = (user: AdminUserIdentityRow): string | nul
// ── Connections ─────────────────────────────────────────────────────────────

/** The health status a row displays. A connection that was never probed carries
* no verdict, which is `unknown` in the shared vocabulary. */
export const connectionHealthStatus = (connection: AdminConnectionRow): HealthStatus =>
connection.lastHealth?.status ?? "unknown";
* no verdict, which is `unknown` in the shared vocabulary. A tool-sync stamp
* (`reason: "tool_sync_failed"`) is catalog state, not credential health —
* the same rule the console's `presentableHealth` applies — so it reads
* `unknown` here rather than painting the row degraded. */
export const connectionHealthStatus = (connection: AdminConnectionRow): HealthStatus => {
const last = connection.lastHealth;
if (last === null) return "unknown";
if (last.reason === "tool_sync_failed") return "unknown";
return last.status;
};

/** The catalog row this view needs: the slug it marks, plus the `kind` that
* says whether connecting is even a thing one can do to it. Structural so
Expand Down
54 changes: 53 additions & 1 deletion packages/react/src/lib/use-connection-health.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "@effect/vitest";
import type { HealthCheckResult } from "@executor-js/sdk/shared";

import { HEALTH_REVALIDATE_MS, revalidateQuery } from "./use-connection-health";
import { HEALTH_REVALIDATE_MS, presentableHealth, revalidateQuery } from "./use-connection-health";

const verdict = (status: HealthCheckResult["status"]): HealthCheckResult => ({
status,
Expand Down Expand Up @@ -46,3 +46,55 @@ describe("revalidateQuery", () => {
expect(windows, "only the healthy path is gated").toEqual([undefined, undefined, undefined]);
});
});

// ---------------------------------------------------------------------------
// Tool-sync stamps are not connection health. `toolSyncHealth` writes a
// degraded verdict with the "Tool sync failing" detail prefix into
// `last_health` when catalog production fails; presenting that as the
// connection's health painted whole integration rows "Degraded" (one bad
// sweep stamps many connections at once) for credentials that were fine.
// ---------------------------------------------------------------------------

describe("presentableHealth", () => {
it("passes genuine probe verdicts through untouched", () => {
const expired: HealthCheckResult = {
status: "expired",
checkedAt: Date.now(),
detail: "HTTP 401",
};
expect(presentableHealth(expired), "a probe verdict presents as-is").toBe(expired);
});

it("hides a tool-sync stamp, old (detail-only) and new (reason) alike", () => {
const stamped: HealthCheckResult = {
status: "degraded",
checkedAt: Date.now(),
detail: "Tool sync failing: upstream returned HTTP 429",
reason: "tool_sync_failed",
};
// Stamps written before `reason` existed carry only the detail prefix.
const legacy: HealthCheckResult = {
status: "degraded",
checkedAt: Date.now(),
detail: "Tool sync failing: plugin returned an incomplete tool catalog",
};
expect(presentableHealth(stamped), "a sync stamp is not connection health").toBeNull();
expect(presentableHealth(legacy), "pre-reason stamps hide the same way").toBeNull();
});

it("treats missing verdicts as missing", () => {
expect(presentableHealth(null)).toBeNull();
expect(presentableHealth(undefined)).toBeNull();
});

it("does not hide a probe verdict whose upstream text merely mentions syncing", () => {
// The marker is the detail PREFIX, owned by the sync stamp writer — an
// upstream error that contains similar words elsewhere stays visible.
const probeVerdict: HealthCheckResult = {
status: "degraded",
checkedAt: Date.now(),
detail: "Health check request failed: upstream sync service unavailable",
};
expect(presentableHealth(probeVerdict)).toBe(probeVerdict);
});
});
23 changes: 21 additions & 2 deletions packages/react/src/lib/use-connection-health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { RegistryContext, useAtomSet } from "@effect/atom-react";
import * as Exit from "effect/Exit";
import { isToolSyncHealth } from "@executor-js/sdk/shared";
import type { Connection, HealthCheckResult, HealthStatus, Owner } from "@executor-js/sdk/shared";

import { checkConnectionHealth, connectionsOptimisticAtom } from "../api/atoms";
Expand Down Expand Up @@ -54,6 +55,17 @@ export const revalidateQuery = (
): { readonly ifStaleMs?: number } =>
last?.status === "healthy" ? { ifStaleMs: HEALTH_REVALIDATE_MS } : {};

/** The verdict a HEALTH surface may present, or null when the persisted
* verdict is a tool-sync stamp (`isToolSyncHealth`). A "Tool sync failing"
* verdict describes the CATALOG — tools may be stale or missing — not the
* credential, so rendering it as the connection's health painted whole rows
* "Degraded" (an entire integrations list, when one bad sweep stamped many
* connections at once) for connections whose credentials were fine.
* Surfaces show the stamp as its own muted note instead. */
export const presentableHealth = (
last: HealthCheckResult | null | undefined,
): HealthCheckResult | null => (last == null || isToolSyncHealth(last) ? null : last);

/** Identity of a persisted verdict, for detecting the reconnect transition.
* An OAuth re-mint clears `last_health`, so a verdict giving way to `null`
* means the grant was replaced and the row must re-probe even though it never
Expand Down Expand Up @@ -115,7 +127,7 @@ export function useConnectionHealth(connection: Connection): {
const doCheck = useAtomSet(checkConnectionHealth, { mode: "promiseExit" });
const invalidateConnections = useInvalidateConnections();

const probe = freshestVerdict(liveProbe, connection.lastHealth);
const probe = freshestVerdict(liveProbe, presentableHealth(connection.lastHealth));
const status: HealthStatus = probe?.status ?? "unknown";

// Health checks are AUTOMATIC: loading the list revalidates any verdict
Expand All @@ -137,6 +149,10 @@ export function useConnectionHealth(connection: Connection): {
seenEpoch.current = epoch;
if (!firstSight && !cleared) return;
if (healthyAndFresh(last)) return;
// A tool-sync stamp still PROBES (it is non-healthy, and the probe is
// what discovers the credential's real state — a never-probed connection
// whose first sync failed must still surface Expired); it just never
// RENDERS as connection health (see `presentableHealth`).
void doCheck({
params: connectionParams(connection),
query: revalidateQuery(last),
Expand Down Expand Up @@ -226,7 +242,10 @@ export function useConnectionsHealth(

return useCallback(
(connection: Connection) =>
freshestVerdict(liveProbes.get(probeKey(connection)) ?? null, connection.lastHealth),
freshestVerdict(
liveProbes.get(probeKey(connection)) ?? null,
presentableHealth(connection.lastHealth),
),
[liveProbes],
);
}
22 changes: 21 additions & 1 deletion packages/react/src/pages/integration-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IntegrationSlug,
ToolAddress,
effectivePolicyFromSorted,
isToolSyncHealth,
type Connection,
type Owner,
} from "@executor-js/sdk/shared";
Expand Down Expand Up @@ -255,7 +256,26 @@ export function IntegrationDetailPage(props: {
const healthProbeFor = useConnectionsHealth(integrationConnections);
const toolsHealthIssue = useMemo(() => {
const issues = integrationConnections
.map((connection) => ({ connection, probe: healthProbeFor(connection) }))
.map((connection) => {
// The health hook hides tool-sync stamps from CONNECTION-health
// display, but this consumer explains an empty CATALOG, where the
// "Tool sync failing" stamp is a real answer (it names the sync
// failure and drives the "Check and sync tools" recovery). Precedence:
// an expired/degraded PROBE verdict wins — a rejected credential
// names its own fix ("Connection rejected" → reconnect) — but a
// healthy or unknown probe must NOT hide the stamp: a working
// credential does not refute a broken catalog, and only a successful
// sync clears the stamp.
const probe = healthProbeFor(connection);
const catalogStamp = isToolSyncHealth(connection.lastHealth) ? connection.lastHealth : null;
return {
connection,
probe:
probe?.status === "expired" || probe?.status === "degraded"
? probe
: (catalogStamp ?? probe),
};
})
.filter(
(
entry,
Expand Down
Loading