feat: add read-only mode for GitOps-managed deployments#1346
feat: add read-only mode for GitOps-managed deployments#1346frank-bee wants to merge 1 commit intokagent-dev:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in read-only mode intended for GitOps-managed deployments, enforcing (or signaling) read-only behavior in the API layer and hiding/guarding write-related UI flows in the Next.js frontend.
Changes:
- Backend: introduce
ReadOnlyAuthorizerand wire it viaKAGENT_READ_ONLY=true. - Frontend: add
ReadOnlyProvidercontext driven byNEXT_PUBLIC_READ_ONLY, and hide/disable create/edit/delete controls. - Frontend: add route guards for “new” pages and skip onboarding while in read-only mode; update empty states with GitOps messaging.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| go/internal/httpserver/auth/authz.go | Adds ReadOnlyAuthorizer that rejects non-GET verbs. |
| go/cmd/controller/main.go | Selects ReadOnlyAuthorizer vs NoopAuthorizer via KAGENT_READ_ONLY. |
| ui/src/components/ReadOnlyProvider.tsx | New React context + hook to expose read-only state. |
| ui/src/app/layout.tsx | Wraps app with ReadOnlyProvider; forces dynamic rendering; reads env flag. |
| ui/src/components/Header.tsx | Hides “Create” dropdowns when read-only. |
| ui/src/components/AppInitializer.tsx | Skips onboarding wizard when read-only. |
| ui/src/components/AgentList.tsx | Updates empty state messaging; hides create button when read-only. |
| ui/src/components/AgentCard.tsx | Hides edit/delete controls when read-only. |
| ui/src/components/sidebars/AgentDetailsSidebar.tsx | Hides agent edit button when read-only. |
| ui/src/components/sidebars/ChatItem.tsx | Hides chat deletion option when read-only. |
| ui/src/app/models/page.tsx | Hides “New Model” and edit/delete actions when read-only. |
| ui/src/app/models/new/page.tsx | Redirects away (route guard) when read-only. |
| ui/src/app/agents/new/page.tsx | Redirects away (route guard) when read-only. |
| ui/src/app/servers/page.tsx | Hides server add/delete controls; updates empty state messaging when read-only. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (a *ReadOnlyAuthorizer) Check(ctx context.Context, principal auth.Principal, verb auth.Verb, resource auth.Resource) error { | ||
| if verb != auth.VerbGet { | ||
| return fmt.Errorf("read-only mode: %s operations are not permitted", verb) | ||
| } |
There was a problem hiding this comment.
The current implementation blanket-denies all non-GET verbs for every resource type, but the PR description/test plan indicates chat should remain functional in read-only mode. Consider either (a) using the resource argument to allow specific non-GET operations needed for chat (e.g., sessions/events) while still blocking GitOps-managed resources, or (b) updating the PR description to clarify that backend read-only enforcement only applies to certain resource handlers.
There was a problem hiding this comment.
Not an issue. The authorization check is not applied as global middleware — it's called selectively inside individual handlers via a Check() helper in handlers/helpers.go.
Only these handlers call Check(): Agents, ModelConfig, Memory, ToolServers. Session/chat endpoints (/api/sessions/*, /api/sessions/{id}/events POST) never call the authorizer, so chat works fine in read-only mode.
The current blanket deny on non-GET is correct for the resources that use it (GitOps-managed: agents, models, servers).
| @@ -19,22 +22,26 @@ export const metadata: Metadata = { | |||
| }; | |||
|
|
|||
| export default function RootLayout({ children }: { children: React.ReactNode }) { | |||
| const readOnly = process.env.NEXT_PUBLIC_READ_ONLY === "true"; | |||
|
|
|||
There was a problem hiding this comment.
export const dynamic = "force-dynamic" in the root layout forces the entire app to be dynamically rendered and disables most static optimization/caching. If the only goal is to derive a single boolean flag from env, consider alternatives that avoid globally forcing dynamic rendering (e.g., accept build-time config for the UI, or scope dynamic behavior to a smaller boundary) to reduce performance and hosting costs.
There was a problem hiding this comment.
Intentional. The env var NEXT_PUBLIC_READ_ONLY needs to be read at container runtime (not build time) since this is deployed as a Docker image with configurable env vars. force-dynamic is required for that.
Scoping it per-page would add boilerplate for negligible gain — this is a K8s dashboard app, not a public-facing website where static optimization matters.
| type ReadOnlyAuthorizer struct{} | ||
|
|
||
| func (a *ReadOnlyAuthorizer) Check(ctx context.Context, principal auth.Principal, verb auth.Verb, resource auth.Resource) error { | ||
| if verb != auth.VerbGet { | ||
| return fmt.Errorf("read-only mode: %s operations are not permitted", verb) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
Add unit tests for ReadOnlyAuthorizer to verify it allows GET and rejects POST/PUT/DELETE with the expected error. There are already middleware tests in this package (e.g., authn_test.go), so covering this new authorizer here would help prevent regressions when expanding authz usage across more handlers.
There was a problem hiding this comment.
Already done — see authz_test.go added in this PR with table-driven tests covering both ReadOnlyAuthorizer (allows GET, rejects POST/PUT/DELETE) and NoopAuthorizer (allows all verbs).
Add a read-only mode that disables all write operations (create, update, delete) in both the backend API and the UI. This is useful for GitOps-managed deployments where resources are managed exclusively via Kubernetes CRDs. Backend: - Add ReadOnlyAuthorizer to auth package that only allows GET operations - Wire KAGENT_READ_ONLY env var in controller main.go Frontend: - Add ReadOnlyProvider React context reading NEXT_PUBLIC_READ_ONLY env var - Conditionally hide create/edit/delete controls across all UI components - Add route guards on /agents/new and /models/new pages - Skip onboarding wizard in read-only mode - Show "managed via GitOps" messaging in empty states Closes kagent-dev#1344 Signed-off-by: Frank Bernhardt <Frank.Bernhardt@quantum-machines.co>
4d83eae to
02d3bb0
Compare
Summary
ReadOnlyAuthorizerto the backend that only allows GET operations, activated viaKAGENT_READ_ONLY=trueenv varReadOnlyProviderReact context for the frontend, readingNEXT_PUBLIC_READ_ONLYenv var/agents/newand/models/newpagesThis enables GitOps-managed deployments (FluxCD, ArgoCD) where agents, models, and MCP servers are defined declaratively as Kubernetes CRDs and the UI serves as a read-only dashboard.
Environment Variables
KAGENT_READ_ONLYfalseNEXT_PUBLIC_READ_ONLYfalseFiles Changed
Backend (Go):
go/internal/httpserver/auth/authz.go— newReadOnlyAuthorizergo/cmd/controller/main.go— env var wiringFrontend (Next.js):
ui/src/components/ReadOnlyProvider.tsx— new React contextui/src/app/layout.tsx— wrap app inReadOnlyProvider, addforce-dynamicTest plan
KAGENT_READ_ONLY=true+NEXT_PUBLIC_READ_ONLY=true, verify all write controls are hiddenmake -C go testandmake -C ui buildCloses #1344
Related: #1270, #593