Skip to content

feat(agent-platform): add version-tolerant kagent client - #1998

Merged
marians merged 6 commits into
mainfrom
agent-platform-kagent-client
Jul 27, 2026
Merged

feat(agent-platform): add version-tolerant kagent client#1998
marians merged 6 commits into
mainfrom
agent-platform-kagent-client

Conversation

@marians

@marians marians commented Jul 27, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds the data layer behind the upcoming Agent Platform Sessions list: a
version-tolerant client for the kagent REST API. No visible change yet — this
lands the contract before any UI depends on it.

Stacked on #1997 (the backend proxy), so review that first. This PR's base
is agent-platform-kagent-proxy and will retarget to main automatically when
#1997 merges.

The constraint that shapes all of it: kagent ships no OpenAPI spec, GS pins
v0.9.9 while upstream is already on v0.10.0-beta9, and every installation is an
independent deployment. So the fleet can run several kagent versions at once, and
everything here is keyed per installation and defensive by default.

Wire → domain boundary (lib/kagentSchema.ts, lib/kagentSessions.ts).
Permissive zod schemas (unknown fields pass through, every field may be absent or
retyped) normalize into a stable KagentSession, so a kagent schema change is
absorbed in one function instead of rippling through components. Specifics worth
reviewing:

  • Rows are validated individually, so one malformed entry is skipped rather
    than costing the whole list.
  • Envelope drift is tolerated: data absent (Go's omitempty on an empty
    slice — this is what "no sessions" actually looks like), data: null, and a
    bare top-level array.
  • Go zero time is dropped. created_at/updated_at are non-pointer
    time.Time, so unset arrives as 0001-01-01T00:00:00Z — which browsers render
    as "Dec 31, 0000".

Per-installation capability negotiation (lib/kagentCapabilities.ts,
hooks/useKagentCapabilities.ts). A cached /version probe per installation
yields named flags, so components never compare versions inline. The supported
window is explicit (MIN_SUPPORTED 0.9.9, TESTED_UP_TO 0.10.0): below the floor
is flagged but still renders, above the ceiling proceeds optimistically and logs
once. A failed or unparseable probe degrades to the oldest supported version, and
never blocks the sessions query.

An isUserScoped flag from a /me probe. kagent's unsecure auth mode
ignores the forwarded token and resolves every caller to a shared built-in user —
so the list would silently not be "your sessions". Better detected than
mislabelled.

Auth, per installation. There is no fleet-wide token: each installation has
its own Dex and oidcTokenProvider. The client mints lazily per request, so a
mint failure degrades one installation (you may be signed in to some and not
others) rather than the page. Tokens are never cached — the plugin's query
cache is persisted to localStorage, which is no place for a credential. The
minting sequence moves out of useDeployAgent into
lib/installationOidcToken.ts so the deploy flow and this client mint
identically.

What is the effect of this change to users?

None yet — nothing renders this. It unblocks the Sessions tab.

How does it look like?

Version-matrix fixtures are the contract, since kagent offers no spec. They
include a captured live v0.9.9 response, on the grounds that a payload from
production beats a hand-written one — it already exercises absent source, absent
deleted_at, both real id shapes (64-char hex and UUID), and kagent's
20-character truncated titles.

The core assertions: v0.9.9 and v0.10 payloads normalize to identical output,
and a synthetic future version carrying unknown fields plus an unknown source
value changes nothing.

Those fixtures immediately earned their keep by catching a zod subtlety I had
wrong: wrapping z.unknown() in a .transform() makes the field required
again, so every row that merely omits a key was being dropped — which is most
real rows. Without the live fixture this would have shipped as a mysteriously
short list.

177 tests pass in plugins/agent-platform (32 of them pre-existing); tsc and
lint are clean. The 8 remaining no-mixed-plugin-imports lint warnings are
pre-existing files importing from gs — none from this PR.

Any background context you can provide?

Re-implementation of the Sessions view from the
agent-platform-ui prototype.
The prototype's Session has ~20 fields against kagent's 7, so the eventual UI is
a deliberately reduced, read-only version.

Two notes for the next PR, both discovered from the live payload:

  • source is absent on every real row, so the planned "hide A2A subagent
    sessions" filter is a no-op against current data. Kept as forward-compatibility,
    but it hides nothing today.
  • Titles are truncated to 20 chars by kagent ("What issues are assi...") and
    the full first message is unrecoverable. The Session column will therefore be
    weak, and the Agent column ends up carrying most of a row's meaning — worth
    bearing in mind when reviewing the table.

Do the docs need to be updated?

The docs/agent-platform.md Sessions section lands with the UI PR, where the
behaviour is user-visible. Design rationale lives in doc comments here.

Should this change be mentioned in the release notes?

  • A changeset describing the change and affected packages was added. (more info)

kagent sessions live in kagent's Postgres and are served over the controller's
HTTP API, so unlike agents and model configs they are not Kubernetes resources
and the Kubernetes proxy the rest of the plugin uses cannot reach them. This
adds the transport the upcoming Sessions list needs.

A backend hop is unavoidable: the browser cannot call kagent.<baseDomain>
cross-origin, and the user's per-installation Dex ID token has to become the
Authorization header toward kagent (inbound, that header carries the Backstage
identity). Same approach as muster-backend, via a separate
backstage-kagent-authorization header.

The base URL is derived per installation as https://kagent.<baseDomain>/api --
the oauth2-proxy-fronted host whose nginx sidecar proxies /api/ to
kagent-controller:8083 -- and is overridable per installation, which also acts
as an allowlist since kagent is only deployed on some installations.

Responses pass through verbatim: the {error, data, message} envelope is not
unwrapped and unknown fields are not stripped, so kagent schema drift is
absorbed by the frontend client instead of needing a backend release. The split
is backend = transport, frontend = schema.

All config keeps the default backend visibility; apiBaseUrl embeds baseDomain,
which deanonymizes customers, so /kagent/installations returns names only.
@marians
marians requested a review from a team as a code owner July 27, 2026 10:31
marians added 2 commits July 27, 2026 12:45
…-read failures

Addresses review feedback on #1997. Both findings verified against the kagent
source and the connectivity chart.

Remove the /version probe entirely. kagent serves /version at its server root,
and neither door we support routes the root to the controller: the derived door's
nginx sidecar proxies only `location /api/` to kagent-controller:8083 while
`location /` goes to the kagent UI (which answers HTML, so our own non-JSON guard
reported a sign-in page on healthy installations), and the agentgateway override
matches only the /kagent prefix so a root-relative /version never matches its
HTTPRoute. Nothing under /api exposes the controller version either -- the
Version fields in /api/substrate/status are per-actor, not the controller's -- so
there is nothing reachable to probe. Version tolerance is unaffected: it lives in
the frontend's permissive parsing, which is where it does the real work. A test
now asserts every request stays under /api, and another that the route is gone.

Map failures while reading the response body. The try/catch only covered fetch(),
but the abort signal stays armed after the headers arrive, so a mid-stream abort,
a connection reset, or truncated/invalid JSON escaped untyped and surfaced as a
generic 500 instead of the 503 the frontend keys off for "kagent unreachable".

Reject a non-absolute apiBaseUrl at startup with one clear message, rather than
letting new URL()/fetch fail opaquely on every request.
The data layer behind the upcoming Sessions list. No visible change yet.

kagent ships no OpenAPI spec, GS pins v0.9.9 while upstream is on v0.10.0-beta9,
and every installation is an independent deployment -- so the fleet can run
several kagent versions at once. Everything here is keyed per installation and
defensive by default.

Wire/domain boundary: responses are parsed with permissive zod schemas and
normalized into a stable KagentSession, so a schema change is absorbed in one
function rather than rippling through components. Rows are validated one at a
time, so a single malformed entry is skipped instead of costing the whole list.
Envelope drift is tolerated (data absent per Go's omitempty, data: null, or a
bare array), and Go zero time -- which browsers render as "Dec 31, 0000" -- is
dropped along with anything unparseable.

Capability negotiation: a cached /version probe per installation yields named
flags, so components never compare versions inline. The supported window is
explicit; below the floor still renders, above the ceiling proceeds optimistically
and logs once, and a failed probe degrades to the oldest supported version without
ever blocking the session query. A /me probe adds isUserScoped, because kagent's
unsecure auth mode ignores the forwarded token and resolves every caller to a
shared user -- worth detecting rather than mislabelling the list as the user's own.

