Skip to content

Commit 03662b1

Browse files
fullsend-ai-coder[bot]gabemonteroclaude
authored
feat(#3647): auth pivot, dev scaffold, entity provider fixes (#3648)
* feat(#3647): auth pivot, dev scaffold, entity provider fixes Pivot Kagenti authentication from per-user RFC 8693 token exchange to service-account OAuth2 Client Credentials Grant. Add dev scaffold (app-config.yaml, packages/backend, load-secrets.sh). Fix entity provider API URLs and response handling. Auth pivot (config.d.ts, schemas.ts, schemas.test.ts): - Replace tokenExchange.{enabled,audience,userTokenHeader} with flat tokenEndpoint, clientId, clientSecret, tokenExpiryBufferSeconds - Bump BOOST_CONFIG_SCHEMA_VERSION to 2 - clientSecret marked sensitive: true with @visibility secret - tokenExpiryBufferSeconds: .number().int().min(0).optional() - Add validation tests for new fields Entity provider fixes (kagenti-entity-provider): - New KeycloakAuthClient: OAuth2 Client Credentials Grant with token caching and configurable expiry buffer - Fix API URLs from /a2a/ to /api/v1/ for agents and tools - Add unwrapItems helper for {items:T[]} and T[] responses - Pass optional authClient to entity providers - Module reads boost.kagenti.auth.* and constructs auth client - Add tests for auth, URL patterns, response unwrapping Dev scaffold: - app-config.yaml with guest auth, SQLite, dev providers - packages/backend host with metrics shim and boost plugins - scripts/load-secrets.sh for K8s dev secret loading Spec updates across 14 specification/openspec files: - Rename "Per-User Identity Delegation" to "Service-Account Keycloak Authentication" throughout - Add boost-responses-api-toolkit and boost-toolscope to workspace structure docs - Add Local Development section to boost-context.md - Update staged-issues.md issue 13 and add issue 16 URL TODO comments added to mcp/routes.ts and skills/routes.ts for future catalog cross-reference. Closes #3647 * fix(kagenti-entity-provider): address review findings for PR 3648 - Remove fail-open auth pattern: let getBearerToken() throw propagate to refresh() catch which serves cached entities (not unauthenticated) - Wire tokenExpiryBufferSeconds from config to KeycloakAuthClient - Clamp token expiry to prevent hot-loop when buffer >= token lifetime - Validate access_token exists in Keycloak response before caching - Remove dead auth field from KagentiEntityProviderConfig type - Update tests to expect auth-failure-serves-cached behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(boost): address review findings from second review agent run - Add partial auth config detection with warning log in readKagentiAuthConfig when only 1-2 of 3 required fields (tokenEndpoint, clientId, clientSecret) are present - Rename KeycloakTokenManager to KeycloakAuthClient across all docs and specs for naming consistency with the actual code class name - Fix stale class name and file path in pluggable-ai-platform-architecture PRD cache migration table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com> * docs(boost): update issue 3309 and staged-issues with PR 3648 completions Mark tasks 7.1, 7.2, 7.4, 7.5a as complete in tasks.md and staged-issues.md. Split task 7.5 into 7.5a (entity providers, done) and 7.5b (KagentiApiClient, remaining). Add note clarifying that remaining auth tasks target boost-backend-module-kagenti. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(boost): add issue 17 (KeycloakAuthClient cacheService migration) to staged-issues Tracks #3654 — refactor KeycloakAuthClient to use Backstage cacheService instead of in-memory token cache, aligning with design principle 1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(boost): harden dev scaffold security defaults - Remove dangerouslyAllowOutsideDevelopment from guest auth config; Backstage already allows guest auth in development mode by default - Make NODE_TLS_REJECT_UNAUTHORIZED overridable in load-secrets.sh so operators can enforce TLS verification when not using self-signed certs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(boost): clarify X-Backstage-User scope in auth specs X-Backstage-User header only applies to user-initiated requests via KagentiApiClient (chat, agent operations), not entity provider background polling which has no user context. Updated access-control spec, security design doc, and boost-context to make this distinction explicit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: gabemontero <gmontero@redhat.com> --------- Signed-off-by: gabemontero <gmontero@redhat.com> Co-authored-by: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Co-authored-by: gabemontero <gmontero@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e2df0a3 commit 03662b1

33 files changed

Lines changed: 990 additions & 150 deletions

File tree

workspaces/boost/app-config.yaml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
app:
2+
title: Boost Dev
3+
baseUrl: http://localhost:3000
4+
5+
organization:
6+
name: Red Hat
7+
8+
backend:
9+
baseUrl: http://localhost:7007
10+
listen:
11+
port: 7007
12+
database:
13+
client: better-sqlite3
14+
connection: ':memory:'
15+
auth:
16+
dangerouslyDisableDefaultAuthPolicy: true
17+
18+
auth:
19+
providers:
20+
guest: {}
21+
22+
catalog:
23+
rules:
24+
- allow:
25+
- Component
26+
- Resource
27+
- API
28+
- System
29+
- Domain
30+
- Location
31+
- Template
32+
- Group
33+
- User
34+
35+
boost:
36+
security:
37+
mode: 'development-only-no-auth'
38+
39+
model:
40+
baseUrl: ${BOOST_MODEL_BASE_URL:-http://localhost:8080/v1}
41+
name: ${BOOST_MODEL:-default}
42+
43+
providers:
44+
llamastack:
45+
baseUrl: ${LLAMASTACK_BASE_URL:-http://localhost:8321}
46+
47+
kagenti:
48+
baseUrl: ${KAGENTI_BASE_URL:-http://localhost:8080}
49+
namespaces:
50+
- ${KAGENTI_NAMESPACE:-default}
51+
52+
kagenti:
53+
auth:
54+
tokenEndpoint: ${KAGENTI_TOKEN_ENDPOINT:-}
55+
clientId: ${KAGENTI_CLIENT_ID:-}
56+
clientSecret: ${KAGENTI_CLIENT_SECRET:-}
57+
58+
entityProviders:
59+
kagenti:
60+
baseUrl: ${KAGENTI_BASE_URL:-http://localhost:8080}
61+
namespaces:
62+
- ${KAGENTI_NAMESPACE:-default}
63+
agentRefreshIntervalSeconds: 300
64+
toolRefreshIntervalSeconds: 300

workspaces/boost/openspec/changes/platform-operations-deployment/specs/runtime-config/spec.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,14 @@ The following new features MUST have runtime configuration fields as specified b
103103

104104
#### Scenario: Token exchange configuration
105105

106-
- **WHEN** the admin configures per-user Kagenti auth
106+
- **WHEN** the admin configures Kagenti service-account auth
107107
- **THEN** the following fields are available:
108108
| Field | Scope | Description |
109109
|---|---|---|
110-
| `boost.kagenti.auth.tokenExchange.enabled` | yaml-only | Enable RFC 8693 token exchange |
111-
| `boost.kagenti.auth.tokenExchange.audience` | yaml-only | Target audience for exchanged token |
112-
| `boost.kagenti.auth.tokenExchange.userTokenHeader` | yaml-only | Header containing user OIDC token |
110+
| `boost.kagenti.auth.tokenEndpoint` | yaml-only | Keycloak token endpoint URL |
111+
| `boost.kagenti.auth.clientId` | yaml-only | OAuth2 client ID |
112+
| `boost.kagenti.auth.clientSecret` | yaml-only | OAuth2 client secret (sensitive) |
113+
| `boost.kagenti.auth.tokenExpiryBufferSeconds` | yaml-only | Seconds before expiry to refresh (default: 60) |
113114

114115
#### Scenario: Credential encryption
115116

workspaces/boost/openspec/changes/platform-operations-deployment/tasks.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
- [ ] 2.2 Generate `config.d.ts` types from Zod schemas
2020
- [ ] 2.3 Validate all config writes (YAML and DB) via Zod `.parse()` — no hand-written validators
2121
- [ ] 2.4 Annotate each field with `configScope`: `yaml-only`, `db-overridable`, or `db-only`
22-
- [ ] 2.5 Add Zod schemas for new config fields: agentApproval, skillsMarketplace, tokenExchange, DevSpaces credentials
22+
- [ ] 2.5 Add Zod schemas for new config fields: agentApproval, skillsMarketplace, Keycloak service-account auth, DevSpaces credentials
2323
- [ ] 2.6 Admin UI shows only DB-overridable and DB-only fields
2424
- [ ] 2.7 Implement credential encryption for sensitive DB-stored values (DevSpaces tokens)
2525
- [ ] 2.8 Implement schema version tracking: store schema version alongside DB values, re-validate on startup

workspaces/boost/openspec/changes/pluggable-ai-platform-architecture/design.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Boost implements the provider abstraction as modular RHDH dynamic plugins from t
1818
- Modifying chat interaction behavior or message rendering
1919
- Rewriting the ADK orchestration library
2020
- Creating catalog entities for models/agents (covered in agent-creation-discovery change)
21-
- Per-user token exchange (covered in security-safety-governance change)
21+
- Kagenti service-account auth (covered in security-safety-governance change — OAuth2 Client Credentials Grant adopted)
2222

2323
## Decisions
2424

workspaces/boost/openspec/changes/pluggable-ai-platform-architecture/specs/provider-packaging/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ The Kagenti provider MUST be packaged as an independent Backstage backend module
6161

6262
#### Scenario: Keycloak token cache uses Backstage cacheService
6363

64-
- **WHEN** the Kagenti module caches Keycloak access tokens (currently `KeycloakTokenManager`)
64+
- **WHEN** the Kagenti module caches Keycloak access tokens (currently `KeycloakAuthClient`)
6565
- **THEN** it uses `coreServices.cache` with TTL derived from token expiry
6666
- **AND** the token cache is Redis-backed in production for multi-instance safety
6767

workspaces/boost/openspec/changes/security-safety-governance/design.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
Boost builds the security and governance layer with Backstage fine-grained permissions as the sole authorization mechanism from day one. The Augment reference prototype's governance system grew into a parallel authorization layer where authorization decisions bypassed `permissions.authorize()`. Boost avoids this entirely.
66

7-
Boost also implements RFC 8693 token exchange for per-user identity delegation to Kagenti from the start, enabling per-user audit trails.
7+
Boost also implements OAuth2 Client Credentials Grant via `KeycloakAuthClient` for service-account authentication to Kagenti, with user identity propagated via `X-Backstage-User` header for audit trails.
88

99
## Goals
1010

1111
- Implement 16 fine-grained Backstage permissions as the sole authorization mechanism
1212
- Add conditional permission rules for ownership (IS_OWNER), separation of duties (IS_NOT_CREATOR), and lifecycle stage gating (HAS_LIFECYCLE_STAGE)
13-
- Implement RFC 8693 token exchange for per-user Kagenti identity
13+
- Implement OAuth2 Client Credentials Grant for Kagenti service-account auth
1414
- Use `development-only-no-auth` as the only dev security mode name (no legacy aliases)
1515
- Add CSRF protection and credential encryption
1616
- Export all permissions from `boost-common`
@@ -40,20 +40,20 @@ These rules are evaluated against loaded resources via `createConditionalDecisio
4040

4141
The `IS_NOT_CREATOR` permission rule is the primary enforcement mechanism. A route-level guard remains as defense-in-depth (belt and suspenders). Both layers are active in `security.mode === 'full'`.
4242

43-
### Decision 4: Per-user token exchange is backend-only with graceful fallback
43+
### Decision 4: Service-account auth with KeycloakAuthClient
4444

45-
`TokenExchangeManager` reads the user's OIDC token from a configurable request header (injected by auth proxy), exchanges it via RFC 8693, caches per-user, and deduplicates concurrent exchanges. All failures fall back silently to the shared service-account token — no request is ever blocked by token exchange issues. This is deliberately conservative: token exchange enhances audit trails and per-user authorization but must never degrade availability.
45+
`KeycloakAuthClient` acquires tokens via OAuth2 Client Credentials Grant, caches them with a configurable expiry buffer (`tokenExpiryBufferSeconds`, default: 60), and automatically refreshes before expiry. On 401 responses, the token is refreshed and the request retried once (max-1-retry). For user-initiated requests via `KagentiApiClient`, user identity is propagated via `X-Backstage-User` header for audit trails; entity provider background polling omits this header (no user context). This is deliberately simple: service-account auth provides consistent authentication without per-user token management complexity.
4646

4747
### Decision 5: Separation of authorization concerns
4848

4949
Three non-overlapping authorization layers:
5050

5151
- **Backstage** governs: UI visibility, agent lifecycle governance, ownership, approval workflows, admin operations
52-
- **Kagenti** governs: agent specs, tools, runtime operations — via per-user exchanged token when enabled
52+
- **Kagenti** governs: agent specs, tools, runtime operations — via service-account token with user identity in `X-Backstage-User` header
5353
- **Kubernetes** governs: pod/deployment operations, namespace scoping, SPIRE mTLS
5454

5555
## Risks
5656

5757
- **RBAC policy complexity:** 16 permissions with conditions is more complex than 2. Mitigated by sensible defaults — `boost.access` as top-level gate and `boost.admin` available for coarse control.
58-
- **Token exchange reliability:** Keycloak availability becomes a dependency. Mitigated by graceful fallback to service-account token on any failure.
58+
- **Keycloak availability:** Keycloak becoming unavailable blocks Kagenti API calls. Mitigated by token caching with configurable expiry buffer, reducing the number of token requests.
5959
- **SonataFlow trust boundary:** Callbacks bypass self-approval prevention via header. Callback identity verification should be implemented to close this gap.

workspaces/boost/openspec/changes/security-safety-governance/proposal.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ Enterprise AI platforms must treat security, safety, and governance as foundatio
2121
### Identity & Authentication
2222

2323
- RBAC via Keycloak OIDC + Backstage permissions (`boost.access`, `boost.admin` as top-level gates)
24-
- RFC 8693 token exchange for per-user Kagenti identity delegation
25-
- Graceful fallback to service-account token on any exchange failure
24+
- OAuth2 Client Credentials Grant for Kagenti service-account authentication via `KeycloakAuthClient`
25+
- Token caching with configurable expiry buffer and max-1-retry on 401
2626
- MCP 4-level auth chain
2727
- Kagenti SPIRE integration for infrastructure mTLS
2828

@@ -45,4 +45,4 @@ Enterprise AI platforms must treat security, safety, and governance as foundatio
4545
- `plugins/boost-common/src/permissions.ts` — 16 permission definitions with resource types
4646
- `plugins/boost-backend/src/middleware/security.ts``authorizeLifecycleAction` middleware
4747
- `plugins/boost-frontend/src/components/SecurityGate.tsx` — granular permission checks
48-
- `plugins/boost-backend/src/services/TokenExchangeManager.ts`RFC 8693 implementation
48+
- `plugins/kagenti-entity-provider/src/providers/kagentiAuth.ts``KeycloakAuthClient` (OAuth2 Client Credentials Grant)

workspaces/boost/openspec/changes/security-safety-governance/specs/access-control/spec.md

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,41 +81,63 @@ Inference responses are not stored on the server when ZDR is enabled.
8181

8282
## ADDED Requirements
8383

84-
### Requirement: Per-User Kagenti Identity via Token Exchange
84+
### Requirement: Service-Account Keycloak Authentication for Kagenti
8585

86-
User identity MUST be delegated to Kagenti via RFC 8693 OAuth2 Token Exchange so agent operations are authorized per-user.
86+
Kagenti API calls MUST be authenticated via OAuth2 Client Credentials Grant using `KeycloakAuthClient` for service-account authentication. For user-initiated requests (chat, agent operations via `KagentiApiClient`), user identity is propagated via the `X-Backstage-User` header for audit purposes. Entity provider background polling has no user context and omits this header.
8787

88-
#### Scenario: Token exchange enabled
88+
#### Scenario: Token acquisition
8989

90-
- **WHEN** `boost.kagenti.auth.tokenExchange.enabled` is `true`
91-
- **AND** a user's OIDC token is available via the configured header (default: `X-Forwarded-Access-Token`)
92-
- **THEN** `TokenExchangeManager` exchanges the user's token for a Kagenti-scoped token via RFC 8693
93-
- **AND** the exchanged token is cached per-user with TTL from token expiry
94-
- **AND** concurrent exchanges for the same user are deduplicated
95-
- **AND** the per-user token is used for all Kagenti API calls on behalf of that user
90+
- **WHEN** `boost.kagenti.auth.tokenEndpoint`, `clientId`, and `clientSecret` are all configured
91+
- **THEN** `KeycloakAuthClient` acquires a bearer token via OAuth2 Client Credentials Grant
92+
- **AND** the token is cached until `expires_in - tokenExpiryBufferSeconds` seconds
93+
- **AND** the bearer token is included in all Kagenti API requests as `Authorization: Bearer <token>`
9694

97-
#### Scenario: Token exchange graceful fallback
95+
#### Scenario: Streaming lifecycle with token refresh
9896

99-
- **WHEN** token exchange fails (missing header, Keycloak error, exchange failure, disabled config)
100-
- **THEN** the system silently falls back to the shared service-account token
101-
- **AND** no request is blocked due to token exchange failure
102-
- **AND** the fallback is logged for debugging
97+
- **WHEN** a cached token is about to expire (within `tokenExpiryBufferSeconds`)
98+
- **THEN** a fresh token is acquired before the next API call
99+
- **AND** in-flight requests continue with the previously cached token
103100

104-
#### Scenario: Token exchange configuration
101+
#### Scenario: 401 retry with max-1-retry constraint
105102

106-
- **WHEN** token exchange is configured
107-
- **THEN** the following config is used:
103+
- **WHEN** a Kagenti API call returns HTTP 401
104+
- **THEN** the cached token is invalidated and a fresh token is acquired
105+
- **AND** the request is retried with the new token
106+
- **AND** if the retried request also returns 401, the error is propagated to the caller
107+
108+
#### Scenario: User identity propagation (KagentiApiClient only)
109+
110+
- **WHEN** a user-initiated Kagenti API call is made via `KagentiApiClient` (chat, agent operations)
111+
- **THEN** the `X-Backstage-User` header is set to the user's Backstage identity
112+
- **AND** the service-account bearer token is used for authentication (not the user's token)
113+
- **AND** entity provider background polling omits this header (no user context available)
114+
115+
#### Scenario: Service-account auth configuration
116+
117+
- **WHEN** Kagenti auth is configured
118+
- **THEN** the following config keys are used:
108119
| Key | Default | Description |
109120
|---|---|---|
110-
| `boost.kagenti.auth.tokenExchange.enabled` | `false` | Enable per-user token exchange |
111-
| `boost.kagenti.auth.tokenExchange.audience` || Target audience for exchanged token |
112-
| `boost.kagenti.auth.tokenExchange.userTokenHeader` | `X-Forwarded-Access-Token` | Header containing user's OIDC token |
121+
| `boost.kagenti.auth.tokenEndpoint` || Keycloak token endpoint URL |
122+
| `boost.kagenti.auth.clientId` || OAuth2 client ID |
123+
| `boost.kagenti.auth.clientSecret` || OAuth2 client secret (visibility: secret) |
124+
| `boost.kagenti.auth.tokenExpiryBufferSeconds` | `60` | Seconds before expiry to refresh |
125+
126+
#### Scenario: Kagenti REST API endpoints
127+
128+
- **WHEN** fetching agents from Kagenti
129+
- **THEN** the URL pattern is `GET /api/v1/agents?namespace={ns}` (not `/a2a/`)
130+
- **AND** the response is unwrapped via `unwrapItems` to handle both `{ items: T[] }` and `T[]` shapes
131+
132+
- **WHEN** fetching tools from Kagenti
133+
- **THEN** the URL pattern is `GET /api/v1/tools?namespace={ns}` (not `/a2a/`)
134+
- **AND** the response is unwrapped via `unwrapItems` to handle both response shapes
113135

114136
#### Scenario: LlamaStack provider unaffected
115137

116-
- **WHEN** token exchange is configured
138+
- **WHEN** Kagenti auth is configured
117139
- **THEN** `ResponsesApiProvider` is not modified — `setUserContext` is optional and not implemented
118-
- **AND** token exchange is Kagenti-specific
140+
- **AND** Keycloak auth is Kagenti-specific
119141

120142
### Requirement: CSRF Protection
121143

workspaces/boost/openspec/changes/security-safety-governance/tasks.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@
4343
- [ ] 6.4 Gate MCP panel with `boost.mcp.manage`
4444
- [ ] 6.5 Gate config panel with `boost.config.manage`
4545

46-
## 7. Token Exchange (P1)
46+
## 7. Keycloak Service-Account Auth (P1)
4747

48-
- [ ] 7.1 Create `TokenExchangeManager` implementing RFC 8693 exchange
49-
- [ ] 7.2 Add per-user token caching with TTL from token expiry
50-
- [ ] 7.3 Add concurrent exchange deduplication
51-
- [ ] 7.4 Add graceful fallback to service-account token on all failures
52-
- [ ] 7.5 Add config schema: `boost.kagenti.auth.tokenExchange.{enabled, audience, userTokenHeader}`
53-
- [ ] 7.6 Integrate into `KagentiApiClient.requestCore()` — inject per-user token when available
54-
- [ ] 7.7 Extract user OIDC token from configurable request header in route handlers
48+
- [x] 7.1 Create `KeycloakAuthClient` implementing OAuth2 Client Credentials Grant
49+
- [x] 7.2 Add token caching with configurable expiry buffer (`tokenExpiryBufferSeconds`, default: 60)
50+
- [ ] 7.3 Add max-1-retry on 401 (refresh token and retry once)
51+
- [x] 7.4 Add config schema: `boost.kagenti.auth.{tokenEndpoint, clientId, clientSecret, tokenExpiryBufferSeconds}`
52+
- [x] 7.5a Integrate into entity providers — inject bearer token (PR #3648)
53+
- [ ] 7.5b Integrate into `KagentiApiClient` — inject bearer token
54+
- [ ] 7.6 Propagate user identity via `X-Backstage-User` header for audit
5555

5656
## 8. CSRF and Credential Security (P2)
5757

@@ -70,7 +70,7 @@
7070
- [ ] 10.2 Verify IS_OWNER blocks non-owner promote/delete/withdraw
7171
- [ ] 10.3 Verify IS_NOT_CREATOR blocks self-approval
7272
- [ ] 10.4 Verify `boost.admin` works as coarse-grained alternative to fine-grained permissions
73-
- [ ] 10.5 Verify token exchange fallback: disabled config → service-account token
74-
- [ ] 10.6 Verify token exchange fallback: Keycloak error → service-account token
75-
- [ ] 10.7 Verify token exchange fallback: missing header → service-account token
73+
- [ ] 10.5 Verify KeycloakAuthClient: token acquisition and caching
74+
- [ ] 10.6 Verify KeycloakAuthClient: max-1-retry on 401
75+
- [ ] 10.7 Verify entity providers include bearer token when auth is configured
7676
- [ ] 10.8 Verify `none` is rejected with a clear error pointing to `development-only-no-auth`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint.backend');

0 commit comments

Comments
 (0)