The client mints each installation's Dex token lazily per request so a mint
failure degrades one installation rather than the page, and never caches tokens:
the plugin's query cache is persisted to localStorage, which is no place for a
credential. The token-minting sequence moves out of useDeployAgent so the deploy
flow and this client mint identically.

Version-matrix fixtures -- including a captured live v0.9.9 response -- assert
that v0.9.9 and v0.10 normalize to identical output and that a synthetic future
version with unknown fields changes nothing. Those fixtures immediately earned
their keep by catching a zod subtlety: wrapping z.unknown() in a transform makes
the field required again, which had been failing every row that omits a key --
which is most real rows.

@marians marians left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful pass over the whole diff. The wire→domain layer is solid — I replicated the zod schemas against zod 4.4.3 and confirmed every documented tolerance actually holds (missing keys, null rows, arrays-as-rows, data absent/null/non-array, bare top-level array, and the unknown-in-transform-needs-.optional() subtlety). I also verified the external facts the design rests on against kagent itself: /version returns the bare VersionResponse (no envelope) and /api/me returns claims-or-{sub} in both v0.9.9 and v0.10.0-beta9, APIPathMe exists in v0.9.9, and admin@kagent.dev is genuinely UnsecureAuthenticator's fallback user ID. No issues found in kagentSchema.ts's parsing, normalizeSessionList's drift logic, normalizeTimestamp, capabilitiesForVersion's semver gating, installationOidcToken.ts, or the plugin.tsx wiring.

7 findings inline, none blocking. The two I'd actually act on before the UI lands are the retry: 1 override (it silently undoes the fail-fast contract that handleResponse's error names exist to provide, and 404/503 is the common case across the fleet) and the unmapped 400 (the backend throws InputError for an installation outside its kagent allowlist, which the sessions provider would then surface as an error rather than "not deployed here"). The rest are smaller: the unconditional token mint making the backend's optional-token probe path unreachable, isError/message parsed then discarded, an array identity in the memo deps that defeats the signature it sits next to, and isUserScoped collapsing "unknown" into false.

One thing outside this PR's diff, but it decides whether the capability negotiation works at all: KagentClient.getVersion resolves new URL('/version', apiBaseUrl), i.e. https://kagent.<baseDomain>/version at the host root. If the agentic-platform-connectivity nginx sidecar only proxies /api/ to kagent-controller:8083, that request never reaches kagent and every installation permanently degrades to FALLBACK_KAGENT_CAPABILITIES — silently, since the fallback is by design indistinguishable from a healthy 0.9.9. Worth confirming against a live installation before the Sessions UI starts gating on these flags.

queryKey: kagentVersionQueryKey(installation),
queryFn: () => kagentApi.getVersion(installation),
staleTime: VERSION_STALE_TIME_MS,
retry: 1,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retry: 1 on both probe queries (here and L64) replaces the QueryClientProvider retry predicate rather than tightening it, which defeats the short-circuit KagentApiClient.handleResponse exists for.

The predicate in components/QueryClientProvider.tsx returns false for NotFoundError / ServiceUnavailableError precisely so that "kagent isn't deployed on this installation" fails fast. Since kagent is only on a couple of installations, 404/503 is the normal outcome for most of the fleet — and with retry: 1 each of those now costs a second request after a retryDelay of 1s, per probe, per installation. On a 20-installation fleet with kagent on 2 that's ~36 extra doomed round trips per page load, each one also re-running getInstallationOidcToken (a broker token exchange).

Dropping retry here inherits the predicate; if you want a lower ceiling than the default failureCount <= 2, pass a predicate that keeps the name check.

(errorData as { error?: { message?: string } })?.error?.message ??
`kagent request failed with status ${response.status}`;
const error = new Error(message);
if (response.status === 401) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400 is unmapped, and the backend produces it on a path this client can actually hit.

agent-platform-backend's resolveInstallation throws Backstage InputError (→ 400) twice: when ?installation= is missing, and — the interesting one — when the name is not in the configured kagent allowlist (Unknown kagent installation '<name>'). The allowlist is gs.installations entries with a baseDomain, or the explicit agentPlatform.kagent.installations block, which is not the same set as "installations the frontend considers reachable".

So if useKagentCapabilitiesMap (or the upcoming sessions provider) is handed anything wider than listInstallations() — e.g. useReachableInstallations() output — those installations return 400, which becomes a plain Error. Two consequences: the retry predicate retries it 3× with exponential backoff instead of failing fast, and the doc comment right above says everything that isn't 404/503 is "we couldn't read it, surfaced" — so a misconfigured/unlisted installation will render as a user-visible error rather than "kagent isn't deployed here".

Mapping 400 to a distinct name (or at least to a non-retryable one) would make this classifiable.

const headers: Record<string, string> = {};
if (installation) {
url.searchParams.set('installation', installation);
headers[KAGENT_AUTH_HEADER] = await getInstallationOidcToken(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token is minted unconditionally for every installation-scoped call, which makes the backend's deliberate "token optional for /version and /me" path unreachable.

router.ts in #1997 reads the token with { required: false } for both probes, with the comment: "kagent's /version is unauthenticated in-process … and a token-mint failure never turns the probe into a hard 401". But get() mints before fetching whenever installation is set, so a mint failure (expired main Dex session, broker_unreachable/session-expired from GSAuthProviders.createClusterTokenProvider, or an installation whose oidcTokenProvider isn't wired) rejects the probe without any request reaching the backend.

Scenario: the broker exchange fails for one installation. /version would have answered fine unauthenticated, but instead capabilitiesForVersion never runs, that installation silently degrades to FALLBACK_KAGENT_CAPABILITIES, and isUserScoped stays undefined — i.e. the exact diagnostics this PR adds go dark for the installation that most needs them.

Suggest a per-call requireToken flag (true for /sessions, false for /version and /me, where a failed mint just omits the header).

.transform(envelope => ({
rows: envelope.data ?? [],
hadDataArray: envelope.data !== undefined && envelope.data !== null,
isError: envelope.error === true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isError and message are extracted here but never read — normalizeSessionList only ever touches parsed.data.rows and hadDataArray.

That turns an in-band error into a silent empty list: a 200 response carrying { "error": true, "message": "…", "data": null } normalizes to { sessions: [] } with no drift at all, so the UI shows "no sessions" and nothing is logged. The backend can't cover this for you — KagentClient.request only classifies on HTTP status and returns any 2xx body verbatim, exactly so the envelope stays the frontend's problem.

Either fold isError into the drift result (drift: message ?? 'kagent reported an error in the envelope') or drop the two fields from the transform so it's clear nothing checks them.


return result;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [installations, signature]);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment at L68-70 says the memo "keys off a stable signature of the outcomes rather than the array identities", but installations — an array identity — is still in the dep list, so the signature buys nothing when the caller derives the array inline.

useReachableInstallations happens to return a memoized array, but the natural call site is something like useKagentCapabilitiesMap(installations.filter(…)) or an intersection with listInstallations(), which is fresh every render. Then byInstallation is a new Map each render, useCallback hands back a new capabilitiesFor identity each render, and any consumer memo/useEffect keyed on it re-runs — including, potentially, a sessions useQueries built from it.

The established pattern in this plugin is useReachableInstallations, which keys on allInstallations.join(',') and explicitly excludes the array identity. Same fix here: [signature] alone already encodes the installation names.

*/
const UNSCOPED_SUBJECTS = ['admin@kagent.dev'];

function isUserScopedSubject(sub: string | undefined): boolean {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning false for a missing sub conflates "we couldn't tell" with "confirmed shared built-in user", and the caller has already spent undefined on "still loading" — so there is no value left to mean "unknown".

This is reachable without kagent being in unsecure mode. HandleGetCurrentUser returns principal.Claims verbatim when claims exist (only falling back to {sub: principal.User.ID} when they don't), so /api/me under trusted-proxy returns whatever the IdP put in the token. And getIdentity maps any unparseable body to { sub: undefined } and resolves — so status === 'success', isUserScoped === false.

Net effect: an odd-but-working deployment gets whatever "these are not your sessions" affordance the next PR renders, which is the mislabelling this probe was added to prevent. Consider a tri-state (true / false / undefined for "no subject reported") so the UI can stay silent when it doesn't know.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430isUserScoped is now genuinely tri-state.

I'd reasoned that false was the safer default ("wrongly claiming scoping is the more misleading error") but you've identified the flaw: undefined was already spent on "loading", so there was no value left for "can't tell", and the two collapsed into the alarming one.

I checked the reachability claim in kagent's source and it holds — HandleGetCurrentUser responds with principal.Claims verbatim whenever claims exist, only falling back to {sub: principal.User.ID} when they don't. So under trusted-proxy the body is whatever the IdP emitted, and a deployment configured with a different userIdClaim, or an IdP that doesn't emit sub, legitimately yields no subject. Combined with getIdentity resolving on an unparseable body, that meant status === 'success' and isUserScoped === false on a perfectly healthy install — the exact mislabelling the probe was added to prevent.

isUserScopedSubject now returns boolean | undefined, the type documents all three states, and the contract for PR 3 is explicit: stay silent on undefined, warn only on an explicit false.

) {
warnedAboveCeiling.add(installation);
// eslint-disable-next-line no-console
console.warn(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this is a side effect inside useMemo, guarded by module-level mutable state.

useMemo callbacks aren't guaranteed to run once per logical update — React may discard and recompute them — so the "once" property here rests entirely on warnedAboveCeiling, which is module-global and never cleared. Two consequences worth a thought: the warning is suppressed for the whole page session even across full app remounts (an installation upgraded past the ceiling mid-session warns once and never again, which is fine, but so does a genuinely new occurrence), and reportedDrift in KagentApiClient.ts has the same unbounded-growth shape with a key that embeds a varying row count (skipped 2 unreadable session rows), so it dedupes less than it looks like it does.

An useEffect keyed on signature would put the logging where effects belong and let React own the once-per-commit guarantee.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific code here is gone — the above-ceiling warning went with the version flags when /version turned out to be unreachable through either door (see #1997), so there's no longer a side effect inside this useMemo.

The second half of your point outlived it though, and I've fixed that: reportedDrift in KagentApiClient.ts had the same unbounded-growth shape with a key embedding a varying row count. It now keys on a closed set of drift kinds rather than the formatted message, so the set is bounded by installations × kinds and recurring drift warns once. Details in that thread.

Taking the general note too: no useMemo in this hook performs side effects any more, so React is free to discard and recompute them without observable consequences.

Follows the removal of the unreachable /version probe in the backend (review
feedback on #1997). kagent's /version sits at the server root, which neither
supported door proxies to the controller, and nothing under /api reports the
controller version -- so there is no version to negotiate on.

Removes getVersion from the client, the version wire schema, the semver
dependency, and the three version-derived flags (hasSessionShares,
canRenameSessionViaPatch, hasSessionReadOnly) along with the MIN_SUPPORTED /
TESTED_UP_TO window. All three flags were unused -- nothing in the Sessions list
consumes them -- so this removes speculative code rather than a feature.

Capabilities keep the one thing we can actually observe: isUserScoped, from the
/me probe, which matters because kagent's unsecure auth mode would otherwise
present a shared session list as the user's own.

Version tolerance is untouched. It lives in the permissive parsing layer and its
version-matrix fixtures, which is where it does the real work; version detection
was the speculative part. If gating is ever needed, probe by behaviour (call a
version-specific endpoint and treat 404 as absent) rather than by version string.
@marians
marians force-pushed the agent-platform-kagent-client branch from f8a1e5c to e66cf73 Compare July 27, 2026 10:52
@marians

marians commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Rebased onto the updated #1997 and dropped the version-derived capability flags, following the review finding there that kagent's /version is unreachable through either door (it sits at the server root; the derived door's nginx sends / to the kagent UI, and the agentgateway override only matches /kagent). Nothing under /api reports the controller version either, so there is no version to negotiate on.

Removed: getVersion, the version wire schema, the semver dependency, and the three version-derived flags (hasSessionShares, canRenameSessionViaPatch, hasSessionReadOnly) plus the MIN_SUPPORTED/TESTED_UP_TO window. All three flags were unused — the Sessions list consumes none of them — so this deletes speculative code rather than a feature.

Kept: isUserScoped from the /me probe, which is reachable (it's under /api) and load-bearing, since kagent's unsecure auth mode would otherwise present a shared session list as the user's own.

Version tolerance is unaffected, which is worth being explicit about given it was the headline of this PR: it lives in the permissive parsing layer and its version-matrix fixtures — v0.9.9 and v0.10 payloads still normalize to identical output, unknown fields and unknown source values still pass through untouched. Version detection was the speculative part; tolerance is where the work actually happens.

177 tests pass, tsc clean, no new lint problems.

@marians marians left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after e66cf73 (drop version-derived capability flags).

The version probe removal is the right fix, and it's clean: no dangling references to capabilitiesForVersion, getVersion, kagent_version, isAboveTestedCeiling, the three flags, or the MIN_SUPPORTED/TESTED_UP_TO window anywhere in either plugin (the remaining semver hits are the unrelated Flux OCIRepository range in composeManifests.ts). Changeset rewritten coherently, dep dropped from package.json + yarn.lock, and router.test.ts now pins that /kagent/version 404s. Keeping isUserScoped as the one observable capability, plus the "probe by behaviour, treat 404 as absent" note for any future gating, is the durable version of this.

Two earlier comments are resolved by the removal:

  • The console.warn side effect inside useMemo guarded by the never-cleared warnedAboveCeiling module-global — gone with the version code.
  • isUserScopedSubject(undefined) === false — now an explicit, documented, and tested decision rather than an implicit one. Fair call; withdrawn.

Six from the first pass still stand, re-anchored below to the current lines. All non-blocking.

One nit outside this diff, in #1997's territory: KagentClient.ts:39 still documents the token as "Optional because /version and /me are useful even when no token could be minted" — /version no longer exists, so only /me is left to justify it.

queryKey: kagentIdentityQueryKey(installation),
queryFn: () => kagentApi.getIdentity(installation),
staleTime: IDENTITY_STALE_TIME_MS,
retry: 1,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still stands after the version query's removal, halved in cost but unchanged in kind: retry: 1 replaces the QueryClientProvider retry predicate rather than tightening it, so NotFoundError/ServiceUnavailableError no longer short-circuit.

kagent is deployed on only a couple of installations, so 404/503 is the normal outcome for most of the fleet — and each probe now pays a second doomed request after a 1s delay, plus a re-run of the broker token exchange in getInstallationOidcToken.

retry: (failureCount, error) =>
  failureCount < 1 && !['NotFoundError', 'ServiceUnavailableError'].includes(error.name),

or drop retry entirely and let the provider's predicate apply.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430 — dropped the retry override entirely so the provider predicate applies.

I'd not appreciated that a per-query retry replaces the default rather than composing with it, which is exactly backwards from what I wanted: the predicate in QueryClientProvider.tsx exists to make NotFoundError/ServiceUnavailableError fail fast, and overriding it re-enabled retries for precisely the case it was written to short-circuit. Your fleet arithmetic is the part that makes it matter — 404/503 is the normal outcome on most installations, so the cost scales with the fleet rather than with anything going wrong, and each doomed retry re-runs the broker token exchange.

I went with dropping retry rather than the composed predicate you offered, since failureCount <= 2 is a reasonable ceiling for genuinely unknown errors and one fewer place to keep the name list in sync. There's a comment at the call site explaining why there's deliberately no override, so it doesn't get "helpfully" re-added.

Comment on lines +129 to +140
if (response.status === 401) {
error.name = 'UnauthorizedError';
}
if (response.status === 403) {
error.name = 'ForbiddenError';
}
if (response.status === 404) {
error.name = 'NotFoundError';
}
if (response.status === 503) {
error.name = 'ServiceUnavailableError';
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

400 is still unmapped, and the backend does throw it: resolveInstallation raises InputError (→400) for an installation it has no client for (router.ts:123). That set is gs.installations entries with a baseDomain, which is not the same as the frontend's "reachable" set — so this is reachable in normal operation, not just on a coding error.

When it happens the failure becomes a plain Error: retried 3× with backoff by the provider predicate, and — per the doc comment right above this block — classified as "we couldn't read it" (surfaced to the user) rather than "kagent isn't deployed here" (silent). Mapping 400 to NotFoundError, or adding a distinct name the sessions provider treats as not-deployed, would put it on the silent path where it belongs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430 — 400 now maps to NotFoundError.

The distinction you drew is the one I'd missed: the backend's allowlist (gs.installations entries with a baseDomain, or the explicit block) is not the frontend's reachable set, so an unlisted installation is ordinary operation rather than a coding error. Left unmapped it got the worst of both — retried three times with backoff, then surfaced to the user as a read failure, when the honest reading is "kagent isn't available here".

I chose NotFoundError over a new distinct name because it puts the case on the existing silent path with no further wiring, and it matches the backend's own 404 wording. The trade-off I'm accepting: a genuinely missing ?installation= would also be 400 and would now be swallowed silently. That's tolerable because the client always sets the parameter whenever an installation is passed, so the unknown-installation branch is the only reachable 400 — but it's noted in a comment so the assumption is visible if that ever changes.

Comment on lines +101 to +108
if (installation) {
url.searchParams.set('installation', installation);
headers[KAGENT_AUTH_HEADER] = await getInstallationOidcToken(
this.kubernetesApi,
this.kubernetesAuthProvidersApi,
installation,
);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token is minted unconditionally whenever installation is set. The backend reads it for /kagent/me with readUserToken(req, { required: false }) (router.ts:173) — and now that the version route is gone, /me is the only route with that relaxation, so this client makes the deliberate optional-token path entirely dead code.

The cost is where it hurts: a broker or session failure rejects the capability probe before any request is sent, so the installation silently degrades to FALLBACK_KAGENT_CAPABILITIES with isUserScoped undefined. That's the one case where the answer matters most — an unsecure-mode deployment showing a shared session list as "your sessions" — and the diagnostics go dark exactly there.

A per-call tokenRequired flag (or .catch(() => undefined) on the mint for the probe paths only) would let /me reach the backend token-less and still report a subject.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430 — added a per-call token requirement, false for /kagent/me.

You're right that this made the backend's relaxation dead code, and the consequence you point out is the sharp bit: the probe would fail before any request was sent, so the installation degraded to unknown capabilities exactly when the answer mattered most — an unsecure deployment presenting a shared session list as the user's own. Losing that diagnostic to a broker hiccup is precisely the wrong failure mode.

getIdentity now mints best-effort and omits the header when the mint fails, so the request still reaches the backend token-less. /sessions keeps a required mint, since without a token kagent has no subject to scope on. A test asserts the token-less request goes out and still returns a subject.

Comment on lines +74 to +76
isError: envelope.error === true,
message:
typeof envelope.message === 'string' ? envelope.message : undefined,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isError and message are extracted here and in the bare-array branch, but nothing reads them — normalizeSessionList only uses rows and hadDataArray.

So a 200 carrying {"error": true, "message": "...", "data": null} normalizes to an empty session list with no drift note and no log: indistinguishable from "this user has no sessions". Either feed them into the drift path (drift: message ?? 'error envelope') or drop the two fields so the schema doesn't imply a tolerance it doesn't have.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430isError now feeds the drift path.

This was the most consequential of the batch: I'd extracted the two fields and then never read them, so a 200 carrying {"error": true, "data": null} normalized to {sessions: []} with no drift and no log — an empty table that looks exactly like "you have no sessions". And you're right that the backend can't cover it, by its own design: it classifies on HTTP status and returns 2xx bodies verbatim precisely so the envelope stays the frontend's problem.

normalizeSessionList now returns drift: {kind: 'error-envelope', message}, falling back to a generic message when the envelope carries none. Two tests cover it, one from a fixture and one for the no-message case.

Comment on lines +19 to +32
/** Drift is worth knowing about, but only once per message per page-session. */
const reportedDrift = new Set<string>();

function reportDrift(installation: string, drift: string) {
const key = `${installation}:${drift}`;
if (reportedDrift.has(key)) {
return;
}
reportedDrift.add(key);
// eslint-disable-next-line no-console
console.warn(
`kagent sessions response drift on installation '${installation}': ${drift}`,
);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reportedDrift is module-global and never cleared, so "once per message per page-session" is really once per process — a Backstage tab stays open for days, and a genuinely new drift after a kagent upgrade is reported once and then never again.

The key also undercuts the bound in the other direction: kagentSessions.ts builds drift as skipped ${skippedRows} unreadable session…, so a varying row count yields a fresh key and a fresh warn each time.

Since the values are already surfaced as drift on the return, letting the caller decide (or keying on a stable drift kind rather than the formatted message) avoids both edges.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430 — dedupe now keys on a stable drift kind rather than the formatted message.

Both edges you describe were real and they pulled in opposite directions, which is what made the original wrong in a way that looked fine: skipped ${n} unreadable session rows yields a fresh key per row count, so the same recurring problem re-warned indefinitely, while the set itself grew without bound in a long-lived tab.

normalizeSessionList now returns drift: {kind, message} with kind from a closed set (unparseable-body, error-envelope, data-not-array, skipped-rows), and the client dedupes on installation:kind. That bounds the set at installations × kinds, and keeps recurring drift to one warning. The message is still what gets logged, so the row count is not lost — it just no longer participates in the key.

Comment on lines +46 to +54
// useQueries returns fresh arrays every render, so the memo below keys off a
// stable signature of the outcomes rather than the array identities. Computed
// plainly (not memoized) because it is the memo's own dependency and is cheap.
const signature = installations
.map((installation, index) => {
const identity = identityQueries[index];
return `${installation}:${identity?.status}:${identity?.data?.sub ?? ''}`;
})
.join('|');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the memo keys off a stable signature "rather than the array identities", but installations is still in the dep array below (line 73), so the array identity is load-bearing after all.

Any caller deriving the array inline — useKagentCapabilitiesMap(reachable.map(i => i.name)) — rebuilds the Map and the returned callback identity on every render, invalidating downstream memos. useReachableInstallations in this same plugin does it the other way, keying on allInstallations.join(',') and keeping the array out of the deps. Either match that, or reword the comment to say the array identity is intentionally part of the key.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7424430installations is out of the dep array; the memo keys on [signature] alone.

Fair hit: the comment asserted a property the code didn't have. The signature already encodes the installation names, so it was doing the work while the array identity quietly invalidated everything anyway.

Your point about the natural call site is what makes this more than cosmetic — the upcoming sessions provider intersects the reachable set with listInstallations(), which is a fresh array every render, so capabilitiesFor would have had a new identity on each render and any useQueries derived from it would have churned. Now matching useReachableInstallations, which keys on allInstallations.join(',') and keeps the array out of the deps.

Added a test that renders with an inline array literal — my first attempt reused one array instance across rerenders and so proved nothing, which is worth flagging since it's an easy way to write a test that passes for the wrong reason.

Seven findings from review on #1998, all verified against the code and the kagent
source before changing anything.

Inherit the retry predicate. A per-query `retry: 1` *replaced* the
QueryClientProvider predicate rather than tightening it, so NotFoundError and
ServiceUnavailableError no longer short-circuited. Since kagent runs on only a
couple of installations, 404/503 is the normal outcome for most of the fleet, and
each one was paying a second doomed request plus another broker token exchange.

Map 400. The backend raises InputError (400) for an installation outside its
kagent allowlist, and that allowlist is not the same set as the frontend's
reachable installations -- so this happens in ordinary operation. Unmapped it was
retried with backoff and classified as "we couldn't read it", i.e. surfaced to the
user, when it means "kagent isn't available here".

Make the token optional where the backend says it is. /kagent/me is read with
`{required: false}`, but the client minted unconditionally, so a broker or
Dex-session failure rejected the probe before any request was sent. That silently
cost the isUserScoped diagnostic on exactly the installation where it matters
most. The mint is now best-effort for that path.

Surface in-band errors. `isError`/`message` were extracted from the envelope and
never read, so `{error: true, data: null}` on a 200 normalized to an empty list
with nothing logged -- indistinguishable from "this user has no sessions". The
backend passes 2xx bodies through verbatim by design, so this is ours to check.

Give drift a stable kind. Dedupe keyed on the formatted message, but the
skipped-rows message embeds a varying count, so it deduped less than it appeared
to while also growing unbounded. Drift now carries `{kind, message}` and dedupe
keys on the kind.

Drop the array identity from the memo deps. The comment claimed the memo keyed off
an outcome signature rather than array identities, but `installations` was still in
the dep list -- so any caller deriving the array inline rebuilt the Map and the
returned callback every render, invalidating downstream memos. The signature
already encodes the names, matching useReachableInstallations.

Make isUserScoped tri-state. Returning false for a missing subject conflated "we
can't tell" with "confirmed shared user", and /api/me returns the token's claims
verbatim -- so an IdP that doesn't emit `sub` would have been shown the very "these
aren't your sessions" warning the probe exists to prevent. undefined now means
unknown, and callers must stay silent on it.
@marians

marians commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

All seven findings addressed in 7424430. Each was valid; I verified the claims against the code and kagent's source before changing anything, and two of them were catching real user-visible bugs rather than style.

Finding Fix
retry: 1 replaced the provider predicate Dropped the override so 404/503 short-circuit again
400 unmapped NotFoundError, onto the silent "not available here" path
Token minted unconditionally Per-call requirement; best-effort for /me
isError/message never read Fed into the drift path
Drift dedupe keyed on formatted message Stable {kind, message}, dedupe on kind
Array identity in memo deps Keys on [signature] alone
isUserScoped conflated unknown with shared Genuine tri-state

The two I'd single out as more than hygiene:

  • The in-band error would have rendered {"error": true, "data": null} as an empty table with nothing logged — indistinguishable from "you have no sessions". No error would ever have reached a user or Sentry.
  • The isUserScoped conflation would have shown a healthy deployment whose IdP omits sub the very "these aren't your sessions" warning the probe exists to prevent. I checked HandleGetCurrentUser in kagent: it returns principal.Claims verbatim, so that's reachable in normal operation, not a hypothetical.

The one comment I didn't act on as written is the useMemo side-effect note: that code went with the version flags in the previous round, so there's nothing left to move into a useEffect. Its second half — the unbounded reportedDrift set — I did fix.

184 tests pass (up from 177), tsc clean, no new lint problems. New coverage worth noting: the token-less /me probe, the 400 mapping, the error envelope with and without a message, and a memo-stability test using an inline array literal — my first version of that one reused a single array instance and would have passed for the wrong reason.

Base automatically changed from agent-platform-kagent-proxy to main July 27, 2026 12:30
@github-actions

Copy link
Copy Markdown

JS Dependency Audit

0 added · 0 removed · 213 total (0 vs base)

Projects audited
  • . (manager: yarn-berry)
  • ./packages/app (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./packages/backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./packages/backend-common (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./packages/backend-headless-service (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/agent-platform (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/agent-platform-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/ai-chat (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/ai-chat-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/ai-chat-react (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/auth-backend-module-gs (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/catalog-backend-module-gs (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/error-reporter-react (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/flux (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/flux-react (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/gs (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/gs-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/gs-common (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/gs-node (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/kubernetes-react (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/muster (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/muster-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/plans (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/plans-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/roadmap (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/roadmap-backend (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/scaffolder-backend-module-gs (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/techdocs-backend-module-gs (manager: unknown) — skipped on PR head: no recognized lockfile
  • ./plugins/ui-react (manager: unknown) — skipped on PR head: no recognized lockfile

No change in vulnerabilities compared to the base branch.

Full current vulnerability list (213)
  • 🔴 critical cipher-base (<=1.0.4) — cipher-base is missing type checks, leading to hash rewind and passing on crafted data advisory
  • 🔴 critical elliptic (<=6.6.0) — Elliptic's private key extraction in ECDSA upon signing a malformed input (e.g. a string) advisory
  • 🔴 critical handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has JavaScript Injection via AST Type Confusion advisory
  • 🔴 critical pbkdf2 (>=3.0.10 <=3.1.2) — pbkdf2 returns predictable uninitialized/zero-filled memory for non-normalized or unimplemented algos advisory
  • 🔴 critical pbkdf2 (>=1.0.0 <=3.1.2) — pbkdf2 silently disregards Uint8Array input, returning static keys advisory
  • 🔴 critical shell-quote (>=1.1.0 <=1.8.3) — shell-quote quote() does not escape newlines in object .op values advisory
  • 🟠 high @backstage/backend-defaults (<0.12.2) — Backstage has a Possible Symlink Path Traversal in Scaffolder Actions advisory
  • 🟠 high @backstage/plugin-scaffolder-node (>=0.12.0 <0.12.3) — Backstage has a Possible Symlink Path Traversal in Scaffolder Actions advisory
  • 🟠 high @backstage/plugin-scaffolder-node (<0.11.2) — Backstage has a Possible Symlink Path Traversal in Scaffolder Actions advisory
  • 🟠 high @grpc/grpc-js (>=1.14.0 <1.14.4) — @grpc/grpc-js: A malformed request can cause a server crash advisory
  • 🟠 high @grpc/grpc-js (>=1.14.0 <1.14.4) — @grpc/grpc-js: An incoming malformed compressed message can cause a client or server crash advisory
  • 🟠 high @hono/node-server (<1.19.10) — @hono/node-server has authorization bypass for protected static paths via encoded slashes in Serve Static Middleware advisory
  • 🟠 high adm-zip (<0.6.0) — adm-zip: Crafted ZIP file triggers 4GB memory allocation advisory
  • 🟠 high axios (>=1.15.2 <1.18.0) — Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning advisory
  • 🟠 high axios (>=0.31.1 <0.33.0) — Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning advisory
  • 🟠 high brace-expansion (>=2.0.0 <2.1.2) — brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups advisory
  • 🟠 high brace-expansion (<1.1.16) — brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups advisory
  • 🟠 high brace-expansion (>=3.0.0 <5.0.7) — brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups advisory
  • 🟠 high brace-expansion (<=5.0.7) — brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash advisory
  • 🟠 high fast-uri (<=3.1.0) — fast-uri vulnerable to path traversal via percent-encoded dot segments advisory
  • 🟠 high fast-uri (<=3.1.1) — fast-uri vulnerable to host confusion via percent-encoded authority delimiters advisory
  • 🟠 high fast-uri (>=3.0.0 <=3.1.3) — fast-uri vulnerable to host confusion via literal backslash authority delimiter advisory
  • 🟠 high fast-uri (>=3.0.0 <3.1.3) — fast-uri vulnerable to host confusion via failed IDN canonicalization advisory
  • 🟠 high fast-xml-parser (>=4.0.0-beta.3 <4.5.5) — fast-xml-parser affected by numeric entity expansion bypassing all entity expansion limits (incomplete fix for CVE-2026-26278) advisory
  • 🟠 high fast-xml-parser (>=5.0.0 <5.5.6) — fast-xml-parser affected by numeric entity expansion bypassing all entity expansion limits (incomplete fix for CVE-2026-26278) advisory
  • 🟠 high flatted (<3.4.0) — flatted vulnerable to unbounded recursion DoS in parse() revive phase advisory
  • 🟠 high flatted (<=3.4.1) — Prototype Pollution via parse() in NodeJS flatted advisory
  • 🟠 high glob (>=10.2.0 <10.5.0) — glob CLI: Command injection via -c/--cmd executes matches with shell:true advisory
  • 🟠 high glob (>=11.0.0 <11.1.0) — glob CLI: Command injection via -c/--cmd executes matches with shell:true advisory
  • 🟠 high handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has JavaScript Injection via AST Type Confusion by tampering @partial-block advisory
  • 🟠 high handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has JavaScript Injection via AST Type Confusion when passing an object as dynamic partial advisory
  • 🟠 high handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has Denial of Service via Malformed Decorator Syntax in Template Compilation advisory
  • 🟠 high handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has JavaScript Injection in CLI Precompiler via Unescaped Names and Options advisory
  • 🟠 high hono (<4.12.25) — hono: CORS Middleware reflects any Origin with credentials when origin defaults to the wildcard advisory
  • 🟠 high immutable (<3.8.3) — Immutable is vulnerable to Prototype Pollution advisory
  • 🟠 high immutable (<4.3.9) — Immutable.js List 32-bit trie overflow → unrecoverable DoS advisory
  • 🟠 high immutable (<4.3.9) — Immutabl: Hash-collision algorithmic complexity denial of service in Immutable.Map/Set advisory
  • 🟠 high js-yaml (>=4.0.0 <4.3.0) — js-yaml: YAML merge-key chains can force quadratic CPU consumption advisory
  • 🟠 high js-yaml (>=3.0.0 <3.15.0) — js-yaml: YAML merge-key chains can force quadratic CPU consumption advisory
  • 🟠 high js-yaml (>=5.0.0 <=5.2.1) — js-yaml: Exponential parsing time in flow collections leads to denial of service advisory
  • 🟠 high jsonata (<2.2.0) — jsonata: Malicious inputs to "$toMillis" function can cause resource exhaustion advisory
  • 🟠 high jws (=4.0.0) — auth0/node-jws Improperly Verifies HMAC Signature advisory
  • 🟠 high jws (<3.2.3) — auth0/node-jws Improperly Verifies HMAC Signature advisory
  • 🟠 high linkify-it (<=5.0.0) — LinkifyIt#match scan loop has quadratic algorithmic complexity advisory
  • 🟠 high linkify-it (<=5.0.1) — linkify-it: Quadratic-complexity DoS via the mailto: validator scan-loop on attacker text advisory
  • 🟠 high lodash (>=4.0.0 <=4.17.23) — lodash vulnerable to Code Injection via _.template imports key names advisory
  • 🟠 high lodash-es (>=4.0.0 <=4.17.23) — lodash vulnerable to Code Injection via _.template imports key names advisory
  • 🟠 high path-to-regexp (>=8.0.0 <8.4.0) — path-to-regexp vulnerable to Denial of Service via sequential optional groups advisory
  • 🟠 high picomatch (<2.3.2) — Picomatch has a ReDoS vulnerability via extglob quantifiers advisory
  • 🟠 high picomatch (>=4.0.0 <4.0.4) — Picomatch has a ReDoS vulnerability via extglob quantifiers advisory
  • 🟠 high postcss (<=8.5.11) — PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments advisory
  • 🟠 high postcss (<=8.5.17) — PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure advisory
  • 🟠 high react-router (>=7.12.0 <8.3.0) — React Router: RSC Mode CSRF Bypass Allows Action Execution Before 400 Response advisory
  • 🟠 high shell-quote (<=1.8.4) — shell-quote: Quadratic-complexity Denial of Service in parse() (CWE-407) advisory
  • 🟠 high svgo (>=1.0.0 <2.8.3) — SVGO removeScripts plugin leaves some executable scripts intact advisory
  • 🟠 high tmp (<0.2.6) — tmp has Path Traversal via unsanitized prefix/postfix that enables directory escape advisory
  • 🟠 high underscore (<=1.13.7) — Underscore has unlimited recursion in _.flatten and _.isEqual, potential for DoS attack advisory
  • 🟡 moderate @babel/runtime (<7.26.10) — Babel has inefficient RegExp complexity in generated code with .replace when transpiling named capturing groups advisory
  • 🟡 moderate @backstage/backend-common (0.25.0) — This package is deprecated, please follow the deprecation instructions for the exports you still use
  • 🟡 moderate @backstage/cli-common (<=0.1.16) — @backstage/cli-common has a possible resolveSafeChildPath Symlink Chain Bypass advisory
  • 🟡 moderate @backstage/plugin-circleci (0.3.35) — This package has been moved to the to the https://github.com/CircleCI-Public/backstage-plugin repository. You should migrate to using that instead.
  • 🟡 moderate @fortawesome/react-fontawesome (0.2.6) — v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater.
  • 🟡 moderate @hono/node-server (<1.19.13) — @hono/node-server: Middleware bypass via repeated slashes in serveStatic advisory
  • 🟡 moderate @hono/node-server (<2.0.5) — Node.js Adapter for Hono: Path traversal in serve-static on Windows via encoded backslash (%5C) advisory
  • 🟡 moderate @humanwhocodes/config-array (0.13.0) — Use @eslint/config-array instead
  • 🟡 moderate @humanwhocodes/object-schema (2.0.3) — Use @eslint/object-schema instead
  • 🟡 moderate @material-ui/core (4.12.4) — Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.
  • 🟡 moderate @material-ui/lab (4.0.0-alpha.61) — Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.
  • 🟡 moderate @material-ui/pickers (3.3.11) — This package no longer supported. It has been relaced by @mui/x-date-pickers
  • 🟡 moderate @material-ui/styles (4.11.5) — Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.
  • 🟡 moderate @octokit/endpoint (>=9.0.5 <9.0.6) — @octokit/endpoint has a Regular Expression in parse that Leads to ReDoS Vulnerability Due to Catastrophic Backtracking advisory
  • 🟡 moderate @octokit/plugin-paginate-rest (>=1.0.0 <9.2.2) — @octokit/plugin-paginate-rest has a Regular Expression in iterator Leads to ReDoS Vulnerability Due to Catastrophic Backtracking advisory
  • 🟡 moderate @octokit/plugin-paginate-rest (>=9.3.0-beta.1 <11.4.1) — @octokit/plugin-paginate-rest has a Regular Expression in iterator Leads to ReDoS Vulnerability Due to Catastrophic Backtracking advisory
  • 🟡 moderate @octokit/request (>=1.0.0 <8.4.1) — @octokit/request has a Regular Expression in fetchWrapper that Leads to ReDoS Vulnerability Due to Catastrophic Backtracking advisory
  • 🟡 moderate @octokit/request-error (>=1.0.0 <5.1.1) — @octokit/request-error has a Regular Expression in index that Leads to ReDoS Vulnerability Due to Catastrophic Backtracking advisory
  • 🟡 moderate @react-hookz/deep-equal (1.0.4) — PACKAGE IS DEPRECATED AND WILL BE DETED SOON, USE @ver0/deep-equal INSTEAD
  • 🟡 moderate @rjsf/material-ui (5.24.13) — Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
  • 🟡 moderate @sentry/browser (<7.119.1) — Sentry SDK Prototype Pollution gadget in JavaScript SDKs advisory
  • 🟡 moderate @types/http-proxy-middleware (1.0.0) — This is a stub types definition. http-proxy-middleware provides its own type definitions, so you do not need this installed.
  • 🟡 moderate @types/keyv (4.2.0) — This is a stub types definition. keyv provides its own type definitions, so you do not need this installed.
  • 🟡 moderate @ungap/structured-clone (1.3.0) — Potential CWE-502 - Update to 1.3.1 or higher
  • 🟡 moderate atlassian-openapi (1.0.19) — DEPRECATED: atlassian-openapi has moved to @atlassian/atlassian-openapi. The latest version is 1.0.6. Please update your dependency.
  • 🟡 moderate axios (>=1.0.0 <1.18.0) — Axios: Excessive recursion in formDataToJSON can cause denial of service advisory
  • 🟡 moderate axios (>=0.28.0 <0.33.0) — Axios: Excessive recursion in formDataToJSON can cause denial of service advisory
  • 🟡 moderate axios (>=1.15.2 <1.18.0) — Axios: Prototype pollution auth subfields can inject Basic auth advisory
  • 🟡 moderate axios (>=1.0.0 <1.18.0) — Axios: Deep formToJSON Key Recursion Can Cause Denial of Service advisory
  • 🟡 moderate axios (>=0.28.0 <0.33.0) — Axios: Deep formToJSON Key Recursion Can Cause Denial of Service advisory
  • 🟡 moderate axios (>=1.7.0 <1.18.0) — Axios: Fetch adapter ReadableStream uploads bypass maxBodyLength advisory
  • 🟡 moderate axios (<0.33.0) — Axios: Prototype pollution gadgets can alter axios request construction advisory
  • 🟡 moderate axios (>=1.0.0 <1.18.0) — Axios: Prototype pollution gadgets can alter axios request construction advisory
  • 🟡 moderate axios (>=0.31.0 <0.33.0) — Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios advisory
  • 🟡 moderate axios (>=1.15.0 <1.18.0) — Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios advisory
  • 🟡 moderate axios (>=1.15.1 <1.18.0) — Axios form serializer maxDepth bypass via {} metatoken advisory
  • 🟡 moderate axios (>=0.31.1 <0.33.0) — Axios form serializer maxDepth bypass via {} metatoken advisory
  • 🟡 moderate axios (>=1.0.0 <1.18.0) — Axios: Nested axios option objects can consume polluted prototype values advisory
  • 🟡 moderate axios (>=0.8.0 <0.33.0) — Axios: Nested axios option objects can consume polluted prototype values advisory
  • 🟡 moderate axios (>=1.13.0 <1.18.0) — Axios: HTTP/2 streamed uploads bypass maxBodyLength advisory
  • 🟡 moderate bn.js (>=5.0.0 <5.2.3) — bn.js affected by an infinite loop advisory
  • 🟡 moderate bn.js (<4.12.3) — bn.js affected by an infinite loop advisory
  • 🟡 moderate boolean (3.2.0) — Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
  • 🟡 moderate brace-expansion (<1.1.13) — brace-expansion: Zero-step sequence causes process hang and memory exhaustion advisory
  • 🟡 moderate brace-expansion (>=2.0.0 <2.0.3) — brace-expansion: Zero-step sequence causes process hang and memory exhaustion advisory
  • 🟡 moderate brace-expansion (>=4.0.0 <5.0.5) — brace-expansion: Zero-step sequence causes process hang and memory exhaustion advisory
  • 🟡 moderate brace-expansion (>=5.0.0 <5.0.6) — brace-expansion: Large numeric range defeats documented max DoS protection advisory
  • 🟡 moderate core-js (2.6.12) — core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
  • 🟡 moderate dompurify (>=3.1.3 <3.2.7) — DOMPurify contains a Cross-site Scripting vulnerability advisory
  • 🟡 moderate dompurify (>=3.1.3 <=3.3.1) — DOMPurify contains a Cross-site Scripting vulnerability advisory
  • 🟡 moderate dompurify (<3.4.0) — DOMPurify: FORBID_TAGS bypassed by function-based ADD_TAGS predicate (asymmetry with FORBID_ATTR fix) advisory
  • 🟡 moderate dompurify (>=1.0.10 <3.4.0) — DOMPurify has a SAFE_FOR_TEMPLATES bypass in RETURN_DOM mode advisory
  • 🟡 moderate dompurify (>=3.0.1 <3.4.0) — DOMPurify: Prototype Pollution to XSS Bypass via CUSTOM_ELEMENT_HANDLING Fallback advisory
  • 🟡 moderate dompurify (<=3.4.5) — DOMPurify: Cross-realm IN_PLACE sanitization leaves executable markup intact via realm-bound instanceof checks advisory
  • 🟡 moderate dompurify (<=3.4.5) — DOMPurify: IN_PLACE mode preserves attributes of a clobbered root element, allowing XSS via attacker-controlled root DOM advisory
  • 🟡 moderate dompurify (<=3.4.6) — DOMPurify IN_PLACE Sanitization Bypass via Attached Shadow Root Inside .content advisory
  • 🟡 moderate dompurify (<=3.4.10) — DOMPurify: Permanent ALLOWED_ATTR pollution via setConfig() bypassing the hook clone-guard (incomplete fix of the 3.4.7 hook-pollution patch) advisory
  • 🟡 moderate dompurify (<3.4.7) — DOMPurify: Hook mutation of data.allowedTags / data.allowedAttributes permanently pollutes DEFAULT_ALLOWED_TAGS / DEFAULT_ALLOWED_ATTR advisory
  • 🟡 moderate dompurify (<=3.3.3) — DOMPurify's ADD_TAGS function form bypasses FORBID_TAGS due to short-circuit evaluation advisory
  • 🟡 moderate dompurify (<=3.3.1) — DOMPurify ADD_ATTR predicate skips URI validation advisory
  • 🟡 moderate dompurify (<=3.3.1) — DOMPurify USE_PROFILES prototype pollution allows event handlers advisory
  • 🟡 moderate dompurify (<3.3.2) — DOMPurify is vulnerable to mutation-XSS via Re-Contextualization advisory
  • 🟡 moderate eslint (8.57.1) — This version is no longer supported. Please see https://eslint.org/version-support for other options.
  • 🟡 moderate fast-xml-parser (>=5.0.0 <5.5.7) — Entity Expansion Limits Bypassed When Set to Zero Due to JavaScript Falsy Evaluation in fast-xml-parser advisory
  • 🟡 moderate fast-xml-parser (>=4.0.0-beta.3 <4.5.5) — Entity Expansion Limits Bypassed When Set to Zero Due to JavaScript Falsy Evaluation in fast-xml-parser advisory
  • 🟡 moderate fast-xml-parser (<5.7.0) — fast-xml-parser XMLBuilder: XML Comment and CDATA Injection via Unescaped Delimiters advisory
  • 🟡 moderate file-type (>=13.0.0 <21.3.1) — file-type affected by infinite loop in ASF parser on malformed input with zero-size sub-header advisory
  • 🟡 moderate follow-redirects (<=1.15.11) — follow-redirects leaks Custom Authentication Headers to Cross-Domain Redirect Targets advisory
  • 🟡 moderate glob (7.2.3) — Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
  • 🟡 moderate handlebars (>=4.0.0 <4.7.9) — Handlebars.js has Prototype Pollution Leading to XSS through Partial Template Injection advisory
  • 🟡 moderate handlebars (>=4.6.0 <=4.7.8) — Handlebars.js has a Prototype Method Access Control Gap via Missing lookupSetter Blocklist Entry advisory
  • 🟡 moderate har-validator (5.1.5) — this library is no longer supported
  • 🟡 moderate hono (<4.12.12) — Hono missing validation of cookie name on write path in setCookie() advisory
  • 🟡 moderate hono (<4.12.12) — Hono: Non-breaking space prefix bypass in cookie name handling in getCookie() advisory
  • 🟡 moderate hono (>=4.0.0 <=4.12.11) — Hono: Path traversal in toSSG() allows writing files outside the output directory advisory
  • 🟡 moderate hono (<4.12.12) — Hono: Middleware bypass via repeated slashes in serveStatic advisory
  • 🟡 moderate hono (<4.12.12) — Hono has incorrect IP matching in ipRestriction() for IPv4-mapped IPv6 addresses advisory
  • 🟡 moderate hono (<4.12.18) — Hono has CSS Declaration Injection via Style Object Values in JSX SSR advisory
  • 🟡 moderate hono (<4.12.18) — Hono's Cache Middleware ignores Vary: Authorization / Vary: Cookie leading to cross-user cache leakage advisory
  • 🟡 moderate hono (<4.12.16) — Hono: bodyLimit() can be bypassed for chunked / unknown-length requests advisory
  • 🟡 moderate hono (<4.12.16) — hono/jsx has Unvalidated JSX Tag Names that May Allow HTML Injection advisory
  • 🟡 moderate hono (<4.12.21) — Hono: IP Restriction bypasses static deny rules for non-canonical IPv6 advisory
  • 🟡 moderate hono (<4.12.21) — Hono: Cookie helper does not sanitize sameSite and priority, allowing Set-Cookie injection advisory
  • 🟡 moderate hono (<4.12.21) — Hono: JWT middleware accepts any Authorization scheme, not only Bearer advisory
  • 🟡 moderate hono (<4.12.21) — Hono: app.mount() strips mount prefix using undecoded path, causing incorrect routing for percent-encoded paths advisory
  • 🟡 moderate hono (<4.12.14) — hono Improperly Handles JSX Attribute Names Allows HTML Injection in hono/jsx SSR advisory
  • 🟡 moderate hono (<4.12.25) — hono: Body Limit Middleware can be bypassed on AWS Lambda by understating Content-Length advisory
  • 🟡 moderate hono (<4.12.25) — hono: Lambda@Edge adapter keeps only the last value of a repeated request header, dropping the rest advisory
  • 🟡 moderate hono (<4.12.25) — hono: Path traversal in serve-static on Windows via encoded backslash (%5C) advisory
  • 🟡 moderate hono (<4.12.25) — hono: AWS Lambda adapter merges multiple Set-Cookie headers into one value, dropping cookies on ALB single-header and Lattice advisory
  • 🟡 moderate hono (>=4.3.3 <4.12.27) — Hono: API Gateway v1 adapter can drop a distinct repeated request header value during de-duplication advisory
  • 🟡 moderate hono (>=4.11.8 <4.12.27) — hono/jsx does not isolate context per request, leading to cross-request data disclosure advisory
  • 🟡 moderate hono (>=4.0.0 <4.12.27) — Hono: Server-Side XSS via JSX Escaping Bypass in cx() Utility advisory
  • 🟡 moderate http-proxy-middleware (>=3.0.0 <3.0.5) — http-proxy-middleware allows fixRequestBody to proceed even if bodyParser has failed advisory
  • 🟡 moderate http-proxy-middleware (>=3.0.0 <3.0.4) — http-proxy-middleware can call writeBody twice because "else if" is not used advisory
  • 🟡 moderate http-proxy-middleware (>=0.16.0 <2.0.10) — http-proxy-middleware router host+path substring matching allows Host-header-driven backend routing bypass advisory
  • 🟡 moderate http-proxy-middleware (>=3.0.0 <3.0.6) — http-proxy-middleware router host+path substring matching allows Host-header-driven backend routing bypass advisory
  • 🟡 moderate inflight (1.0.6) — This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
  • 🟡 moderate ip-address (<=10.1.0) — ip-address has XSS in Address6 HTML-emitting methods advisory
  • 🟡 moderate js-yaml (<3.15.0) — JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases advisory
  • 🟡 moderate js-yaml (>=4.0.0 <=4.1.1) — JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases advisory
  • 🟡 moderate launch-editor (<=2.14.0) — launch-editor: NTLMv2 hash disclosure via UNC path handling on Windows advisory
  • 🟡 moderate lodash (<=4.17.23) — lodash vulnerable to Prototype Pollution via array path bypass in _.unset and _.omit advisory
  • 🟡 moderate lodash-es (<=4.17.23) — lodash vulnerable to Prototype Pollution via array path bypass in _.unset and _.omit advisory
  • 🟡 moderate lodash-es (>=4.0.0 <=4.17.22) — Lodash has Prototype Pollution Vulnerability in _.unset and _.omit functions advisory
  • 🟡 moderate lodash.get (4.4.2) — This package is deprecated. Use the optional chaining (?.) operator instead.
  • 🟡 moderate lodash.isequal (4.5.0) — This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
  • 🟡 moderate markdown-it (>=13.0.0 <14.1.1) — markdown-it is has a Regular Expression Denial of Service (ReDoS) advisory
  • 🟡 moderate markdown-it (<=14.1.1) — markdown-it: Quadratic complexity DoS in smartquotes rule via replaceAt string operations advisory
  • 🟡 moderate morgan (>=1.2.0 <=1.10.1) — morgan vulnerable to Log Forging via unneutralized control characters in :remote-user advisory
  • 🟡 moderate nanoid (<3.3.8) — Predictable results in nanoid generation when given non-integer values advisory
  • 🟡 moderate node-domexception (1.0.0) — Use your platform's native DOMException instead
  • 🟡 moderate path-to-regexp (>=8.0.0 <8.4.0) — path-to-regexp vulnerable to Regular Expression Denial of Service via multiple wildcards advisory
  • 🟡 moderate picomatch (<2.3.2) — Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching advisory
  • 🟡 moderate picomatch (>=4.0.0 <4.0.4) — Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching advisory
  • 🟡 moderate postcss (<8.5.10) — PostCSS has XSS via Unescaped </style> in its CSS Stringify Output advisory
  • 🟡 moderate prebuild-install (7.1.3) — No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
  • 🟡 moderate prismjs (<1.30.0) — PrismJS DOM Clobbering vulnerability advisory
  • 🟡 moderate qs (<6.14.1) — qs's arrayLimit bypass in its bracket notation allows DoS via memory exhaustion advisory
  • 🟡 moderate qs (>=6.11.1 <=6.15.1) — qs has a remotely triggerable DoS: qs.stringify crashes with TypeError on null/undefined entries in comma-format arrays when encodeValuesOnly is set advisory
  • 🟡 moderate react-beautiful-dnd (13.1.1) — react-beautiful-dnd is now deprecated. Context and options: react-beautiful-dnd is now deprecated atlassian/react-beautiful-dnd#2672
  • 🟡 moderate request (<=2.88.2) — Server-Side Request Forgery in Request advisory
  • 🟡 moderate rimraf (3.0.2) — Rimraf versions prior to v4 are no longer supported
  • 🟡 moderate stable (0.1.8) — Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility
  • 🟡 moderate tar (<=7.5.20) — node-tar: Uncontrolled recursion in mapHas/filesFilter allows uncatchable stack-overflow DoS via crafted long-path tar with member selection advisory
  • 🟡 moderate tough-cookie (<4.1.3) — tough-cookie Prototype Pollution vulnerability advisory
  • 🟡 moderate uuid (<11.1.1) — uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided advisory
  • 🟡 moderate webpack-dev-server (<=5.2.3) — webpack-dev-server vulnerable to cross-origin source code exposure on non-HTTPS origins advisory
  • 🟡 moderate webpack-dev-server (<5.2.5) — webpack-dev-server vulnerable to HMR WebSocket interception via permissive user proxies advisory
  • 🟡 moderate webpack-dev-server (<=5.2.5) — webpack-dev-server vulnerable to cross-site request forgery via internal developer endpoints advisory
  • 🟡 moderate webpack-dev-server (<=5.2.5) — webpack-dev-server vulnerable to denial of service via a malformed Host or Origin header advisory
  • 🟡 moderate yaml (>=1.0.0 <1.10.3) — yaml is vulnerable to Stack Overflow via deeply nested YAML collections advisory
  • 🟡 moderate yaml (>=2.0.0 <2.8.3) — yaml is vulnerable to Stack Overflow via deeply nested YAML collections advisory
  • 🔵 low @babel/core (<=7.29.0) — @babel/core: Arbitrary File Read via sourceMappingURL Comment advisory
  • 🔵 low @backstage/backend-defaults (<0.12.2) — Backstage has a Possible SSRF when reading from allowed URL's in backend.reading.allow advisory
  • 🔵 low @backstage/integration (<=1.20.0) — Backstage vulnerable to potential reading of SCM URLs using built in token advisory
  • 🔵 low @smithy/config-resolver (<4.4.0) — AWS SDK for JavaScript v3 adopted defense in depth enhancement for region parameter value advisory
  • 🔵 low @tootallnate/once (<2.0.1) — @tootallnate/once vulnerable to Incorrect Control Flow Scoping advisory
  • 🔵 low body-parser (>=2.0.0 <2.3.0) — body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement advisory
  • 🔵 low body-parser (<1.20.6) — body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement advisory
  • 🔵 low brace-expansion (>=1.0.0 <=1.1.11) — brace-expansion Regular Expression Denial of Service vulnerability advisory
  • 🔵 low brace-expansion (>=2.0.0 <=2.0.1) — brace-expansion Regular Expression Denial of Service vulnerability advisory
  • 🔵 low cookie (<0.7.0) — cookie accepts cookie name, path, and domain with out of bounds characters advisory
  • 🔵 low diff (>=4.0.0 <4.0.4) — jsdiff has a Denial of Service vulnerability in parsePatch and applyPatch advisory
  • 🔵 low diff (>=5.0.0 <5.2.2) — jsdiff has a Denial of Service vulnerability in parsePatch and applyPatch advisory
  • 🔵 low dompurify (<=3.4.11) — DOMPurify: CUSTOM_ELEMENT_HANDLING bypasses afterSanitizeElements for allowed custom elements. advisory
  • 🔵 low dompurify (<3.4.9) — DOMPurify: Trusted Types policy survives clearConfig() and can poison later RETURN_TRUSTED_TYPE output advisory
  • 🔵 low dompurify (>=3.0.0 <=3.4.7) — DOMPurify: SAFE_FOR_TEMPLATES bypass - template expressions survive sanitization inside content when using DOM output modes advisory
  • 🔵 low dompurify (<=3.4.6) — DOMPurify: IN_PLACE mode trusts attacker-controlled nodeName on live non-form nodes, allowing script retention and XSS via attacker-supplied DOM objects advisory
  • 🔵 low elliptic (<6.6.0) — Valid ECDSA signatures erroneously rejected in Elliptic advisory
  • 🔵 low elliptic (<=6.6.1) — Elliptic Uses a Cryptographic Primitive with a Risky Implementation advisory
  • 🔵 low esbuild (>=0.27.3 <0.28.1) — esbuild allows arbitrary file read when running the development server on Windows advisory
  • 🔵 low handlebars (>=4.0.0 <=4.7.8) — Handlebars.js has a Property Access Validation Bypass in container.lookup advisory
  • 🔵 low hono (<4.12.18) — Hono has improper validation of NumericDate claims (exp, nbf, iat) in JWT verify() advisory
  • 🔵 low on-headers (<1.1.0) — on-headers is vulnerable to http response header manipulation advisory
  • 🔵 low tmp (<=0.2.3) — tmp allows arbitrary temporary file / directory write via symbolic link dir parameter advisory

@marians
marians merged commit 7654695 into main Jul 27, 2026
13 checks passed
@marians
marians deleted the agent-platform-kagent-client branch July 27, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant